前言
大家好吖,欢迎来到 YY 滴Linux系列 ,热烈欢迎! 本章主要内容面向接触过Linux的老铁
主要内容含:
欢迎订阅 YY滴C++专栏!更多干货持续更新!以下是传送门!
一.进程创建
1.fork函数
【1】fork函数与其返回值
- 它从已存在进程中创建一个新进程。新进程为子进程,而原进程为父进程。
- fork之前父进程独立执行,fork之后,父子两个执行流分别执行。注意,fork之后,谁先执行完全由调度器
决定。
#include <unistd.h>
pid_t fork(void);
返回值:在子进程中返回0
在父进程中返回子进程id,出错返回-1
【2】fork函数中的写时拷贝
- 通常,父子代码共享,父子再不写入时, 数据也是共享的
- 当任意一方试图写入,便以写时拷贝的方式各自一份 副本 (蓝色部分) ,具体见下图:
- 注意,其发生写时拷贝时,权限也会相应变化
2.fork函数/进程创建的场景——常规用法
- 一个父进程希望复制自己,使父子进程同时执行不同的代码段。 例如:父进程等待客户端请求,生成子进程来处理请求。
- 一个进程要执行一个不同的程序。例如:子进程从fork返回后,调用exec函数(后文进程替换会提到)
二.进程终止
1.进程退出的场景
- 代码运行完毕,结果正确
- 代码运行完毕,结果不正确
- 代码异常终止
2.进程应对不同退出场景的退出方法
前置知识:echo $? 查看进程退出码
- ?:保存的是最近一个子进程执行完毕时的退出码
- 情况1:?= 0 ,表示成功
- 情况2:?!=0 , 可以用1,2,3,4,5不同的数字表示不同的错误原因
echo $? ------指令
10 -------结果
前置知识:错误码VS退出码
-
**错误码:**通常是衡量一个库函数或者是一个系统调用一个 函数 的调用情况
-
**退出码:**通常是一个 进程 退出的时候,他的退出结果
-
**error错误码:**C语言提供的 全局变量 ,调用函数时如果出错了,他就会被设置成出错原因对应的数字
-
**strerror:**再把数字转换成描述出来的具体错误原因
#include <errno.h>
-- 测试调用失败情况
FILE *fp = fopen("./log.txt","r");
strerror():
printf("after: %d, error string : %s\n, errno, strerror(errno)");
【1】正常退出————通过退出码判断
1.exit函数 与 _exit函数
- 任意地点调用exit,表示进程退出,不进行后续执行
- exit( 退出码 ) 是 库函数 ——头文件:stdlib.h
- _exit( 退出码 ) 是 系统调用 ——头文件:unistd.h
- exit终止进程的时候, 会自动刷新缓冲区 。exit终止进程的时候, 不会自动刷新缓冲区
验证是否自动刷新缓冲区
- exit函数 会 自动刷新缓冲区
// 对比下面两个程序,一个带\n,一个不带\n
// \n是行刷新,刷新到显示器上
int main()
{
printf("you can see me!\n");
sleep(6);
exit(1);
}现象:遇到刷新条件,立刻刷新显示you can see me,6s后,程序退出
int main()
{
printf("you can see me!");
sleep(6);
exit(1);
}现象:不立刻显示,6s后程序退出,强制刷新显示
- _exit函数 不会 自动刷新缓冲区
// 对比下面两个程序,一个带\n,一个不带\n
// \n是行刷新,刷新到显示器上
int main()
{
printf("you can see me!\n");
sleep(6);
_exit(1);
}现象:遇到刷新条件,立刻刷新显示you can see me,6s后,程序退出
int main()
{
printf("you can see me!");
sleep(6);
_exit(1);
}现象:不立刻显示,6s后程序退出,也不会刷新显示
2.从main返回——return退出——等同于正常退出的exit
- return是一种更常见的退出进程方法
- 执行return n 等同于 执行exit(n)
- 因为调用main的运行时,函数会将 main函数 的返回值当做 exit 的参数。
- 换句话说, main函数 的退出码是可以被父进程获取的,用来判断子进程的运行结果
【2】异常退出(程序崩溃)——操作系统转换成信号——进程被操作系统杀掉
- 我们输入
kill -l
可以看到有许多信号 - 崩溃时就是执行了-9信号,有时也会显示其他信号
- 信号具体内容可以看YY后续信号博客
※ctrl + c也能强制退出——信号终止
三.进程等待
1.进程等待基本介绍
- 通过wait/waitpid的方式,让父进程(一般)对子进程进行 资源回收 的等待过程
2.进程等待的必要性
- 子进程退出,父进程如果不管不顾,就可能造成 ‘僵尸进程’的问题,进而造成内存泄漏——进程一旦变成僵尸状态,那就刀枪不入,“杀人不眨眼”的 kill -9 也无能为力 ,因为谁也没有办法杀死一个已经死去的进程。
- 我们需要知道父进程派给子进程的 任务完成的如何 ,如:子进程运行完成,结果对还是不对,或者是否正常退出。
- 父进程通过 进程等待 的方式,回收子进程资源,获取子进程退出信息
- 有时候进程也会 等待硬件资源 ,利用wait进程等待把自己挂起
3.如何进行等待(wait&waitpid)
【1】wait函数参数与返回值介绍&演示
- 返回值pid_t:成功返回 被等待进程pid ,失败返回-1。
- 参数status: 输出型参数 ,获取子进程退出状态, 不关心则可以设置成为NULL
#include<sys/types.h>
#include<sys/wait.h>
pid_t wait(int*status);
演示:
- 下方有一段程序,我们将参数设置成NULL即可
void Worker(int number)
{
int cnt = 5;
while(cnt)
{
printf("I am child process, pid: %d, ppid: %d, cnt: %d, number: %d\n", getpid(), getppid(), cnt--);//蓝色部分
sleep(1);
}
}
int main()
{
pid_t id = fork();
if(id == 0) {//child
Worker();
exit(0);
}
else{//father橙色部分
pid_t rid2 = wait(NULL);//wait函数
if(rid == id)
{
printf("wait success, pid:%d\n",getpid());
}
return 0;
}
- 执行结果如下
- 逐步打印完蓝色部分后(子进程),执行橙色部分(父进程)
- 本质:子进程和父进程同时存在——>子进程变成 僵尸状态 ——>等待结束时, 子进程消失 ,只剩下父进程
【2】waitpid函数参数与返回值介绍&演示
返回值:
- 当正常返回的时候,waitpid返回收集到的子进程的进程ID;
- 如果option设置了选项WNOHANG,而调用中waitpid发现没有已退出的子进程可收集,则返回0;
- 如果调用中出错,则返回-1 ,这时errno会被设置成相应的值以指示错误所在;
参数:
- pid:
Pid=-1, 等待任一一个子进程 。与wait等效。
Pid>0.等待其 进程ID与pid相等 的子进程。 - status:
WIFEXITED(status): 若为正常终止子进程返回的状态,则为真。(查看进程是否是正常退出)
WEXITSTATUS(status): 若WIFEXITED非零,提取子进程退出码。(查看进程的退出码)
无需其他操作,设置成NULL即可 - options:
- WNOHANG: 非阻塞等待。若pid指定的子进程没有结束,则waitpid()函数返回0,不予以等待。若正常结束,则返回该子进程的ID。
- option设置为0,表示阻塞等待
pid_ t waitpid(pid_t pid, int *status, int options);
演示:
- 下方有一段程序,我们将参数中,pid设置成fork出的子进程id,status设置成NULL,而option设置成0——表示阻塞就等待的设置
void Worker(int number)
{
int cnt = 5;
while(cnt)
{
printf("I am child process, pid: %d, ppid: %d, cnt: %d, number: %d\n", getpid(), getppid(), cnt--);
sleep(1);
}
}
int main()
{
pid_t id = fork();
if(id == 0)
{//child
Worker();
exit(0);
}
else{//father
printf("wait before\n");
pid_t rid = waitpid(id,NULL,0);//waitpid函数
printf("wait after\n");
if(rid == id)
{
printf("wait success, pid:%d\n",getpid());
}
return 0;
}
- 执行结果如下
- 先执行橙色部分(父进程)中的打印, 逐步打印完蓝色部分后(子进程) 【先后顺序与操作系统调度有关,不确定谁先开始,但一般是父进程最后退出】
- 本质:子进程和父进程同时存在——>子进程变成 僵尸状态 ——>等待结束时, 子进程消失 ,只剩下父进程
【3】输出型参数-status的原理演示&status规则&现象演示(要点)
pid_ t waitpid(pid_t pid, int *status, int options);
- status参数 为 整形指针 类型,是 输出型参数 —— status用于接受子进程的退出码
- 因为进程具有独立性,父进程无法直接获得子进程的退出信息
- 我们也可以打印status观察它的值,但是要 注意status的规则 ! —— 意味着不能对status整体使用
原理:
- 我们需要定义一个整型变量 status
- 把 status 的地址,即我们的参数给内核;内核再 把这个地址指向区域上的值给回用户层变量中
status规则:
- status是一个int的整数,一共32bit,只研究低16位
- status用于接受子进程的退出码
- 所以一共记录三种信息,1.退出状态 2.core dump标志 3.终止信号
- 但是并 不是每个信息都会用到 ,下图分别是穷举2个场景的例子——正常终止以及被信号所杀时的区域划分情况
现象演示:
- 把子进程退出码改成10
exit(10);
,通过status把子进程的退出信息返回
void Worker(int number)
{
int cnt = 5;
while(cnt)
{
printf("I am child process, pid: %d, ppid: %d, cnt: %d, number: %d\n", getpid(), getppid(), cnt--);
sleep(1);
}
}
int main()
{
pid_t id = fork();
if(id == 0)
{//child
Worker();
exit(10);//设置成10
}
else{//father
printf("wait before\n");
int status =0; //定义一个整型变量 status
pid_t rid = waitpid(id,&status,0);//通过status把子进程的退出信息返回
printf("wait before\n");
if(rid == id)
{
printf("wait success, pid:%d\n",getpid(),status);//打印status指向的区域的内容
}
return 0;
}
- 执行结果为
2560 //输出结果为2560,而不是10
- 分析:该情况为正常退出,10作为退出状态填到9-15位的区域中,而打印是打印整体,也就是2560
四.进程程序替换
1.进程替换概念
【1】进程替换概念
- 我们所创建的所有的子进程,执行的代码,都是父进程代码的一部分
- 如果我们想让子进程 执行新的程序呢???-----进程替换:执行全新的代码和访问全新的数据,不再和父进程有瓜葛
- 注意:进程替换不创建子进程—— 目标程序的进程不变pid不变,只是改变代码和数据
- 如果直接替换代码和数据区,耦合可能同时影响父子进程,要维持进程的独立性——写时拷贝解决;我们有结论: 程序替换也存在写时拷贝
【2】进程替换的机制&情景演示
机制:
- 进程替换成功: 子进程执行新的程序了, 剩下的代码不会执行了,被覆盖了。
- 进程替换失败: 继续执行子进程剩余内容。 只有失败才有返回值
演示:
- 当程序替换成功时,打印语句begin和执行替换后的语句,不打印语句end
- 当程序替换失败时,打印语句begin和语句end
//注:我们只要知道下面execl函数是 起到进程替换的作用就行
//即执行ls -a -l -n 指令,具体使用细节看下文exec类函数模块
int main()
{
pid_t id = fork();
if(id == 0)
{
printf("pid: %d, exec command begin\n",getpid());//语句begin
execl("/usr/bin/ls", "lsaaa", "-a", "-l", "-n", NULL);
printf("pid: %d, exec command end\n", getpid());//语句end
exit(1);
}
else{
// father
pid_t rid = waitpid(-1, NULL, 0);
if(rid > 0)
{
printf("wait success, rid: %d\n", rid);
}
}
return 0;
}
2.进程替换用到的exec类函数解释&命名规则&使用演示
【1】exec类函数
exec类函数有如下几种:都是为了满足各种调用的场景
- 通过man exec可查看
注意事项:
- 所有的exec类函数以 NULL结尾 表示完成
- 参数中后的…表示 可变参数
- exec用于程序替换,使用要满足:(1)必须找到可执行程序(2)必须要告诉exec*,怎么执行 ,下面会有具体演示
【2】exec类函数命名规则
- l(list) : 表示参数采用 列表——execl ,execlp,execle
- v(vector) : 参数用 数组——execv,execvp,execve
- p(path) : 有p 自动搜索环境变量PATH——execvp
- e(env) : 表示自己维护 环境变量——execve
【3】exec类函数使用演示
- exec用于程序替换,使用要满足:(1)必须找到可执行程序(2)必须要告诉exec*,怎么执行 ,下面会有具体演示
1. execl
#include <unistd.h>
int main()
{
//第一个参数找到可执行程序
//后面的参数以列表形式(带l)告诉exec怎么执行
//以NULL结尾
execl("/bin/ps", "ps", "-ef", NULL);
execl("/usr/bin/top", "top", NULL);
execl("/usr/bin/pwd", "pwd", NULL);
execl("/usr/bin/bash", "bash","test.sh", NULL);
exit(0);
}
2. execlp
#include <unistd.h>
int main()
{
//第一个参数,execlp带p的,可以使用环境变量PATH,无需写全路径
//后面的参数以列表形式(带l)告诉exec怎么执行
//以NULL结尾
execlp("ps", "ps", "-ef", NULL);
execlp("ls", "ls", "-a","-l", NULL);
exit(0);
}
3. execv和execvp
#include <unistd.h>
int main()
{
//第一个参数,execv和execvp带v的,使用数组传参
//后面的参数以列表形式(带l)告诉exec怎么执行
//以NULL结尾
char *const argv[] = {"ps", "-ef", NULL};
execv("/bin/ps", argv);
// 带p的,可以使用环境变量PATH,无需写全路径
execvp("ps", argv);
exit(0);
}
【4】exec类函数原理
- 事实上, 只有execve是真正的系统调用 ,其它五个函数最终都调用 execve
- 所以execve在man手册 第2节,其它函数在man手册第3节。这些函数之间的关系如下图所示