Linux 程序员必修课:命令、脚本、程序、进程与线程的全貌

Linux 程序员必修课:命令、脚本、程序、进程与线程的全貌

1. Linux 命令、脚本、程序、进程与线程的关系对比表

概念 定义 存储形式 执行方式 是否需要进程 是否支持并发 示例
命令 用户输入给 shell 的指令,可以是内置命令或外部命令 直接输入在 shell 中(无固定存储) shell 解析并执行 仅外部命令需要 取决于命令,如 & 可后台运行 lscdecho
脚本 一组命令的集合,存储在文件中,通常是 shell 脚本 文本文件.sh.py 解释执行(bash script.sh 需要(bash 进程) 取决于脚本内容 bash backup.shpython3 script.py
程序 一个 ELF 可执行文件,存储在磁盘上 ELF 二进制文件(如 /bin/ls shell 或其他程序执行 需要(新建进程) 取决于程序设计 /bin/ls/usr/bin/vim
进程 正在运行的程序实例,占用 CPU内存 进程 IDPID)存在于内存 fork()exec() 方式创建 本身就是进程 进程间独立,可并发 sshdnginx
线程 进程内部的执行单元,共享 进程 资源 线程 IDTID)存在于进程内存 pthread_create()std::thread 依赖于进程 支持并发,多线程共享内存 Java 线程、Goroutinepthreads

说明

  1. 命令 vs 脚本

    • 命令是单条指令,脚本是多条命令的集合。
    • 脚本需要解释器(如 bashpython)来执行。
  2. 脚本 vs 程序

    • 脚本是解释执行的(逐行解析),而程序是编译执行的(预先编译成二进制)。
    • 解释执行比编译执行慢,但脚本更灵活。
  3. 程序 vs 进程

    • 程序是静态ELF 文件,进程是程序运行时的实例。
    • 一个程序可以启动多个进程(如 nginx 的多进程模式)。
  4. 进程 vs 线程

    • 进程有独立的 PID,不同进程不共享内存。
    • 线程是进程内部的执行单元,不同线程共享进程资源(如 全局变量)。
    • 进程间通信(IPC)比线程同步成本更高。

2. Linux 中的命令

Linux 中,命令 是用户与操作系统交互的基本方式,它可以是 shell 内置的功能,也可以是一个独立的可执行程序。掌握 Linux 命令的执行机制,能帮助你更深入理解 shell 工作原理,并优化脚本或程序的性能。


2.1 什么是 Linux 命令?

Linux 命令的本质是一个可执行的任务,它可以是:

  1. 内置命令(Built-in Command)

    • shell 解释器(如 bashzsh)直接提供,不需要创建子进程
    • 示例:cdechoexitsetalias
  2. 外部命令(External Command)

    • 独立的可执行文件,存储在 /bin/usr/bin 等目录,需要 shell 解析路径后执行。
    • 示例:lsgrepfindawkpythongcc

2.2 内置命令 vs 外部命令

类别 定义 是否需要子进程 执行速度 示例
内置命令 shell 解释器自带的命令 不需要 cdechoexport
外部命令 磁盘上的可执行程序 需要 稍慢 /bin/ls/usr/bin/grep
2.2.1 如何区分内置命令和外部命令?
$ type cd
cd is a shell builtin

$ type ls
ls is /bin/ls
2.2.2 为什么 cd 必须是内置命令?

因为 cd 需要改变 shell 进程的工作目录,而外部命令运行在子进程中,子进程修改的工作目录不会影响父进程(shell 本身)。


2.3 shell 如何解析命令?

当你在 bash 终端输入 ls -l 时,shell 会按以下步骤解析并执行命令:

  1. 解析命令行,将 ls -l 分解成 命令 ls参数 -l
  2. 检查是否为 shell 内置命令,如果是,则直接执行(例如 cd)。
  3. PATH 变量中搜索外部命令ls 不是内置命令)。
    • shell 依次检查 PATH 变量中定义的目录,例如:
      $ echo $PATH
      /usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
      
    • shell 依次在 /usr/local/sbin/usr/local/bin/usr/sbin/usr/bin 等目录中查找 ls
    • 找到 /bin/ls 后,执行该程序。
  4. 执行 /bin/ls,并传递参数 -l
2.3.1 查看 shell 解析的完整过程

可以使用 bash -x 调试命令解析:

$ bash -x -c "ls -l"
+ ls -l

2.4 如何查找 Linux 命令的路径?

Linux 提供了多个工具来帮助你查找命令的具体路径、类型和解析方式:

1. which —— 查找外部命令路径
$ which ls
/bin/ls

which 只适用于外部命令,对 shell 内置命令无效。

2. type —— 查询命令类型
$ type cd
cd is a shell builtin

$ type ls
ls is /bin/ls

typewhich 更强大,它可以区分内置命令和外部命令。

3. hash —— 查看 shell 缓存的命令路径

bash 可能会缓存命令路径,提高执行效率。要查看缓存的命令路径,可以使用 hash 命令:

$ hash
hits    command
   3    /bin/ls
   2    /usr/bin/grep

如果你修改了 PATH 或者新增了命令,可以使用 hash -rbash 重新解析路径:

$ hash -r

2.5 PATH 环境变量:决定命令执行的搜索顺序

PATH 变量控制 shell 在何处查找外部命令。要查看 PATH 变量的值:

$ echo $PATH
/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
2.5.2 如何修改 PATH
  1. 临时修改 PATH(仅当前 shell 生效):
    export PATH=$PATH:/home/user/bin
    
  2. 永久修改 PATH(修改 ~/.bashrc~/.bash_profile):
    echo 'export PATH=$PATH:/home/user/bin' >> ~/.bashrc
    source ~/.bashrc
    

2.6 alias 伪装命令

有时候,你输入的 ls 可能不是 /bin/ls,而是 alias 命令:

$ alias ls='ls --color=auto'
$ ls

可以使用 \lscommand ls 绕过 alias,直接执行 /bin/ls

$ \ls  # 忽略 alias
$ command ls  # 忽略 alias

要取消 alias,可以使用 unalias

$ unalias ls

2.7 小结

  • Linux 命令分为 内置命令cdecho)和 外部命令/bin/ls)。
  • shell 解析命令的顺序:先检查 内置命令,再在 PATH 变量中查找 外部命令
  • 使用 whichtypecommand -v 查找命令的实际路径。
  • PATH 变量决定 shell 的搜索路径,可临时或永久修改。
  • alias 可以改变命令行为,unalias 可以移除 alias

掌握这些知识,你就能更清楚 Linux 运行命令的底层机制,为深入理解 shellLinux 内核打下坚实的基础!

3. Linux 中的脚本

3.1 什么是 Shell 脚本?如何执行?

Shell 脚本是一种用 shell 命令编写的可执行文件,它可以自动执行一系列任务。例如:

#!/bin/bash
echo "Hello, Linux!"

执行方式:

$ chmod +x script.sh
$ ./script.sh
Hello, Linux!

3.2 #!/bin/bash#!/usr/bin/env bash 的区别

  • #!/bin/bash:直接指定 bash 解释器路径。
  • #!/usr/bin/env bash:利用 env 查找 bash 位置,更加通用。

3.3 sourcebash script.sh 的不同之处

  • source script.sh:在当前 shell 执行,不创建新进程。
  • bash script.sh:创建新 shell 进程执行脚本。

4. Linux 中的程序

4.1 程序的本质:可执行文件

Linux 中,程序本质上是一个 ELF(Executable and Linkable Format) 文件,它包含了代码、数据、符号表等信息。

当我们在 Linux 终端输入一个可执行文件的名称并按回车,系统会:

  1. PATH 变量指定的路径中查找该文件。
  2. 确定它是否为可执行文件(ELF 格式或脚本)。
  3. 通过 加载器(Loader) 将其加载到内存,并分配地址空间。
  4. CPU 指令指针(IP) 指向程序的 入口地址(通常是 _start),开始执行。

4.2 ELF 文件格式解析

还记得 C 语言的 Hello World 吗?
#include 
int main() {
    printf("Hello, Linux!\n");
    return 0;
}

使用 GCC 编译:

$ gcc hello.c -o hello

生成的 hello 文件就是一个 ELF 格式的可执行文件。

ELF 文件的组成

ELF 文件包含多个部分,主要包括:

ELF 组件 描述
ELF Header(ELF 头部) 存储 ELF 文件的基本信息,如文件类型、入口地址、程序头表偏移量等。
Program Header Table(程序头表) 描述可执行文件的 (Segment),用于进程加载。
Sections(节/段表) 包含代码(.text)、数据(.data)、符号表(.symtab)、动态链接信息等。
Symbol Table(符号表) 记录函数、变量等符号信息,调试和动态链接时使用。
Relocation Table(重定位表) 记录需要修改的地址,在动态链接时更新。
Dynamic Linking Information(动态链接信息) 存储动态库的相关信息,如 ld.so 需要加载哪些共享库。
ELF 结构示意图
+----------------------+
| ELF Header          |  # ELF 头部
+----------------------+
| Program Header Table|  # 可执行文件的段信息
+----------------------+
| .text (代码段)      |  # 存放可执行指令
+----------------------+
| .data (数据段)      |  # 存放全局变量、已初始化变量
+----------------------+
| .bss  (未初始化数据)|  # 存放未初始化全局变量
+----------------------+
| .rodata (只读数据)  |  # 存放字符串常量等
+----------------------+
| .symtab (符号表)    |  # 记录函数、变量信息
+----------------------+
| .rel.text (重定位表)|  # 记录需要重定位的地址
+----------------------+
| .dynamic (动态信息) |  # 记录动态库相关信息
+----------------------+
| Section Header Table|  # ELF 文件的节信息
+----------------------+

4.3 操作系统如何加载 ELF 文件

当你运行 ./hello 时,Linux 需要加载 ELF 文件到内存并执行,过程如下:

  1. 内核调用 execve()
    execve()Linux 运行 ELF 可执行文件的核心系统调用,它负责:

    • 读取 ELF 头部,解析 entry point(入口地址)。
    • 根据 程序头表Program Header Table)将代码段、数据段等映射到进程地址空间。
    • 如果是动态链接程序,还需要加载 ld-linux.so 解析共享库。
    • 跳转到 _start 入口,开始执行。
  2. 地址空间分布(简化版):

+-------------------+ 高地址
| stack            |  # 栈,存储局部变量、函数调用帧
+-------------------+
| mmap区域         |  # 动态库等 mmap 映射区域
+-------------------+
| heap             |  # 堆,`malloc()` 分配的内存
+-------------------+
| .bss             |  # 未初始化全局变量
+-------------------+
| .data            |  # 已初始化全局变量
+-------------------+
| .text (代码段)   |  # 可执行代码
+-------------------+ 低地址

进程加载完成后,CPU 开始执行 _start(),最终进入 main()


4.4 ELF 相关工具速查

Linux 提供了丰富的工具来分析 ELF 可执行文件:

命令 作用
file 查看文件类型(是否 ELF 可执行)
readelf -h 查看 ELF 头部信息
readelf -l 查看程序头表(段信息)
readelf -S 查看节头表(Sections)
objdump -d 反汇编代码
strings 查看可执行文件中的字符串
ldd 查看 ELF 依赖的动态库
nm 查看符号表(函数、变量)

5. Linux 中的进程

5.1 什么是 进程

Linux 中,进程(Process) 是正在运行的程序,每个进程都有:

  • 独立的内存空间(代码段、数据段、堆、栈等)。
  • 唯一的进程 ID(PID),用于标识进程。
  • 进程控制块(PCB),存储进程的各种信息(状态、资源、打开的文件等)。
  • 独立的执行上下文(CPU 寄存器、程序计数器)。

可以使用 toppshtop 命令查看系统中正在运行的进程:

$ ps aux
$ top
$ htop

5.2 进程的创建 (forkexec)

Linux 中,进程的创建通常依赖 fork()exec() 机制:

  • fork():创建一个新的 子进程,它是当前进程的拷贝。
  • exec():用新的程序替换当前进程的映像(改变进程的代码和数据)。

进程创建:fork()

fork() 系统调用用于创建子进程,子进程继承父进程的大部分资源,但有自己的 PID

#include 
#include 

int main() {
    pid_t pid = fork(); // 创建子进程

    if (pid == -1) {
        perror("fork failed");
        return 1;
    }
    else if (pid == 0) {
        printf("我是子进程,PID=%d,父进程PID=%d\n", getpid(), getppid());
    } 
    else {
        printf("我是父进程,PID=%d,子进程PID=%d\n", getpid(), pid);
    }

    return 0;
}

fork() 的返回值

  • pid > 0:在父进程中,返回子进程的 PID
  • pid == 0:在子进程中,返回 0
  • pid == -1:失败,返回 -1,通常是资源不足。

运行 ./a.out,示例输出:

我是父进程,PID=1234,子进程PID=1235
我是子进程,PID=1235,父进程PID=1234

进程映像替换:exec()

exec() 系列函数用于 用新程序替换当前进程,但 PID 不变。

#include 
#include 

int main() {
    printf("执行 ls 命令\n");
    execlp("ls", "ls", "-l", NULL);
    perror("execlp 失败");
    return 1;
}

如果 execlp() 成功,当前进程会被 ls -l 取代,不会执行 perror

常见 exec 函数

函数 描述
execl() 传递可执行文件路径,参数列表以 NULL 结尾
execlp() 搜索 PATH,无需完整路径
execv() 传递参数数组
execvp() execv() + PATH 搜索

5.3 进程的生命周期

Linux 进程的生命周期主要包括 创建、运行、等待、终止 4 个阶段:

1. 创建(fork() / clone()
  • fork() 创建子进程,资源(内存、文件描述符等)拷贝但不共享
  • clone()(更底层)可指定哪些资源 共享(如 CLONE_VM 共享地址空间)。
2. 运行
  • 进程进入 就绪状态(Ready),等待调度。
  • 调度器(CFSO(1)等)将其分配到 CPU 运行。
3. 等待(阻塞 / 挂起)

进程可能因以下原因进入 阻塞(Sleeping) 状态:

  • I/O 等待,如 read()write()
  • sleep(n)usleep() 休眠。
  • waitpid() 等待子进程结束。
4. 终止(Exit)
  • 正常退出exit() / return 0;)。
  • 信号杀死SIGKILL / SIGTERM)。
  • 崩溃(除零错误、段错误 Segmentation Fault)。

可以使用 ps 查看进程状态:

$ ps -eo pid,stat,cmd

常见进程状态:

状态 描述
R (Running) 正在运行或在就绪队列中
S (Sleeping) 休眠(等待 I/O)
Z (Zombie) 僵尸进程(父进程未回收)
T (Stopped) SIGSTOP 暂停

6. Linux 中的线程

6.1 什么是 线程

线程(Thread)进程内的执行单元,多个线程共享 同一个进程的内存地址空间,但有独立的 栈(Stack)、程序计数器(PC)、寄存器

每个进程至少会有 一个线程,称为 主线程(main thread。一个进程也可以包含 多个线程,实现 并发执行

线程是CPU调度的基本单位, 属于同一个进程的多个线程共享进程的资源。


6.2 进程 vs 线程

对比项 进程(Process) 线程(Thread)
资源 进程有独立的内存空间 线程共享同一进程的内存空间
通信 需要使用 IPC(如管道、消息队列、共享内存等) 共享内存,直接访问全局变量(需同步)
创建开销 fork() 创建新进程开销大 pthread_create() 创建线程开销小
崩溃影响 进程崩溃不会影响其他进程 线程崩溃可能导致整个进程崩溃(因共享地址空间)
数据共享 进程之间数据相互隔离 线程共享全局变量、堆、文件描述符
上下文切换 进程切换开销较大 线程切换更轻量级

6.3 线程的生命周期

线程的生命周期包含 创建、运行、阻塞、终止 4 个阶段:

1. 创建
  • pthread_create() 创建一个新线程。
  • 线程开始执行 传入的线程函数
2. 运行
  • 线程开始执行 任务代码
3. 阻塞

线程可能因 等待 I/O、互斥锁、信号量 进入 阻塞(Blocked) 状态:

  • sleep() 休眠线程。
  • pthread_mutex_lock() 等待锁。
  • pthread_cond_wait() 等待条件变量。
4. 终止

线程可以通过以下方式终止:

  • 正常结束:线程函数 return
  • 显式终止pthread_exit()
  • 主线程结束:若主线程通过 returnexit() 结束,整个进程终止,所有线程退出;若主线程调用 pthread_exit(),其他线程可继续运行。
  • 强制终止pthread_cancel()不推荐,容易导致资源泄漏)。

7. 总结

  • 命令shell 解析并执行的基本单位。
  • 脚本 提供了自动化能力。
  • 程序Linux 上的可执行文件。
  • 进程 独立运行,IPC 负责通信。
  • 线程 共享资源,提高并发能力。

希望这篇文章能帮你彻底搞懂 Linux 执行机制!

你可能感兴趣的:(Linux程序员,linux,进程,脚本,线程)