基本概念
进程
进程是系统分配资源的最小单位。在Linux系统中,PCB(Process Control Block)是进程的核心,它记录了进程的基本信息和运行状态。这些内容是记录在一个名叫task_struct
的结构体当中。
PCB包含大量的信息,但一般来说关注的只有:
- 进程标识符(pid):用于唯一标识一个进程
- 父进程标识符(ppid):父进程的pid
- 进程状态(state):进程状态,如运行、等待、睡眠等
- 进程优先级:表示进程执行的优先级,在Linux下由
price
和nice
两个值决定
父子进程
一个进程可以创建另一个进程,它就成了它创建出的进程的父进程,子进程会直接复制父进程的PCB,在修改这个PCB之前,两者使用的其实就是同一个PCB。在子进程修改内容后,就拥有了独立的PCB。
页表
当进程的PCB被创建时,会同步创建其页表项,页表中记录了当前进程的虚拟地址对物理地址的映射。
父子进程的PCB空间几乎完全相同,所以其虚拟地址也相同,但会被不同的页表项映射到不同的物理地址上。所以看上去好像是一个地址出现了两个变量,实际各自拥有不同的物理地址。
进程状态
R
Running/Runnable:表示运行态或者就绪态
进程正在占用CPU执行,或者已经具备运行条件,正在等待系统分配CPU资源以便运行。在Linux中,使用TASK_RUNNING
表示这种状态。
S
Sleeping:睡眠状态,有时也称为可中断的等待状态。
进程正在等待某个事件的发生(如,等待I/O操作完成,等待信号量等),此时进程可以被一些信号唤醒。
D
Disk Sleep:磁盘睡眠状态,有时也称为不可中断的等待状态。
进程正在进行某些不能中断的操作(如正在进行磁盘I/O),此时进程不能被任何信号唤醒。这种状态非常短暂,因为内核需要保护这些操作的完整性。
T
Stopped/Traced:暂停/停止状态,跟踪状态
进程被某种信号(如SIGSTOP
)停止运行,或者正在被调试器跟踪,此时进程就是T状态,可以使用一些信号(如SIGCONT
)让其继续运行。
Z
Zombie:僵死状态(僵尸状态)
进程已经结束,但是其父进程尚未读取其退出状态,此时,进程的内核数据已经无效了,但依然存在,这样的状态叫僵尸状态,父进程回收完毕后,退出僵尸状态。
孤儿进程
当父进程先于子进程结束时,此时子进程就成了孤儿进程,孤儿进程会被1号进程收养。
进程相关命令
top
Linux下的任务管理器,可以通过它进行一些对进程的查阅操作:
P 按CPU使用率排序
M 按内存使用率排序
T 按运行时间排序
k 可以输入pid杀死指定进程
b 高亮显示正在运行的进程
h 帮助菜单
q 退出
ps
静态查询进程信息,默认只显示当前进程和自己的子进程
常用参数:
-e :显示所有进程的基本信息(PID、创建命令)
-f :显示父进程的pid(ppid)
-l :显示PRI和NI值(PRI越低优先级越高,NI是对PRI的微调,取值范围是[-20,20])
aux :查询 CPU/MEM 占用及进程状态(也就是top中现实的内容)
进程相关函数
fork
创建一个子进程
头文件:
#include <unistd.h>
函数声明:
pid_t fork(); // pid_t 是进程ID类型(通常是整型)
返回值:
如果成功,则在父进程中会返回>0的子进程的pid,子进程中会返回0,失败返回<0。
fork 函数在调用时,会生成一个新的子进程,其PCB就是父进程的PCB,其执行的先后次序和父进程间没有明确的先后关系。
父进程内存空间
+-----------------+
| 代码段 |
| 数据段 | <-- fork() 发生时复制整个空间
| 堆栈段 |
+-----------------+
↓
子进程内存空间
+-----------------+
| 代码段 (相同) |
| 数据段 (初始相同)|
| 堆栈段 (初始相同)|
+-----------------+
执行流程的分离:
pid_t pid = fork(); // 分裂点
// 从这行开始,两个进程并行执行相同代码
// 但通过 pid 的值区分执行路径
if (pid == 0) {
// 只有子进程会执行这里
do_child_work();
} else if (pid > 0) {
// 只有父进程会执行这里
do_parent_work();
} else {
// 错误处理
}
经典错误示例:
// 错误用法!
if (fork() == 0) {
printf("Child\n");
} else {
printf("Parent\n"); // 实际父进程也会执行这里
}
不检查返回值的后果:
#include <stdio.h>
#include <unistd.h>
int main() {
fork(); // 分裂点,但不保存返回值
// 以下代码父子进程都会执行!
printf("Hello from PID=%d\n", getpid());
return 0;
}
正常例子:
#include <stdio.h>
#include <unistd.h>
int main() {
pid_t pid = fork(); // 创建子进程
if (pid < 0) {
perror("fork failed");
return 1;
}
if (pid == 0) {
// 子进程代码
printf("Child: My PID = %d, Parent PID = %d\n", getpid(), getppid());
} else {
// 父进程代码
printf("Parent: My PID = %d, Child PID = %d\n", getpid(), pid);
}
return 0;
}
进阶示例:父子进程协作
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>
int main() {
pid_t pid = fork();
if (pid < 0) {
perror("fork");
return 1;
}
if (pid == 0) {
// 子进程:计算1+2+...+5
int sum = 0;
for (int i = 1; i <= 5; i++) sum += i;
printf("Child: Sum = %d\n", sum);
_exit(0); // 子进程直接退出
} else {
// 父进程等待子进程结束
int status;
waitpid(pid, &status, 0); // 阻塞等待
if (WIFEXITED(status)) {
printf("Parent: Child exited with status %d\n", WEXITSTATUS(status));
}
}
return 0;
}
输出:
Child: Sum = 15
Parent: Child exited with status 0
正确的分支用法:
pid_t pid = fork();
if (pid == 0) {
// 子进程专属代码
do_child_task();
exit(0); // 子进程结束后退出
} else if (pid > 0) {
// 父进程专属代码
do_parent_task();
wait(NULL); // 等待子进程
} else {
// 错误处理
}
getpid/getppid
这两个函数头文件都是#include <unistd.h>
,分别可以取出当前进程的pid和父进程的pid。
结合fork
的例子:
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>
int main() {
printf("[原始进程] PID=%d, PPID=%d\n", getpid(), getppid());
pid_t pid = fork(); // 创建子进程
if (pid < 0) {
perror("fork失败");
return 1;
}
if (pid == 0) {
// 子进程代码
printf("[子进程] 我的PID=%d, 父进程PID=%d\n", getpid(), getppid());
sleep(1); // 等待父进程可能结束
printf("[子进程] 1秒后父进程PID=%d\n", getppid());
} else {
// 父进程代码
printf("[父进程] 我的PID=%d, 创建的子进程PID=%d\n", getpid(), pid);
// 父进程立即退出(不等待子进程)
_exit(0); // 使用 _exit 避免刷新缓冲区
}
return 0;
}
输出:
[原始进程] PID=1234, PPID=5678 # 原始进程信息(父进程是Shell)
[父进程] 我的PID=1234, 创建的子进程PID=1235
[子进程] 我的PID=1235, 父进程PID=1234
[子进程] 1秒后父进程PID=1 # 父进程退出后,子进程被init/systemd收养
注意:
当父进程先退出时,子进程会成为孤儿进程
孤儿进程会被 init 进程(PID=1)或 systemd 收养
被收养后,子进程的 getppid() 将返回 1
当从终端运行程序时,Shell 是程序的父进程
使用 ps -ef 命令可以查看完整的进程树:
UID PID PPID C STIME TTY TIME CMD
user 5678 3456 0 10:00 pts/0 00:00:00 bash # Shell
user 1234 5678 0 10:01 pts/0 00:00:00 ./a.out # 我们的程序
user 1235 1234 0 10:01 pts/0 00:00:00 ./a.out # 子进程
常见问题:
Q:为什么父进程退出后,子进程的 PPID 会变成 1?
A:这是 Unix/Linux 的设计机制。当进程失去父进程时,系统会让 PID=1 的
init/systemd 进程收养所有孤儿进程,确保进程能被正确回收
Q:getpid() 返回值会变化吗?
A:不会。进程的 PID 在其生命周期内保持不变。只有调用 exec() 系列函数后,
进程内容会被替换,但 PID 不变。
wait
等待子进程执行
头文件:
#include <sys/wait.h>
#include <sys/types.h>
#include <stdlib.h> // 用于 exit
函数声明:
pid_t wait(int* status);
过程:
当wait
调用时,如果子进程已经结束,则直接抓取其状态码,如果子进程还没结束,则会阻塞等待到子进程结束后,再抓取状态码。
参数是一个出参,带出了子进程的状态码,传入NULL
则表示不在乎子进程的结束状态。子进程结束后,如果不进行wait
操作回收子进程的资源,则子进程就会陷入僵尸状态。
※子进程的结束状态可以是子进程的返回值,或者是使用exit
函数传入的值。wait
接收到的值是该值的256倍。直接右移8位就可以得到原值,系统也提供了WEXITSTATUS
宏,以方便还原原值。
例子:
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>
#include <stdlib.h> // 用于 exit
int main() {
pid_t pid = fork();
if (pid < 0) {
perror("fork失败");
exit(1);
}
if (pid == 0) {
// 子进程代码
printf("子进程 %d 运行中...\n", getpid());
sleep(2);
printf("子进程退出\n");
exit(42); // 设置退出码为42
} else {
// 父进程代码
printf("父进程 %d 等待子进程 %d\n", getpid(), pid);
int status;
pid_t terminated_pid = wait(&status); // 阻塞等待
if (terminated_pid == -1) {
perror("wait失败");
exit(1);
}
printf("子进程 %d 已终止\n", terminated_pid);
if (WIFEXITED(status)) {
printf("正常退出,退出码: %d\n", WEXITSTATUS(status));
} else if (WIFSIGNALED(status)) {
printf("被信号终止,信号: %d\n", WTERMSIG(status));
}
}
return 0;
}
输出:
父进程 1234 等待子进程 1235
子进程 1235 运行中...
子进程退出
子进程 1235 已终止
正常退出,退出码: 42
状态解析宏
宏 说明
WIFEXITED(status) 子进程正常退出时为真
WEXITSTATUS(status) 获取子进程退出码(仅当 WIFEXITED 为真)
WIFSIGNALED(status) 子进程因信号终止时为真
WTERMSIG(status) 获取导致终止的信号编号(仅当 WIFSIGNALED 为真)
WIFSTOPPED(status) 子进程被暂停时为真
WSTOPSIG(status) 获取导致暂停的信号编号
waitpid
和wait的区别仅在于它会等待一个指定pid的子进程。函数声明:
pid_t waitpid(pid_t pid, int* status, int option);
参数、返回值
参数1是要等待的pid
参数2和wait一致
参数3一般给0,表示默认配置。
返回值同wait
非阻塞等待示例:
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>
int main() {
pid_t pid = fork();
if (pid == 0) {
// 子进程
sleep(5);
exit(0);
}
// 父进程:非阻塞等待
printf("父进程开始等待(非阻塞模式)\n");
int status;
pid_t result;
while (1) {
result = waitpid(pid, &status, WNOHANG);
if (result == -1) {
perror("waitpid失败");
break;
} else if (result == 0) {
printf("子进程尚未结束,继续等待...\n");
sleep(1);
} else {
printf("子进程 %d 已结束\n", result);
break;
}
}
return 0;
}
使用进程函数完成的一个父子进程间的通讯:
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/wait.h>
int main()
{
int a; //创建子进程前的资源
int ret = fork(); //复制时,父子进程拥有的是一个a
if (ret < 0) //失败判断
{
perror("fork error ");
return 0;
}
else if(ret > 0) //返回值大于零,表示当前执行的是父进程
{
a = 8;
printf("I'm father %d\n", a);
printf("I'm %d, my son is %d\n", getpid(), ret);
wait(&a); //这里可以用a接取子进程的退出状态,使用WEXITSTATUS还原其值
printf("my son returns %d\n", WEXITSTATUS(a));
}
else
{
a = 10;
printf("I'm son %d\n", a);
printf("I'm %d, my father is %d\n", getpid(), getppid());
//return 10;
exit(10); //这两种方法都可以表示子进程的退出状态为10
}
return 0;
}
exec系列函数
表示执行一个系统命令,这个函数会直接切换到对应命令的PCB执行。
execl、execle、execlp
函数声明:
int execl(const char* path, const char* cmd, ...);
int execle(const char* path, const char* cmd, ..., const char* envp[]);
int execlp(const char* path, const char* cmd, ...);
path
表示对应命令所在的文件路径,cmd
一串命令执行的各个字段,以一个NULL
结尾。evnp
是环境变量。
其中execl
不继承任何环境变量,execle
使用传入的环境变量,execlp
使用默认的环境变量。
execv、execve、execv
函数声明:
int execv(const char* path, const char* cmd[]);
int execve(const char* path, const char* cmd[], const char* envp[]);
int execvp(const char* path, const char* cmd[]);
和上面的一致,只是把不定参换成了一个命令的数组,这个数组一样用NULL
结尾。
待补充
.......补充
练习
通过exec系列函数完成一个终端
#include <unistd.h>
#include <string.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/wait.h>
#define STRNUM 64
int divideStr(char* src, char dst[][64])
{
char* start = src;
int dpos = 0;
char* pos = strchr(src, ' ');
while (pos)
{
strncpy(dst[dpos], start, pos - start);
dst[dpos][pos - start] = '\0';
dpos++;
start = pos + 1;
pos = strchr(start, ' ');
}
strcpy(dst[dpos], start);
return dpos + 1;
}
void copyCmd(char src[][64], char* dst[], int n)
{
int i;
for (i = 0; i < n; i++)
{
dst[i] = src[i];
}
dst[i] = NULL;
}
int main()
{
while (1)
{
char alldata[STRNUM][64] = { 0 };
char* arg[STRNUM];
printf("$ ");
fflush(NULL);
char userinput[256] = { 0 };
scanf("%255[^\n]%*c", userinput);
int ret = fork();
if (ret < 0)
{
perror("fork error ");
}
else if (ret > 0)
{
wait(NULL);
}
else
{
ret = divideStr(userinput, alldata);
copyCmd(alldata, arg, ret);
ret = execvp(arg[0], arg);
if (ret < 0)
{
perror("exec error ");
}
}
}
return 0;
}