Loading

快乐Linux —— 10. 进程创建 & 切换 & 终止 & 僵死

参考:

《UNIX环境高级编程》

https://blog.csdn.net/qq_17268389/article/details/84650956

https://blog.csdn.net/happiness_llz/article/details/82345679

https://blog.csdn.net/yychuyu/article/details/80173039

0. 简述

看看进程创建 fork,进程切换 exec, 进程终止, 僵死进程

1. 进程的创建

pid_t fork(void); //创建一个子进程

当系统执行到fork()时,将执行下面动作:

  1. 向系统申请一个新的PID(Process Identifier)。

  2. 创建子进程,复制父进程的PCB(process control block)。

    获得父进程的数据空间、堆、栈等资源的副本。

  3. 如果创建子进程成功,则在父进程中 fork() 返回子进程的PID,在子进程中 fork() 返回0。

    若创建失败则返回 -1。

几个要注意的问题:

  1. 为什么fork()会调用一次返回两次,注意:是返回两次,而不是返回两个值,一个函数只能有一个返回值

    fork()函数在父进程中调用时,会在创建子进程过程中,复制了父进程的PCB,而PCB中就保存当前程序执行下一步的指令,所以父子进程相当于都执行在fork()函数中。父进程中返回为子进程申请的pid,而子进程的返回值被主动修改为0,然后再让子进程进入调度队列中,等待执行。

  2. 实际使用中,子进程在创建后往往会调用exec系列函数,所以,整个复制过程其实也不会真正完全复制,所以这块有个写时拷贝的技术节省内存。

    简单来说,子进程共用父进程的数据段,栈,堆空间,并且内核将这些空间设置为只读的。当父子进程中任意一个要修改这些空间的值时,此时才会在实际内存上拷贝要修改的区域,分配空间的最小单位是“页”(4KB)。

    关于写时拷贝 https://www.cnblogs.com/biyeymyhjob/archive/2012/07/20/2601655.html

  3. fork() 产生的父子进程是并发执行的,而且谁先执行是不确定的,这块有进程调度算法。

    fork()源码剖析 https://blog.csdn.net/Aspiration_1314/article/details/85604365

  4. CHILD_MAX 这个宏规定了最大进程数量。

pid_t vfork(void); //创建一个目的为执行其他程序的子进程

vfork() 和 fork() 的调用行为和返回值相同,但有点小区别:

  1. vork() 并不会将父进程的地址空间完全复制到子进程中。而是在子进程调用exec 或exit 之前,一直在父空间中运行。

    因为子进程会随后调用exec,从这点讲,vfork() 要比 fork() 后紧跟exec效果能好一点,因为虽然这块有写时拷贝,但不复制还是要比复制快一点。

  2. vfork() 会保证子进程先运行,在子进程调用exec 或者 exit 后,父进程才有可能被调度运行。

    如果子进程等待父进程一些动作,那么会导致死锁。

  3. 在子进程中结束要使用 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 打开的文件是共享的。代码段也是共享的。
posted @ 2019-11-30 23:50  沉云  阅读(157)  评论(0编辑  收藏  举报