概念 | 定义 | 存储形式 | 执行方式 | 是否需要进程 | 是否支持并发 | 示例 |
---|---|---|---|---|---|---|
命令 | 用户输入给 shell 的指令,可以是内置命令或外部命令 |
直接输入在 shell 中(无固定存储) |
由 shell 解析并执行 |
仅外部命令需要 | 取决于命令,如 & 可后台运行 |
ls 、cd 、echo |
脚本 | 一组命令的集合,存储在文件中,通常是 shell 脚本 |
文本文件 (.sh 、.py ) |
解释执行(bash script.sh ) |
需要(bash 进程) |
取决于脚本内容 | bash backup.sh 、python3 script.py |
程序 | 一个 ELF 可执行文件,存储在磁盘上 |
ELF 二进制文件 (如 /bin/ls ) |
由 shell 或其他程序执行 |
需要(新建进程) | 取决于程序设计 | /bin/ls 、/usr/bin/vim |
进程 | 正在运行的程序实例,占用 CPU 和 内存 |
进程 ID (PID )存在于内存 |
fork() 或 exec() 方式创建 |
本身就是进程 | 进程间独立,可并发 | sshd 、nginx |
线程 | 进程内部的执行单元,共享 进程 资源 |
线程 ID (TID )存在于进程内存 |
pthread_create() 或 std::thread |
依赖于进程 | 支持并发,多线程共享内存 | Java 线程、Goroutine 、pthreads |
命令 vs 脚本:
bash
、python
)来执行。脚本 vs 程序:
程序 vs 进程:
ELF
文件,进程是程序运行时的实例。nginx
的多进程模式)。进程 vs 线程:
PID
,不同进程不共享内存。堆
、全局变量
)。IPC
)比线程同步成本更高。在 Linux
中,命令 是用户与操作系统交互的基本方式,它可以是 shell
内置的功能,也可以是一个独立的可执行程序。掌握 Linux
命令的执行机制,能帮助你更深入理解 shell
工作原理,并优化脚本或程序的性能。
Linux
命令?Linux
命令的本质是一个可执行的任务,它可以是:
内置命令(Built-in Command)
shell
解释器(如 bash
、zsh
)直接提供,不需要创建子进程
。cd
、echo
、exit
、set
、alias
。外部命令(External Command)
/bin
、/usr/bin
等目录,需要 shell
解析路径后执行。ls
、grep
、find
、awk
、python
、gcc
。类别 | 定义 | 是否需要子进程 | 执行速度 | 示例 |
---|---|---|---|---|
内置命令 | shell 解释器自带的命令 |
不需要 | 快 | cd 、echo 、export |
外部命令 | 磁盘上的可执行程序 | 需要 | 稍慢 | /bin/ls 、/usr/bin/grep |
$ type cd
cd is a shell builtin
$ type ls
ls is /bin/ls
cd
必须是内置命令?因为 cd
需要改变 shell
进程的工作目录,而外部命令运行在子进程中,子进程修改的工作目录不会影响父进程(shell
本身)。
shell
如何解析命令?当你在 bash
终端输入 ls -l
时,shell
会按以下步骤解析并执行命令:
ls -l
分解成 命令 ls
和 参数 -l
。shell
内置命令,如果是,则直接执行(例如 cd
)。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
后,执行该程序。/bin/ls
,并传递参数 -l
。shell
解析的完整过程可以使用 bash -x
调试命令解析:
$ bash -x -c "ls -l"
+ ls -l
Linux
命令的路径?Linux
提供了多个工具来帮助你查找命令的具体路径、类型和解析方式:
which
—— 查找外部命令路径$ which ls
/bin/ls
但 which
只适用于外部命令,对 shell
内置命令无效。
type
—— 查询命令类型$ type cd
cd is a shell builtin
$ type ls
ls is /bin/ls
type
比 which
更强大,它可以区分内置命令和外部命令。
hash
—— 查看 shell
缓存的命令路径bash
可能会缓存命令路径,提高执行效率。要查看缓存的命令路径,可以使用 hash
命令:
$ hash
hits command
3 /bin/ls
2 /usr/bin/grep
如果你修改了 PATH
或者新增了命令,可以使用 hash -r
让 bash
重新解析路径:
$ hash -r
PATH
环境变量:决定命令执行的搜索顺序PATH
变量控制 shell
在何处查找外部命令。要查看 PATH
变量的值:
$ echo $PATH
/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
PATH
?PATH
(仅当前 shell
生效):export PATH=$PATH:/home/user/bin
PATH
(修改 ~/.bashrc
或 ~/.bash_profile
):echo 'export PATH=$PATH:/home/user/bin' >> ~/.bashrc
source ~/.bashrc
alias
伪装命令有时候,你输入的 ls
可能不是 /bin/ls
,而是 alias
命令:
$ alias ls='ls --color=auto'
$ ls
可以使用 \ls
或 command ls
绕过 alias
,直接执行 /bin/ls
。
$ \ls # 忽略 alias
$ command ls # 忽略 alias
要取消 alias
,可以使用 unalias
:
$ unalias ls
Linux
命令分为 内置命令(cd
、echo
)和 外部命令(/bin/ls
)。shell
解析命令的顺序:先检查 内置命令,再在 PATH
变量中查找 外部命令。which
、type
、command -v
查找命令的实际路径。PATH
变量决定 shell
的搜索路径,可临时或永久修改。alias
可以改变命令行为,unalias
可以移除 alias
。掌握这些知识,你就能更清楚 Linux
运行命令的底层机制,为深入理解 shell
和 Linux
内核打下坚实的基础!
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
位置,更加通用。source
和 bash script.sh
的不同之处source script.sh
:在当前 shell
执行,不创建新进程。bash script.sh
:创建新 shell
进程执行脚本。可执行文件
在 Linux
中,程序本质上是一个 ELF(Executable and Linkable Format) 文件,它包含了代码、数据、符号表等信息。
当我们在 Linux
终端输入一个可执行文件的名称并按回车,系统会:
PATH
变量指定的路径中查找该文件。ELF
格式或脚本)。_start
),开始执行。#include
int main() {
printf("Hello, Linux!\n");
return 0;
}
使用 GCC
编译:
$ gcc hello.c -o hello
生成的 hello
文件就是一个 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 Header | # ELF 头部
+----------------------+
| Program Header Table| # 可执行文件的段信息
+----------------------+
| .text (代码段) | # 存放可执行指令
+----------------------+
| .data (数据段) | # 存放全局变量、已初始化变量
+----------------------+
| .bss (未初始化数据)| # 存放未初始化全局变量
+----------------------+
| .rodata (只读数据) | # 存放字符串常量等
+----------------------+
| .symtab (符号表) | # 记录函数、变量信息
+----------------------+
| .rel.text (重定位表)| # 记录需要重定位的地址
+----------------------+
| .dynamic (动态信息) | # 记录动态库相关信息
+----------------------+
| Section Header Table| # ELF 文件的节信息
+----------------------+
当你运行 ./hello
时,Linux 需要加载 ELF
文件到内存并执行,过程如下:
内核调用 execve()
:
execve()
是 Linux
运行 ELF 可执行文件的核心系统调用,它负责:
entry point
(入口地址)。Program Header Table
)将代码段、数据段等映射到进程地址空间。ld-linux.so
解析共享库。_start
入口,开始执行。地址空间分布(简化版):
+-------------------+ 高地址
| stack | # 栈,存储局部变量、函数调用帧
+-------------------+
| mmap区域 | # 动态库等 mmap 映射区域
+-------------------+
| heap | # 堆,`malloc()` 分配的内存
+-------------------+
| .bss | # 未初始化全局变量
+-------------------+
| .data | # 已初始化全局变量
+-------------------+
| .text (代码段) | # 可执行代码
+-------------------+ 低地址
进程加载完成后,CPU 开始执行 _start()
,最终进入 main()
。
Linux
提供了丰富的工具来分析 ELF
可执行文件:
命令 | 作用 |
---|---|
file |
查看文件类型(是否 ELF 可执行) |
readelf -h |
查看 ELF 头部信息 |
readelf -l |
查看程序头表(段信息) |
readelf -S |
查看节头表(Sections) |
objdump -d |
反汇编代码 |
strings |
查看可执行文件中的字符串 |
ldd |
查看 ELF 依赖的动态库 |
nm |
查看符号表(函数、变量) |
进程
?在 Linux
中,进程(Process) 是正在运行的程序,每个进程都有:
可以使用 top
、ps
、htop
命令查看系统中正在运行的进程:
$ ps aux
$ top
$ htop
fork
、exec
)在 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 搜索 |
Linux 进程的生命周期主要包括 创建、运行、等待、终止 4 个阶段:
fork()
/ clone()
)fork()
创建子进程,资源(内存、文件描述符等)拷贝但不共享。clone()
(更底层)可指定哪些资源 共享(如 CLONE_VM
共享地址空间)。CFS
、O(1)
等)将其分配到 CPU 运行。进程可能因以下原因进入 阻塞(Sleeping) 状态:
I/O
等待,如 read()
、write()
。sleep(n)
或 usleep()
休眠。waitpid()
等待子进程结束。exit()
/ return 0;
)。SIGKILL
/ SIGTERM
)。Segmentation Fault
)。可以使用 ps
查看进程状态:
$ ps -eo pid,stat,cmd
常见进程状态:
状态 | 描述 |
---|---|
R (Running) |
正在运行或在就绪队列中 |
S (Sleeping) |
休眠(等待 I/O) |
Z (Zombie) |
僵尸进程(父进程未回收) |
T (Stopped) |
被 SIGSTOP 暂停 |
线程
?线程(Thread) 是 进程内的执行单元,多个线程共享 同一个进程的内存地址空间,但有独立的 栈(Stack)、程序计数器(PC)、寄存器。
每个进程至少会有 一个线程,称为 主线程(main thread
)。一个进程也可以包含 多个线程,实现 并发执行。
线程是CPU调度的基本单位, 属于同一个进程的多个线程共享进程的资源。
对比项 | 进程(Process) | 线程(Thread) |
---|---|---|
资源 | 进程有独立的内存空间 | 线程共享同一进程的内存空间 |
通信 | 需要使用 IPC (如管道、消息队列、共享内存等) |
共享内存,直接访问全局变量(需同步) |
创建开销 | fork() 创建新进程开销大 |
pthread_create() 创建线程开销小 |
崩溃影响 | 进程崩溃不会影响其他进程 | 线程崩溃可能导致整个进程崩溃(因共享地址空间) |
数据共享 | 进程之间数据相互隔离 | 线程共享全局变量、堆、文件描述符 |
上下文切换 | 进程切换开销较大 | 线程切换更轻量级 |
线程的生命周期包含 创建、运行、阻塞、终止 4 个阶段:
pthread_create()
创建一个新线程。线程可能因 等待 I/O、互斥锁、信号量 进入 阻塞(Blocked) 状态:
sleep()
休眠线程。pthread_mutex_lock()
等待锁。pthread_cond_wait()
等待条件变量。线程可以通过以下方式终止:
return
。pthread_exit()
。return
或 exit()
结束,整个进程终止,所有线程退出;若主线程调用 pthread_exit()
,其他线程可继续运行。pthread_cancel()
(不推荐,容易导致资源泄漏)。shell
解析并执行的基本单位。Linux
上的可执行文件。IPC
负责通信。希望这篇文章能帮你彻底搞懂 Linux
执行机制!