第四节:进程控制
一、进程创建
1.1 fork函数
1.
fork
函数有两个返回值问题
返回的本质就是写入!所以,谁先返回,谁就先写入id,因为进程具有独立性,会发生写时拷贝,父进程和子进程各自指向return语句。
2.
fork
返回后,给父进程返回子进程pid
,给子进程返回0
一个子进程有且仅有一个父进程,使用子进程找到父进程很简单;但是使用父进程找子进程很麻烦,必须要有子进程相关的数据。
3. 同一个id值,会保存两个不同的值,同时让
if
和else if
执行
在fork
函数中,当子(父)进程尝试写入数据,操作系统会进行数据拷贝到新的区域、更改该区域对应页表、再让进程修改数据,为了保证进程的独立性。这时就会出现对同一个变量的同一个虚拟地址,会有两个不同输出值。(写时拷贝)
所以,调用fork函数之前,父进程独立执行;调用fork之后,父子两个执行流分别执行。 fork
之后,谁先执行,完全由调度器决定。
1.2 写时拷贝
通常,父子代码共享,父子再不写入时,数据也是共享的,当任意一方试图写入,便以写时拷贝的方式各自一份副本。如下图:
在这个过程中,子进程改变没有影响父进程。写时拷贝:写的时候进行拷贝。
虚拟地址空间+页表 用于保证进程的独立性;一旦有执行流想要写入代码或数据,就会发生写实拷贝。
1.3 fork用法
- 父进程希望复制自己,使父子进程同时执行不同进程
- 一个进程要执行一个不同的程序。例如:子进程从
fork
返回之后,调用exec
函数。
1.4 fork调用失败
- 系统中有太多进程
- 达到实际用户的进程数超过了限制
二、进程终止
2.1 进程退出码
在自己写C/C++ 代码时,最后总是写一个 return 0
。在系统中,这个语句是:进程退出时,对应的退出码。用于标定进程执行的结果是否正确。
写代码是为了完成某件事,可是在代码执行结束后,我怎么知道代码跑的结果对不对呢?—进程退出码
./mytest # 运行一个进程
echo $? # 永远记录最近一个进程在命令中执行完毕时对应的退出码(main->return ?;)
如果我们不关心进程退出码,直接
return 0
就好了;如果关心进程退出码,就要返回特定数据表明特定错误。退出码的意义:0—成功; !0—表示失败。
一般而言,退出码都有对应的文字描述。可以自定义,也可以使用系统的映射关系(不太频繁)
2.2 进程退出场景
进程退出一共只有三种
- 代码跑完了,结果正确—return 0
- 代码跑完了,结果不正确–return !0
- 代码没跑完,程序异常了,退出码无异意义
2.3 进程如何退出(结束)
- 从
main
函数返回 - 任意地方调用
exit
— 库函数;在进程终止之后会主动刷新缓冲区 _exit
终止进程 — 系统调用;在进程终止之后不会刷新缓冲区
这里的缓冲区是指—用户级缓冲区(基础IO细讲)
三、进程等待
之前讲过,进程状态有一个
Z状态
—会造成进程泄露;我们通过进程等待解决僵尸进程问题。
3.1 进程等待的方法
wiait
方法
#include<sys/types.h>
#include<sys/wait.h> pid_t wait(int*status);
- 返回值:成功—返回被等待进程pid;失败—返回 -1;
- 参数:输出型参数,获取子进程退出状态,不关心则可以设置为 NULL。
waitpid
方法
bash pid_ t waitpid(pid_t pid, int *status, int options);
- 返回值:
- 当正常返回的时候
waitpid
返回收集到的子进程的进程ID;- 如果设置了选项
WNOHANG
, 而调用中waitpid
发现没有已退出的子进程可收集,则返回0;- 如果调用中出错,则返回-1,这时
errno
会被设置成相应的值以指示错误所在;- 参数:
pid
:
- pid=-1,等待任一个子进程。与wait等效。
- pid>0.等待其进程ID与pid相等的子进程。
status
:
- WIFEXITED(status): 若为正常终止子进程返回的状态,则为真。(查看进程是否是正常退出)
- WEXITSTATUS(status): 若WIFEXITED非零,提取子进程退出码。(查看进程的退出码)
options
:
- WNOHANG: 若pid指定的子进程没有结束,则waitpid()函数返回0,不予以等待。若正常结束,则返回该子进程的ID。
3.2 再谈进程退出
- 进程退出,该进程会变成僵尸程序------会把自己的退出结果写入到自己的
task_struct
。 wait/waitpid
是一个系统调用 —>OS—>OS有资格也有能力去读取子进程的task_struct
。- 所以退出结果是操作系统从
task_struct
中读取到的。
3.3 非阻塞式等待
WNOHANG
非阻塞---->子进程没有退出,父进程检测后立即返回,继续自己的任务。waitpid
调用成功&&子进程没有退出:我的waitpid
没有等待失败,仅仅是监测到了子进程没有退出,父进程立即返回。waitpid
调用成功&&子进程退出:父进程回收子进程。waitpid
调用失败:返回值为 -1。- 非阻塞等待不会占用父进程所有时间,在轮询期间可以处理其他任务。
3.4 进程等待小结
- 是什么:通过系统调用,让父进程等待子进程的一种方式。
- 为什么:释放僵尸子进程、获取子进程状态 。
- 怎么办:使用
wait
或waitpid
函数。
四、进程替换
(引子)创建子进程的目的是什么?
- 想让子进程执行父进程代码的一部分(执行父进程对应的磁盘代码中的部分)
- 想让子进程执行一个全新的程序(让子进程想办法,加载磁盘上指定的程序,执行新程序的代码和数据)
我们把“加载磁盘上指定的程序,执行新程序的代码和数据”叫做进程的程序替换
4.1 替换原理
- 调用
exec
系列函数进行程序替换,本质就是讲指定程序的代码和数据加载到指定位置,覆盖自己的代码和数据。 - 程序替换没有创建新的进程。
- 在调用程序替换函数后,函数后续的代码就无法执行,被替换掉了。
exec
系列函数没有调用成功的返回值,调用成功了就和接下来的代码没关系了(第三点),所以该函数调用后只要有返回,就一定是出错了。
4.2 替换函数
常用的以 exec
开头的函数:
#include <unistd.h>// l->list:将参数一个一个的传入exec*。
int execl(const char *path, const char *arg, ...);
// p->path:如何找到程序的功能,带 p 字符的函数,不用告诉我程序的路径。
//你只要告诉我程序的名字,我自己会到系统中去找。
int execlp(const char *file, const char *arg, ...);
int execle(const char *path, const char *arg, ...,char *const envp[]);// v->vector:可以将所有的执行参数,放入数组中,统一传递,而不用进行使用可变参数方案.
int execv(const char *path, char *const argv[]);
// e->envp:环境变量
int execvp(const char *file, char *const argv[]);
int execvpe(const char *file, const char *argv[], char *const envp[]);
4.3 试验
我们可以综合前面的知识,做一个简易的shell。
放在后面单独写一篇文章。