目录
fork函数初识
在linux中fork函数时非常重要的函数,它从已存在进程中创建一个新进程。新进程为子进程,而原进程为父进程
进程调用fork,当控制转移到内核中的fork代码后,内核做:
- 分配新的内存块和内核数据结构给子进程
- 将父进程部分数据结构内容拷贝至子进程
- 添加子进程到系统进程列表当中
- fork返回,开始调度器调度
当一个进程调用fork之后,就有两个二进制代码相同的进程。而且它们都运行到相同的地方。但每个进程都将可以开始它们自己的旅程,看如下程序
int main( void )
{
pid_t pid;
printf("Before: pid is %d\n", getpid());
if ( (pid=fork()) == -1 )perror("fork()"),exit(1);
printf("After:pid is %d, fork return %d\n", getpid(), pid);
sleep(1);
return 0;
}
运行结果:
[root@localhost linux]# ./a.out
Before: pid is 43676
After:pid is 43676, fork return 43677
After:pid is 43677, fork return 0
这里看到了三行输出,一行before,两行after。进程43676先打印before消息,然后它有打印after。另一个after消息有43677打印的。注意到进程43677没有打印before,为什么呢?如下图所示
所以,fork之前父进程独立执行,fork之后,父子两个执行流分别执行。注意,fork之后,谁先执行完全由调度器决定。
fork函数返回值
- 子进程返回0,
- 父进程返回的是子进程的pid
写时拷贝
通常,父子代码共享,父子再不写入时,数据也是共享的,当任意一方试图写入,便以写时拷贝的方式各自一份副本。
fork常规用法
- 一个父进程希望复制自己,使父子进程同时执行不同的代码段。例如,父进程等待客户端请求,生成子进程来处理请求。
- 一个进程要执行一个不同的程序。例如子进程从fork返回后,调用exec函数
fork调用失败的原因
- 系统中有太多的进程
- 实际用户的进程数超过了限制
进程终止
进程退出场景
- 代码运行完毕,结果正常
- 代码运行完毕,结果不正常
- 代码异常终止
当我们写了这样一个程序,我们写了一个makefile文件,用来运行编译程序
[wjy@VM-24-9-centos 16]$ cat makefile
myproc:myproc.c
gcc -o $@ $^
.PHONY:clean
clean:
rm -r myproc
[wjy@VM-24-9-centos 16]$ cat myproc.c
#include <stdio.h>
int main()
{
printf("hello world\n");
return 100;
}
[wjy@VM-24-9-centos 16]$ make
gcc -o myproc myproc.c
[wjy@VM-24-9-centos 16]$ ./myproc
hello world
[wjy@VM-24-9-centos 16]$ echo $?
100
[wjy@VM-24-9-centos 16]$ echo $?
0
[wjy@VM-24-9-centos 16]$ echo $?
0
每次echo $?都会显示上一个程序的退出码。echo无论是一个进程还是一个命令,命令大部分进程下都是一个进程,在命令行当中,每次echo获得的退出码都是一样的,都是0。这时运行完毕结果正确的情况下的返回值。
当ls后面带上的选项比如-a,-b,-c,有些选项是不支持的。当带上错误选项的时候,ls直接终止,当用echo测试他的退出码的时候,是2,它的退出码相当于main函数的返回值,这里的2是代码运行完毕,结果不正确的返回值。
[wjy@VM-24-9-centos 16]$ ls -a -b -c -d -e
ls: invalid option -- 'e'
Try 'ls --help' for more information.
[wjy@VM-24-9-centos 16]$ echo $?
2
以上两种情况是在代码运行完毕后的表现结果:
- 0代表运行成功的返回值(success)
- 非0,也就是运行完毕,结果不正确的返回值(failed),非0的每一个数字,都是错误运行的原因,表明程序为什么运行错误。通过返回不同的数字,表示每一种错误的情况。
strerror:用来把数字转化成错误信息。
非0,每一种都代表一种错误原因,这里的错误信息都是基于C语言的库实现的,如果不想用系统给的,也可以自己实现,自己定义一个指针数组,如char* arr[140],每一个指针存放一种错误码信息,下标代表错误码编号。
[wjy@VM-24-9-centos 16]$ cat myproc.c
#include <stdio.h>
#include <string.h>//strerror的头文件
int main()
{
for(int i=0;i<140;i++)
{
printf("%d:%s\n",i,strerror(i));
}
return 0;
}
//错误码信息
[wjy@VM-24-9-centos 16]$ ./myproc
0:Success
1:Operation not permitted
2:No such file or directory
3:No such process
//...中间太多就不放出来了
//打印到133就没有了,所以错误码信息从0开始一共有134个
132:Operation not possible due to RF-kill
133:Memory page has hardware error
134:Unknown error 134
135:Unknown error 135
136:Unknown error 136
137:Unknown error 137
138:Unknown error 138
139:Unknown error 139
所以当用ls命令要显示一个文件,但是报出错误信息,告诉我们没有这个文件,用echo $?查看错误码,是2,再对应上面的表,发现2对应的信息就是No such file or directory。
[wjy@VM-24-9-centos 16]$ ls -a myfile.txt
ls: cannot access myfile.txt: No such file or directory
[wjy@VM-24-9-centos 16]$ echo $?
2
第三种情况是代码异常终止。
下面这段代码,make编译时候会报错,运行最后有一条显示错误的语句。这种程序运行到一半就异常终止的情况,我们称之为程序崩溃!
当我们用echo $?查看进程退出码的时候,发现是136,无论错误是在哪报的,程序在哪里开始崩溃的,进程吗都是136(可以自己试试),而136对应的错误码信息,是unknown,是未知的。这是因为程序编译出错,错误码信息已经没有意义。需要解决的东西,已经不归错误码管了。
[wjy@VM-24-9-centos 16]$ cat myproc.c
#include <stdio.h>
#include <string.h>
int main()
{
for(int i=0;i<140;i++)
{
printf("%d:%s\n",i,strerror(i));
}
int a=10;
a/=0;
printf("%d\n",a);
return 0;
}
//make编译发现错误
[wjy@VM-24-9-centos 16]$ vim myproc.c
[wjy@VM-24-9-centos 16]$ make
gcc -o myproc myproc.c -std=c99
myproc.c: In function ‘main’:
myproc.c:10:4: warning: division by zero [-Wdiv-by-zero]
a/=0;
[wjy@VM-24-9-centos 16]$ ./myproc
0:Success
1:Operation not permitted
2:No such file or directory
3:No such process
//...
132:Operation not possible due to RF-kill
133:Memory page has hardware error
134:Unknown error 134
135:Unknown error 135
136:Unknown error 136
137:Unknown error 137
138:Unknown error 138
139:Unknown error 139
Floating point exception//运行到最后,有一个错误提示语句
[wjy@VM-24-9-centos 16]$ echo $?
136
进程退出的方式
进程退出方式
- 从main返回
- 调用exit
- _exit
上面我们打印的都是main函数的退出码,那么可不可以从非main函数获得退出码呢?
当调用一个非main函数,会打印它的值。func函数/非main函数的返回值,叫做函数返回。
main函数的返回,叫做进程退出。
[wjy@VM-24-9-centos 16]$ cat myproc.c
#include <stdio.h>
#include <string.h>
int func()
{
printf("func test\n");
return 1;
}
int main()
{
func();
for(int i=0;i<5;i++)
{
printf("%d:%s\n",i,strerror(i));
}
return 0;
}
//运行结果
[wjy@VM-24-9-centos 16]$ make
gcc -o myproc myproc.c -std=c99
[wjy@VM-24-9-centos 16]$ ./myproc
func test
0:Success
1:Operation not permitted
2:No such file or directory
3:No such process
4:Interrupted system call
还有一种叫做,进程终止,就是用exit(进程码)来退出进程。
下面的程序,将exit卸载func函数后,运行后,exit后面的代码都不执行了。并且它的进程退出码就是12.所以无论exit在哪里调用,进程都会退出。
[wjy@VM-24-9-centos 16]$ cat myproc.c
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
int func()
{
printf("func test\n");
return 1;
}
int main()
{
func();
exit(12);//设置进程退出码为12
for(int i=0;i<5;i++)
{
printf("%d:%s\n",i,strerror(i));
}
return 0;
}
//运行结果
[wjy@VM-24-9-centos 16]$ ./myproc
func test
[wjy@VM-24-9-centos 16]$ echo $?
12
hello world[wjy@VM-24-9-centos 16]$ echo $?
0
exit不仅能显示退出码,还有一个功能,看下面代码
当打印hello world语句,没有换行,sleep语句休眠四秒之后,hello world语句会先放到缓冲区中,四秒之后从缓冲区加载出来打印到显示器上。用return显示进程退出码可以让缓冲区的内容加载出来。
同样exit(EXIT_SUCCESS)跟return 0 ,一样进程退出码是0(因为success的对应退出码是0),而且sleep4秒后,将内容从缓冲器刷新到显示器上。exit和main return本身就会要求系统进行缓冲区刷新。
#include <stdio.h>
#include <unistd.h>
int main()
{
printf("hello world");
sleep(4);
return 0;
}
//运行结果
[wjy@VM-24-9-centos 16]$ ./myproc
hello world[wjy@VM-24-9-centos 16]$
但是_exit,虽然能显示退出码,但是它并不会将想要的内容显示出来。
总结:
- main函数return,代表进程退出;非main函数的return代表函数返回。
- exit在任意地方调用,都代表终止进程,参数是退出码。
- _exit终止进程,强制终止进程,不会进行进程的后续收尾工作,比如刷新缓冲区。(这里的缓冲区是用户及缓冲区)
我们用一张图来解释:
exit()会有以下图的过程,而_exit会直接走到终止程序的地方。二者都要走到最后kernel的终止进程。如果缓冲区在kernel这种操作系统上的缓冲区,那么exit和_exit都要执行刷新缓冲区的操作,但是_exit没有刷新,说明缓冲区不在操作系统上。
进程退出的用处
进程退出,在OS(操作系统)层面做了什么?
系统层面,代表少了一个进程:要把进程的PCB,mm_struct,页表和各种映射关系,代码+数据申请的空间都给释放掉。
进程等待
进程等待必要性
进程等待是什么?
当一个进程fork之后创建了子进程,子进程就是为了帮助父进程完成某种任务,从而创建。那么为了让父进程知道子进程的完成进度,什么时候完成的,父进程需要通过wait/waitpid等待子进程退出,这种现象就叫进程等待。
为什么要让如进程等待?
- 通过获取子进程退出的信息,能够得知子进程执行的结果。父进程派给子进程的任务完成的如何,我们需要知道。如,子进程运行完成,结果对还是不对,或者是否正常退出。
- 可以保证:时序问题,子进程先退出,父进程后退出的话。如果父进程没活过子进程,子进程就变成孤儿进程,子进程被系统领养,父进程看不见退出码,这样就得不到退出码现实的各种问题。
- 进程退出的时候会先进入僵尸状态,会造成内存泄漏问题,需要通过父进程wait,释放该子进程占用的资源。如果父进程不进行释放子进程的资源,进程一旦变成僵尸状态,那就刀枪不入,“杀人不眨眼”的kill -9 也无能为力,因为谁也没有办法杀死一个已经死去的进程。
- 父进程通过进程等待的方式,回收子进程资源,获取子进程退出信息
进程等待的方法
下面这段代码中,fork之后,父进程直接退出,子进程继续运行,5秒之后才退出,子进程变成孤儿进程,
所以我们要父进程等待,即使父进程什么事情都不做,他也要等待子进程释放完,返回了进程退出码,他才能释放。
wait方法
如果加了等待,这段代码就会变成:起初父进程先sleep(10)睡眠10秒,在这10秒当中,子进程会先运行,并且父进程不会跟踪子进程的状态,因为父进程在休眠,等5秒之后,子进程运行完变成Z(僵尸)状态。等待10秒之后,父进程开始运行,将子进程回收,子进程就会没有。回收之后,又等待了10秒,此进程结束运行。这证明了,等待可以回收子进程。
#include <sys/types.h>
#include <sys/wait.h>
int main()
{
pid_t id=fork();
if(id==0)
{
//child
int cnt=5;
while(cnt)
{
printf("child[%d] is running:cnt is:%d\n",getpid(),cnt);//获得自己的pid
cnt--;
sleep(1);
}
exit(0);
}
sleep(10);
printf("father wait begin!\n");
//parent
pid_t ret=wait(NULL);
if(ret>0)//等待成功,返回子进程pid
{
printf("father wait:%d\n",ret);
}
else{//如果等待失败,返回-1
printf("father wait failed!\n");
}
sleep(10);
return 0;
}
//运行结果
[wjy@VM-24-9-centos 16]$ ./myproc
child[9747] is running:cnt is:5
child[9747] is running:cnt is:4
child[9747] is running:cnt is:3
child[9747] is running:cnt is:2
child[9747] is running:cnt is:1
father wait begin!
father wait:9747
waitpid方法(重点)
那么用waitpid的方法怎么写呢?其实只是稍稍改动了以下,并且结果和上面是一样的。
int main()
{
//...
//pid_t ret=waitpid(id,NULL,0);//等待指定一个进程
pid_t ret=waitpid(-1,NULL,0);//等待任意一个子进程,等价于wait
if(ret>0)//等待成功,返回子进程pid
{
printf("father wait:%d,success\n",ret);
}
else{//如果等待失败,返回-1
printf("father wait failed!\n");
}
sleep(10);
}
//运行结果
[wjy@VM-24-9-centos 16]$ ./myproc
child[11290] is running:cnt is:5
child[11290] is running:cnt is:4
child[11290] is running:cnt is:3
child[11290] is running:cnt is:2
child[11290] is running:cnt is:1
father wait begin!
father wait:11290,success
status的理解
status是一个输出型参数,他是一个指针。也就是说,只要传入一个指针就能改变它里面的内容(通过解引用改变)。而status里面存放的是等待进程的状态。
当我们将status设置成0,那么得到的status是0.
如果改变子进程的退出码,那么父进程等待子进程的状态又是什么呢?它的status变成了一个不知道的数字,这是为什么呢?
因为父进程一定要让父进程通过status得到子进程的结果,父进程拿到什么status结果,一定和子进程如何退出强相关!!子进程退出的话,就是我们杠杆讲的进程退出码,那么这个结果就是上面所说的进程退出的三种结果:
- 代码运行完毕,返回的正确结果 -- 0;
- 代码运行完毕,反悔的不正确结果 -- 非0数字
- 代码异常终止。
status是32个比特位:只使用低16个比特位,高16个比特位我们暂时不用。
当程序运行完毕,会返回一个退出码,程序代码异常会给出一个信号。那么我们怎么知道程序到底执行完毕了吗?所以进程运行结束,子进程pid会给出两种信息,先给出信号,查看子进程是否正常执行完。这个信号会给到0-7位。如果程序给出信号,那么该进程的代码异常终止。如果是正常情况下,大部分都是0,那么程序会运行完毕,启用后八位来给出退出码信息,这是第二种信息。
还有一个是core dump标志,也是一个信号。
让我们来尝试验证一下这个退出码和信号的值。因为退出状态的退出码在高八位,所以将它右移8位之后,再与上(& 0 .. 0 1111 1111 ),16进制表示0xFF.这样就能得到退出码。
异常信号在0-7位,所以直接与上(&)0111 1111,16进制表示0x7F,得到信号码。
下面这段程序,子进程的退出码设置为11,父进程等待运行后,进程退出信号为0,说明程序没有异常,正常结束,而显示的进程退出码就是子进程exit中设置的进程退出码。验证了我们上面的问题。
int main()
{
pid_t id=fork();
if(id==0)
{
//child
int cnt=3;
while(cnt)
{
printf("child[%d] is running:cnt is:%d\n",getpid(),cnt);
cnt--;
sleep(1);
}
exit(11);
}
//parent
int status=0;
pid_t ret=waitpid(id,
if(ret>0)//等待成功,返回子进程pid
{
printf("father wait:%d,success,status exit code:%d,status exit signal:%d\n",ret,(status>>8)&0xFF,status//子进程的退出码和退出状态
}
else{
printf("father wait failed!\n");
}
return 0;
}
//运行结果
[wjy@VM-24-9-centos 16]$ ./myproc
father wait begin!
child[30125] is running:cnt is:3
child[30125] is running:cnt is:2
child[30125] is running:cnt is:1
father wait:30125,success,status exit code:11,status exit signal:0
下面是进程异常退出对应的不同的信号,可以哟个kill -l命令查看。
所以所有命令行启动的所有进程的父进程是谁呢?通过验证发现,它的父进程ppid对应的就是bash,bash是命令行启动的所有进程的父进程!bash一定是通过wait方式得到子进程的推出结果,所以我们用echo &?能看到子进程的退出码!
waitpid获取退出码和信号的正确表达
上面我们获取子进程的退出码和信号,用到的是位操作运算符,这样并不规范。所以waitpid里面提供了一种不用位操作的宏。
int main()
{
pid_t id=fork();
if(id==0)
{
//child
int cnt=5;
while(cnt)
{
printf("child[%d] is running:cnt is:%d\n",getpid(),cnt);//获得自己的pid
cnt--;
sleep(1);
}
exit(1);
}
//parent
int status=0;
pid_t ret=waitpid(id,
if(WIFEXITED(status))//说明没有收到任何退出信号
{
//正常结束
printf("exit code:%d\n",WEXITSTATUS(status));
}
else{
printf("error,get a signal!\n");
}
}
//运行结果
[wjy@VM-24-9-centos 16]$ ./myproc
child[16413] is running:cnt is:5
child[16413] is running:cnt is:4
child[16413] is running:cnt is:3
child[16413] is running:cnt is:2
child[16413] is running:cnt is:1
exit code:1
waitpid的理解
waitpid是系统调用接口,系统调用接口都是用户调的,用户通过status输出型参数将地址传给waitpid。在操作系统内部,有一个父进程和子进程。waitpid通过调用父进程,来获得子进程的退出码。子进程里有虚拟内存,虚拟内存通过页表映射找到物理内存。子进程结束后进入僵尸状态:PCB保存进程退出时的退出数据,所以子进程的PCB里有两个东西,一个是exit_code(退出码),一个是signal(退出信号)。
当我们定义了一个int* status指针通过waitpid传进来,这时候子进程里的exit_code会赋值给指针指向的status空间,*status_p=exit_code。这个值也是分段赋的,上面获取的时候是右移和与操作,那么这里从子进程赋给status时候*status_p |=(exit_code<<8);*status_p|=signal;然后通过waitpid返回给用户,用户就能卡到这个值(或操作是因为status初始值是0,如果想要获得信号和退出码,就要用或运算符获得)
【options的理解】:
options是父进程在等待时候的状态,在上面设置0,0是默认值,是一个默认行为,代表阻塞等待。如果设置为WNOHANG代表等待方式为非阻塞等待。
什么是阻塞和非阻塞?
举个例子,当A在楼下要请B下楼吃饭,但是B对A说我有事情你先等半个小时,A说好,在这半小时之内A什么都不干,就等B。当A每隔10分钟就对B进行一个询问,检测B的状态,那么这个就是非阻塞状态。如果这30分钟之内什么都不干,直到B下楼了才有所反应,那么就是阻塞状态。
阻塞:
当操作系统有父子两个进曾,父进程在waitpid()的情况下等待子进程,父进程进入祖泽状态,被放入了等待队列。当操作系统OS检测到子进程运行完毕,父进程就从等待状态(S)变为运行状态(R)。
- 阻塞的本质:其实是进程的PCB被放入了等待队列,并将进程的状态改为S状态。
- 返回的本质:进程的PCB从等待队列拿到R队列,从而被CPU调度。
让我们使用非阻塞的代码,并查看它的结果
#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)
{
//child
int cnt=10;
while(cnt)
{
printf("child[%d] is running:cnt is:%d\n",getpid(),cnt);//获得自己的pid
cnt--;
sleep(1);
}
exit(1);
}
//parent
int status=0;
while(1)//轮询检测
{
pid_t ret=waitpid(id,&status,WNOHANG);
if(ret==0)
{
//子进程没有退出,但是waitpid等待是成功的,需要父进程重复进行等待
printf("Do father things!\n");
}
else if(ret>0)
{
//子进程退出了,waitpid也成功了,获取到了对应的的结果
printf("father wait:%d,success,status exit code:%d,status exit signal:%d\n",ret,(status>>8)&0xFF,status
break;
}
else//ret<0
{
perror("waitpid!\n");
break;
}
sleep(1);//父进程每隔1秒检测一次
}
//运行结果
[wjy@VM-24-9-centos 16]$ ./myproc
Do father things!
child[8089] is running:cnt is:10
Do father things!
child[8089] is running:cnt is:9
Do father things!
...
Do father things!
child[8089] is running:cnt is:2
Do father things!
child[8089] is running:cnt is:1
Do father things!
father wait:8089,success,status exit code:1,status exit signal:0