快乐Linux —— 10. 进程创建 & 切换 & 终止 & 僵死
参考:
《UNIX环境高级编程》
https://blog.csdn.net/qq_17268389/article/details/84650956
https://blog.csdn.net/happiness_llz/article/details/82345679
0. 简述
看看进程创建 fork,进程切换 exec, 进程终止, 僵死进程
1. 进程的创建
pid_t fork(void);
//创建一个子进程
当系统执行到fork()时,将执行下面动作:
-
向系统申请一个新的PID(Process Identifier)。
-
创建子进程,复制父进程的PCB(process control block)。
获得父进程的数据空间、堆、栈等资源的副本。
-
如果创建子进程成功,则在父进程中 fork() 返回子进程的PID,在子进程中 fork() 返回0。
若创建失败则返回 -1。
几个要注意的问题:
-
为什么fork()会调用一次返回两次,注意:是返回两次,而不是返回两个值,一个函数只能有一个返回值
fork()函数在父进程中调用时,会在创建子进程过程中,复制了父进程的PCB,而PCB中就保存当前程序执行下一步的指令,所以父子进程相当于都执行在fork()函数中。父进程中返回为子进程申请的pid,而子进程的返回值被主动修改为0,然后再让子进程进入调度队列中,等待执行。
-
实际使用中,子进程在创建后往往会调用exec系列函数,所以,整个复制过程其实也不会真正完全复制,所以这块有个写时拷贝的技术节省内存。
简单来说,子进程共用父进程的数据段,栈,堆空间,并且内核将这些空间设置为只读的。当父子进程中任意一个要修改这些空间的值时,此时才会在实际内存上拷贝要修改的区域,分配空间的最小单位是“页”(4KB)。
关于写时拷贝 https://www.cnblogs.com/biyeymyhjob/archive/2012/07/20/2601655.html
-
fork() 产生的父子进程是并发执行的,而且谁先执行是不确定的,这块有进程调度算法。
fork()源码剖析 https://blog.csdn.net/Aspiration_1314/article/details/85604365
-
CHILD_MAX 这个宏规定了最大进程数量。
pid_t vfork(void);
//创建一个目的为执行其他程序的子进程
vfork() 和 fork() 的调用行为和返回值相同,但有点小区别:
-
vork() 并不会将父进程的地址空间完全复制到子进程中。而是在子进程调用exec 或exit 之前,一直在父空间中运行。
因为子进程会随后调用exec,从这点讲,vfork() 要比 fork() 后紧跟exec效果能好一点,因为虽然这块有写时拷贝,但不复制还是要比复制快一点。
-
vfork() 会保证子进程先运行,在子进程调用exec 或者 exit 后,父进程才有可能被调度运行。
如果子进程等待父进程一些动作,那么会导致死锁。
-
在子进程中结束要使用 exit 不能使用 return 。原因也很简单,子进程在父进程空间中运行,如果子进程return 之后,那么这个main函数栈就被摧毁, 当父进程开始执行时,主函数栈都已被销毁。
更多可以参考这篇博文 https://coolshell.cn/articles/12103.html#comment-1609820
2. 进程的执行的切换
exec 系列函数共性:
- 调用exec 函数将当前进程的用户数据(指令,数据,堆栈),分别替换成新程序的用户数据,所以当前进程的pid没有改变。相当于一个人原来在吃饭,现在让他去玩游戏,任务切换后,这个人依然是这个人。
- 当调用成功时,从新程序main函数开始执行,调用失败返回-1。所以说exec没有返回值,因为返回没有必要,当执行成功时,整个程序以经换为新的程序,原来的调用exec程序已经不存在了。
- 事实上,只有execve是真正的系统调用,其它五个函数最终都调用execve。
int execl(const char *path, const char *arg, ...);
int execle(const char *path, const char *arg, ..., char *const envp[]);
int execve(const char *path, char *const argv[], char *const envp[]);
int execv(const char *path, char *const argv[]);
int execlp(const char *file, const char *arg, ...);
int execvp(const char *file, char *const argv[]);
以上函数区别:
- l 表示的是 list
execl , execle , execlp
要进行主函数参数一个个单独传。这种参数表最后一个必须是空指针。 - v 表示矢量 vector
execv , execvp , execve
表示传命令行参数的形式跟主函数char *argv[]
一样。以字符指针数组形式给出。 - e 表示环境 environ
execle , execve
表示可以给新程序传递环境参数,而其他四个函数会自动为新程序复制当前环境参数。 - p
execlp , execvp
表示可以使用 程序名 作为参数。- 按照环境变量PATH指定的目录中搜索可执行文件。
- 如果filename 中包含 / 则将其视作路径名。
exec函数相互关系
3. 进程的终止
如果子进程正常终止则可以获取到其正常退出状态(也就是传给exit函数的参数),否则由内核产生表示其异常的终止状态。
总之,不管进程如何终止,最终都会执行内核中同一段代码,为进程关闭打开的文件描述符,释放外部资源。并且父进程都可以用wait 或 waitpid 等函数获取其终止状态。
孤儿进程
当父进程结束,而子进程未结束,此时子进程就称为孤儿进程。
孤儿进程都由 init 进程收养:当一个进程终止时,内核逐个检查所有活动进程,以判断它是否为正要终止的子进程。如果是,则将该进程的父进程id改为init 的id ,也就是1。
僵死进程
如果子进程结束,父进程没有对子进程做相应的善后处理(释放子进程占用资源或者获取子进程退出状态等),那么这个进程就称为僵死进程。
init 可以自动处理僵死的子进程。所以不用担心当孤儿进程被init 收养后会僵死。
父进程获取子进程退出码的方式
pid_t wait(int *result);
pid_t waitpid(pid_t pid,int *result,int options);
对 pid_t wait(int *result);
- 如果当前进程没有子进程,则直接返回 -1;
- 如果子进程未结束,则会阻塞,直到子进程结束。
- 当子进程结束,wait 返回子进程的pid,并且result 保存子进程的退出码。
- 如果多个子进程结束,则wait处理第一个结束的子进程。
对 pid_t waitpid(pid_t pid,int *result,int options);
-
pid
如果其指定的进程或进程组不存在或者pid 指定的进程不是调用进程的子进程,则会出错。
-
options
4. 关于父子进程资源共享问题
由上面的分析和第9节进程与文件 可知:
- 数据段 , 堆, 栈 都不是共享的。
- 在fork 打开的文件是共享的。代码段也是共享的。