Linux系统入门:进程控制
目录
一.进程创建
一).fork函数
二).写时拷贝
二.进程终止
一).进程退出的场景
二).进程退出方法
1.退出码
2.exit函数
3._exit函数
4.return退出
三.进程等待
一).进程等待的必要性
二).进程等待的方法
1.wait
2.waitpid
3.子进程status
4.阻塞与非阻塞等待
四.进程程序替换
一).替换原理
二).替换函数
1.execl函数
2.execlp函数
3.execv函数
4.execvp函数
5.execvpe函数编辑
6.execle函数
7.execve函数
一.进程创建
一).fork函数
在Linux系统中fork函数用来从一个已存在的进程中创建一个新进程,新进程为子进程,而原进程为父进程。
返回值:
- 子进程中返回0
- 父进程返回子进程的pid,出错返回-1
进程调用fork,当控制转移到内核中的fork代码后,内核要做以下几个工作:
- 分配新的内存块和内核数据结构(task_struct)给子进程。
- 将父进程部分数据结构内容(代码,数据以及页表等)拷贝到子进程。
- 添加子进程到系统进程列表中。
- fork返回,开始调度器调度。

#include <sys/types.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>int main()
{printf("%d\n",getpid());pid_t pid=fork();//创建子进程printf("%d,pid=%d\n",getpid(),pid);//子进程和父进程都会运行这段代码return 0;
}


fork之前父进程独立执行,fork之后,父子两个执行流分别执行。
二).写时拷贝
通常,父子代码共享,父子不写入时,数据也是共享的,当任意一方试图写入,便以写时拷贝的方式各自一份副本。

在创建子进程之后,数据段的数据就会变成只读的,在对数据段进行修改之后,操作系统发现对只读的字段进行修改,就会产生错误,这种错误导致了写时拷贝。
为什么要写时拷贝
- 减少创建子进程时,变量的拷贝,从而减少创建子进程的时间。
- 写时拷贝只拷贝发生修改的变量,不会拷贝全部变量,减少内存浪费。写时拷贝时以各种延时申请技术,可以提高整机内存的使用率
二.进程终止
进程终止的本质是释放系统资源,就是释放进程申请的相关内核数据结构和对应的数据和代码。
一).进程退出的场景
- 代码运行完毕,结果正确
- 代码运行完毕,结果不正确
- 代码异常终止
二).进程退出方法
main函数的返回值,通常表明程序的执行情况,反映上述两种情况,结果正确通常返回0,结果不正确通常返回非0。不同的返回值表明不同的出错原因。
一般main函数的返回值是返回给父进程bash的。可以通过 echo $? 查看返回值。
#include <stdio.h>int main()
{FILE* fp = fopen("log.txt", "r");if (fp == NULL){return 1;}return 0;
}
因为以只读的方式打开一个不存在的文件所以返回值为1。

main函数的返回值称为进程退出码。echo $? 的作用是打印最近一个程序(进程)退出时的退出码。
常见的进程退出方法
正常终止:
- 从main返回
- 调用exit
- _exit
异常退出:
- ctrl + c,信号终止
注意:
- return语句返回一个整数,是先把局部变量拷贝到寄存器中,然后再把寄存器中的值返回给目标函数。
- 子进程退出时,会把进程退出码写到自己的task_struct中,父进程在子进程僵尸状态的时候读取子进程的退出码
1.退出码
退出码(退出状态)可以告诉我们最后一次执行的命令的状态。在命令结束以后,我们可以知道命令是成功完成的还是以错误结束的。其基本思想是,程序返回退出码 0 时表示执行成功,没有问题。退出码 0 以外的任何代码都被视为不成功。代码异常退出(进程收到信号),退出码无意义了。
#include <stdio.h>
#include <string.h>int main()
{for (int i = 0; i < 150; i++){printf("%d->%s\n", i, strerror(i));}return 0;
}
查看标准C语言中常见的错误码对应的错误信息,一共134个。

- 退出码 0 表示命令执行无误,这是完成命令的理想状态。
- 退出码 1 我们也可以将其解释为 “不被允许的操作”。例如在没有 sudo 权限的情况下使用yum;再例如除以 0 等操作也会返回错误码 1。
- 130 ( SIGINT 或 ^C )和 143 ( SIGTERM )等是终止信号,它们属于128+n 信号,其中 n 代表终止码。
2.exit函数
任何地方调用exit函数,表示进程结束。并返回给父进程bash子进程的退出码。效果和return类似。
参数:
- status 定义了进程的终止状态,父进程通过wait来获取该值
#include <stdio.h>
#include <stdlib.h>void fun()
{printf("fun begin!");exit(40); //main进程直接终止了printf("fun end!");
}int main()
{fun();printf("main!");return 0;
}

3._exit函数
exit是C语言提供的接口,_exit是系统调用。

_exit和exit函数的效果一样,exit中封装了_exit,exit最后也会调用_exit,但在调用_exit之前,还做了其他工作。
_eixt和exit的区别是:调用exit()退出进程时,会进行缓冲区的刷新。调用_exit退出进程时,不会进行缓冲区的刷新。

#include <stdio.h>
#include <stdlib.h>void fun()
{printf("fun begin!");_exit(40);//使用系统调用printf("fun end!");
}int main()
{fun();printf("main!");return 0;
}

可以看到,调用exit()会把结果刷新到显示器上,调用_exit()不会把结果刷新到显示器上。
4.return退出
return是一种更常见的退出进程方法。执行return 等同于执行exit(n),因为调用main的运行时函数会将main的返回值当做 exit 的参数。
三.进程等待
一).进程等待的必要性
- 子进程退出,父进程如果不管不顾,就可能造成‘僵尸进程’的问题,进而造成内存泄漏。
- 另外,进程一旦变成僵尸状态,那就刀枪不入,“杀人不眨眼”的kill -9 也无能为力,因为谁也没有办法杀死一个已经死去的进程。
- 最后,父进程派给子进程的任务完成的如何,我们需要知道。如,子进程运行完成,结果对还是不对,或者是否正常退出。
- 父进程通过进程等待的方式,回收子进程资源,获取子进程退出信息。
二).进程等待的方法
1.wait
参数:
- 输出型参数,获取子进程退出状态,不关心则可以设置成为NULL
返回值:
- 成功返回被等待进程pid,失败返回-1
wait函数和scanf函数一样,如果子进程没有退出,则父进程会阻塞在wait调用处。wait函数等待任一子进程退出,如果有多个子进程,其中一个子进程退出则调用wait函数。
代码中使用wait函数来回收处于僵尸进程的子进程。
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
int main()
{pid_t id = fork();if (id == 0){int cnt = 5;while (cnt){printf("我是子进程,pid: %d, ppid: %d\n", getpid(), getppid());sleep(1);cnt--;}exit(0);}sleep(10);pid_t rid = wait(NULL);if (rid > 0){printf("wait success, rid: %d\n", rid); // rid为成功等待的子进程的pid}sleep(10);return 0;
}
程序启动之后子进程进行5s的循环,此时父进程处于休眠,5s循环完之后,子进程退出,但是父进程还在休眠,没有进行回收,此时子进程处于僵尸状态,父进程休眠10s后,调用wait回收子进程,父进程运行完后面的代码之后退出程序。

2.waitpid
参数:
- pid:pid=-1,等待任⼀个子进程。与wait等效。pid>0.等待其进程ID与pid相等的子进程。
- status:输出型参数。WIFEXITED(status): 若为正常终止子进程返回的状态,则为真。(查看进程是否是正常退出)。WEXITSTATUS(status): 若WIFEXITED非零,提取子进程退出码。(查看进程的退出码)
- options:默认为0,表示阻塞等待
- WNOHANG: 若pid指定的子进程没有结束,则waitpid()函数返回0,不予以等待。若正常结束,则返回该子进程的ID
返回值:
- 当正常返回的时候waitpid返回收集到的子进程的进程ID;如果设置了选项WNOHANG,而调用中waitpid发现没有已退出的子进程可收集,则返回0;如果调用中出错,则返回-1,这时errno会被设置成相应的值以指出错误所在。
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <errno.h>int main()
{pid_t id = fork();if (id == 0){int cnt = 5;while(cnt){printf("我是子进程,pid: %d, ppid: %d\n", getpid(), getppid());sleep(1);cnt--;}exit(40);}int status = 0;pid_t rid = waitpid(id, &status, 0);if (rid > 0){printf("wait success, rid: %d, status: %d\n", rid, status);}else{printf("wait failed: %d: %s\n", errno, strerror(errno));}return 0;
}

上述代码将子进程退出码设为40,但是父进程中接收到的子进程退出状态不为40,可以推测出,退出状态中不只是退出码。
总结:
- 如果子进程已经运行完退出,处于僵尸状态,则父进程中调用wait/waitpid时,会立即返回,并释放资源,获得子进程退出信息。
- 如果子进程存在且正常运行还没退出时,调用wait/waitpid函数,则父进程可能阻塞,等到子进程运行完毕。
- 如果不存在等待的子进程,则立即出错返回-1。

3.子进程status
- wait和waitpid,都有一个status参数,该参数是一个输出型参数,由操作系统填充
- 如果传递NULL,表示不关心子进程的退出状态信息。
- 否则,操作系统会根据该参数,将子进程的退出信息反馈给父进程。
- status不能简单的当作整形来看待,可以当作位图来看待,只研究status低16比特位

第8-15位,表示子进程退出的退出码。所以将上述返回的status右移8位,然后再&0xFF,只提取该八位的值
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <errno.h>int main()
{pid_t id=fork();if(id==0){int cnt=5;while(cnt){printf("我是子进程,pid: %d, ppid: %d\n", getpid(), getppid());sleep(1);cnt--;}exit(40);}int status=0;pid_t rid =waitpid(id,&status,0);if (rid > 0){printf("wait success, rid: %d, exit code: %d\n", rid, (status>>8)&0xFF); }else{printf("wait failed: %d: %s\n", errno, strerror(errno));}return 0;
}

进程异常退出时,一般都是因为进程收到了信号,status中的低7位用于保存异常时对应的信号编号。
没有异常,则低7位为0,一旦低7位不为0,就是异常退出,则8-15位的退出码无意义。
Linux系统中,使用 kill -l 命令查看所有信号。

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <errno.h>
int main()
{pid_t id = fork();if (id == 0){//int cnt = 5;while(1){printf("我是子进程,pid: %d, ppid: %d\n", getpid(), getppid());sleep(1);//cnt--;}exit(40);}int status = 0;pid_t rid = waitpid(id, &status, 0);if (rid > 0){printf("wait success, rid: %d, exit code: %d, exit signal: %d\n", rid, (status>>8)&0xFF, status&0x7F); //rid为成功等待的子进程的pid}else{printf("wait failed: %d: %s\n", errno, strerror(errno));}return 0;
}

注意:
- 僵尸进程的PCB中存储着该进程的退出码和退出信号,父进程通过waitpid()这种系统调用,让OS去处于僵尸状态的子进程的PCB中获得退出码和退出信号,然后返回给父进程中创建的status
- 代码中(status>>8)&0xFF操作就等同于WEXITSTATUS(status)这个宏运算。WIFEXITED(status)宏运算是判断子进程退出是否异常,正常终止返回真,异常返回假
4.阻塞与非阻塞等待
上述的代码例子都是阻塞等待,需要等待子进程完成才进行调用,不然父进程就会一直阻塞在wait()和waitpid()的调用处
非阻塞等待就是当子进程正常运行时,父进程中的waitpid()去检测子进程的状态,一般非阻塞等待都会搭配循环使用,对子进程构成轮询的形式。waitpid()中给option形参传入WNOHANG就能将waitpid()的等待方式换为非阻塞等待方式。
非阻塞等待的例子:
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <errno.h>typedef void (*func_t) ();#define NUM 5
func_t handlers[NUM + 1];//如下是各个功能函数
void Download()
{printf("我是一个下载任务...\n");
}
void Flush()
{printf("我是一个刷新任务...\n");
}
void Log()
{printf("我是一个记录日志的任务...\n");
}//注册函数,将需要各种功能函数写入到上述函数指针数组中
void registerHandler(func_t h[], func_t f)
{int i = 0;for (; i < NUM; i++){if (h[i] == NULL) break;}//指针数组满了if (i == NUM) return;h[i] = f;h[++i] = NULL;
}
int main()
{registerHandler(handlers, Download);registerHandler(handlers, Flush);registerHandler(handlers, Log);pid_t id = fork();if (id == 0){while(1){printf("我是子进程,pid: %d, ppid: %d\n", getpid(), getppid());sleep(3);}exit(10);}//父进程while(1){int status = 0;pid_t rid = waitpid(id, &status, WNOHANG);if (rid > 0){printf("wait success, rid: %d, exit code: %d, exit signal: %d\n", rid, (status>>8)&0xFF, status&0x7F);break;}else if (rid == 0){//函数指针进行回调int i = 0;for (; handlers[i]; i++){handlers[i]();}printf("本轮调用结束,子进程没有退出\n");sleep(1);}else{printf("等待失败\n");break;}}return 0;
}
首先给出一个函数指针func_t的定义,然后创建一个全局的函数指针数组handlers[NUM + 1]。在main函数中通过rigisterHandler函数将各个功能函数指针放入函数指针数组中。父进程通过轮询的方式监测子进程的运行,子进程没有结束,父进程同时也做着自己的任务。当子进程正常结束或者异常结束时,父进程等待成功或者等待失败,结束进程

四.进程程序替换
fork() 之后,父子各自执行父进程代码的一部分如果子进程就想执行一个全新的程序呢?进程的程序替换来完成这个功能!
程序替换是通过特定的接口,加载磁盘上的一个全新的程序(代码和数据),加载到调用进程的地址空间中!
一).替换原理
- 用fork创建子进程后执行的是和父进程相同的程序(但有可能执行不同的代码分支),子进程往往要调用一种exec函数以执行另一个程序。
- 当进程调用一种exec函数时,该进程的用户空间代码和数据完全被新程序替换,从新程序的启动例程开始执行。
- 调用exec并不创建新进程,所以调用exec前后该进程的id并未改变。

#include <stdio.h>
#include <unistd.h>int main()
{printf("我的程序要运行了!\n");execl("/usr/bin/ls", "ls", "-l", "-a", NULL);printf("我的程序运行完毕!\n");return 0;
}
替换之后,后续代码不执行。

二).替换函数
#include <unistd.h>
//前6个都是C语言提供的调用接口,最后一个是Linux提供的系统调用int execl(const char *path, const char *arg, ...);int execlp(const char *file, const char *arg, ...);int execle(const char *path, const char *arg, ...,char *const envp[]);int execv(const char *path, char *const argv[]);int execvp(const char *file, char *const argv[]);int execvpe(const char *file, char *const argv[], char *const envp[]);//系统调用
int execve(const char *path, char *const argv[], char *const envp[]);
- path: 路径+程序名 -- 执行哪个程序
- file:不用带路径(带路径也没错),只用给程序名,函数会从环境变量PATH中查找指定的命令
- arg: 命令行参数,...表示可变参数列表。命令怎么写,这里就怎么写,最后一个参数以NULL结尾,表示参数传递完毕。 -- 怎么执行函数
- argv[]:将命令行参数以指针数组的形式传递
- envp[]:环境变量的指针数组
这些函数如果调用成功则加载新的程序从启动代码开始执行,没有返回值。如果调用出错,则返回-1,所以exec*函数只有失败的返回值,没有成功的返回值
- l(list):表示参数使用列表传递命令行参数。
- v(vector):表示参数使用数组传递命令行参数。
- p(path):带有p的函数名,表示会从环境变量PATH中查找执行的程序。
- e(env):带有e的函数名,表示用户需传递一个自己维护的环境变量指针数组

上述表中的“是否带路径”,若为“是”,表示程序会从PATH中的路径查找,若为“不是”,表示需要用户自己传入程序路径。
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <stdlib.h>
int main()
{printf("我的程序要运行了!\n");if (fork() == 0){sleep(1);//脚本语言执行的程序是解释器//第一个参数是脚本解释器//execl("/usr/bin/python3", "python", "other.py", NULL);execl("/usr/bin/bash", "bash", "other.sh", NULL);exit(1);}waitpid(-1, NULL, 0);printf("我的程序运行完毕!\n");return 0;
}
注意:exec*函数为什么没有影响父进程
- 进程具有独立性。
- 使用exec*函数进行进程替换时,数据和代码都发生了写时拷贝。
1.execl函数

execl函数,需要传递程序的路径,并且使用列表形式传递命令行参数。
#include <sys/types.h>
#include <sys/wait.h>
#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>int main()
{printf("我的程序要运行了!\n");if (fork() == 0){sleep(1);execl("/usr/bin/ls", "ls", "-al", NULL);exit(1);}waitpid(-1, NULL, 0);printf("我的程序运行完毕!\n");return 0;
}

2.execlp函数

execlp函数,只需要传递程序文件名,并且以列表形式传递命令行参数。
#include <sys/types.h>
#include <sys/wait.h>
#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>int main()
{printf("我的程序要运行了!\n");if (fork() == 0){sleep(1);execlp("ls", "ls", "-al", NULL);exit(1);}waitpid(-1, NULL, 0);printf("我的程序运行完毕!\n");return 0;
}

3.execv函数
![]()
execv函数,需要传递程序路径,以数组的形式传递命令行参数。
#include <sys/types.h>
#include <sys/wait.h>
#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>char *const argv[] = {(char*const)"ls",(char*const)"-a",(char*const)"-l",NULL
};int main()
{printf("我的程序要运行了!\n");if (fork() == 0){sleep(1);execv("/usr/bin/ls", argv);exit(1);}waitpid(-1, NULL, 0);printf("我的程序运行完毕!\n");return 0;
}

4.execvp函数
![]()
execvp函数,只需要传递程序文件名,以数组的形式传递命令行参数。
#include <sys/types.h>
#include <sys/wait.h>
#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>char *const argv[] = {(char*const)"ls",(char*const)"-a",(char*const)"-l",NULL
};int main()
{printf("我的程序要运行了!\n");if (fork() == 0){sleep(1);execvp("ls", argv);exit(1);}waitpid(-1, NULL, 0);printf("我的程序运行完毕!\n");return 0;
}

5.execvpe函数
execvpe函数,只需要传递程序文件名,以数组的形式传递命令行参数,不使用当前环境变量,需自己组装环境变量。
other.cc,用于打印命令行参数以及环境变量,并编译为可执行的二进制程序other。
//other.cc#include <iostream>
#include <unistd.h>
#include <stdio.h>int main(int argc, char *argv[], char *env[])
{std::cout << "hello C++" << std::endl;for (int i = 0; i < argc; i++){printf("argv[%d]: %s\n", i, argv[i]);}printf("\n");for (int i = 0; env[i]; i++){printf("env[%d]: %s\n", i, env[i]);}return 0;
}
这里函数名带”p“,传递的时候传递程序名即可,但是other程序的路径没有放入PATH环境变量中,所以这里传递的程序名直接传递程序路径./other
#include <sys/types.h>
#include <sys/wait.h>
#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>char *const argv[] = {(char*const)"ls",(char*const)"-a",(char*const)"-l",NULL
};char*const env[] = {"MYVAL1=1234567","MYVAL2=6666666",NULL
};int main()
{printf("我的程序要运行了!\n");if (fork() == 0){sleep(1);execvpe("./other", argv, env);exit(1);}waitpid(-1, NULL, 0);printf("我的程序运行完毕!\n");return 0;
}

注意:
- 如果exec*系列函数中,函数名没有带”e“,则表明环境变量继承自父进程。
execvpe("./other", argv, env); 改为 execvp("./other", argv);打印的是继承自父进程的环境变量。
6.execle函数

execle函数,需要传递程序路径,以列表形式传递命令行参数,不使用当前环境变量,需自己组装环境变量。
#include <sys/types.h>
#include <sys/wait.h>
#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>char*const env[] = {"MYVAL1=1234567","MYVAL2=6666666",NULL
};int main()
{printf("我的程序要运行了!\n");if (fork() == 0){sleep(1);execle("./other", "other", "-a", "-b", NULL, env);exit(1);}waitpid(-1, NULL, 0);printf("我的程序运行完毕!\n");return 0;
}

7.execve函数
只用execve是真正的系统调用,其他函数内部都封装了execve,这些函数之间的关系如下图所示。






