本文参考博客链接:
https://blog.csdn.net/zz709196484/article/details/54770017
###进程创建
进程的创建主要是通过调用fork( )函数和vfork( )函数来实现的。
####fork()
fork()函数的基本形式如下:
#include
pid_t fork(void){}
pid_t实际上就是short类型,这个在sys/types.h中定义:
typedef short pid_t;
当fork()创建子进程成功后,父进程返回的是子进程的pid,子进程返回0值,如果创建失败,父进程返回-1.
来看一段简单的代码:
1 #include
2 #include
3 #include
4
5
6 int main()
7 {
8 printf("before:%d\n",getpid());
9 pid_t pid = fork();
10 if (pid < 0)
11 {
12 printf("fork error!!\n");
return -1;
13 }
14 printf("after:%d\n",getpid());
15 sleep(1);
16 return 0;
17 }
我们发现,在fork( ) 之前的before只打印了一句,而fork( )之后的after却打印了两句,且3句中有2句是在进程5988内打印的,很明显,5988是父进程的pid,5989是子进程的pid,为什么子进程不打印before呢?
这里我们就得讲一下父子进程的概念了,子进程是如何创建的呢?他与父进程有什么样的关系呢?
解释:每个进程都有自己的进程数据块PCB,操作系统正是通过管理PCB来管理进程的,而子进程的PCB是由父进程拷贝而来的。我们都知道,PCB中有一个程序计数器,用来存储下一句要执行指令的地址,子进程自然也拷贝了父进程的程序计数器,那子进程在创建好后,cpu就会按照程序计数器,让子进程直接执行fork()后面一句指令,这也就是为什么子进程没有打印before那一句的原因。
####fork()与写时拷贝
上面已经说过,子进程的PCB源自于父进程拷贝而来,所以父子进程共享数据段和代码段,但是,创建子进程的目的终究是想让子进程做一些不同于父进程的东西,所以,当改变子进程内数据时,系统就会为子进程的数据重新开辟空间,与父进程分开存储,这就和写时拷贝是一样的(不修改就赚了,不会另外开辟空间)。
下图是关于子进程修改数据后,系统为其开辟空间的图解:
####vfork()
vfork()函数与fork()函数极为相似,都是用于创建新进程,他和fork()的不同之处在于:
1.由vfork()函数创建的子进程是与父进程共享数据段和代码段的,不论修补修改子进程内的数据,都不另外开辟空间,这是真正的**“共享”**。
2.vfork()函数会保证子进程优先运行,且父进程会阻塞在vfork()函数后,直到子进程调用exec或exit后才会继续运行。
(注意!如果子进程没有调用exec或者exit,直接return,会释放父进程的资源,但注意此时父进程还阻塞在vfork()之后。这会造成严重的后果!linux下会造成调用栈混乱…亲测)
来用代码测试一下:
1 #include
2 #include
3 #include
4
5 int i=10;//这里的i定义或者定义在vfork之前都可以,不可以定义在vfork之后,
//倘若定义在vfork之后,父进程先阻塞,子进程创建变量i并修改程20,子进程退出,但
//不释放资源,父进程此刻从vfork后面指令开始运行,运行到"int i = 10"就会把i重
//新赋值成10
6 int main()
7 {
8 pid_t pid = vfork();
9 if (pid < 0)
10 {
11 perror("vfork error:");
12 return -1;
13 }
14 else if (pid == 0)//child
15 {
16 i=20;
17 printf("this is child process:%d i=%d\n ",getpid(),i);
18 exit(EXIT_SUCCESS);
19 }
20 else//parent
21 {
22 printf("this is parent process:%d i=%d\n",getpid(), i);
23 }
24
25 return 0;
26 }
###进程终止
进程退出的情况有如下3中:
1.代码运行完毕,结果正确
2.代码运行完毕,结果不正确
3.程序异常退出
##常见的进程退出方法
#include
void _exit(int status)//status定义了进程的终止状态,父进程通过wait来获取该值.
//虽然status是int,但是仅有低9位可以被父进程所用,所以在_exit(-1)
//时,返回值是255
_exit又叫暴力退出,直接使进程停止运行,清除其使用的内存空间,并销毁其在内核中的各种数据结构;
2.exit
#include
void exit(int status)
exit() 函数在_exit上作了一些包装,在执行退出之前加了若干道工序,也是因为这个原因,有些人认为exit已经不能算是纯粹的系统调用。
###exit与_exit()之间的区别
简单的说,_exit终止调用进程,但不关闭文件,不清除输出缓存,也不调用出口函数。而exit关闭文件,清空输出缓存,调用用户的清理函数.
(exit()函数与_exit()函数最大的区别就在于exit()函数在调用exit系统调用之前要检查文件的打开情况,把文件缓冲区中的内容写回文件,就是"清理I/O缓冲"。 )
####为何在一个fork的子进程分支中使用_exit函数而不使用exit函数?
‘exit()’与‘_exit()’有不少区别在使用‘fork()’,特别是‘vfork()’时变得很突出。
‘exit()’与‘_exit()’的基本区别在于前一个调用实施与调用库里用户状态结构(user-mode constructs)有关的清除工作(clean-up),而且调用用户自定义的清除程序 (自定义清除程序由atexit函数定义,可定义多次,并以倒序执行),相对应,_exit函数只为进程实施内核清除工作。
在由‘fork()’创建的子进程分支里,正常情况下使用‘exit()’是不正确的,这是 因为使用它会导致标准输入输出(stdio: Standard Input Output)的缓冲区被清空两次,而且临时文件被出乎意料的删除(临时文件由tmpfile函数创建在系统临时目录下,文件名由系统随机生成)。在C++程序中情况会更糟,因为静态目标(static objects)的析构函数(destructors)可以被错误地执行。
(还有一些特殊情况,比如守护程序,它们的父进程需要调用‘_exit()’而不是子进程;
3.return
return是很常见的退出进程的方式,但只有在main( )函数中调用return才能结束进程.
其实,return (n)和exit( n )是一样的,因为调用main的运行时函数会将main的返回值做为exit的参数.
###进程等待
####进程为什么要等待?
学过进程的人都知道,在父进程创建子进程后,父进程要等待子进程退出,读取他的退出信息,释放他的资源,否则就会产生僵尸进程,而进程等待就是这么一回事 : 防止僵尸进程的产生
####进程等待的方法
###1.wait( )
#include
#include
pid_t wait(int *status)
//返回值:成功则返回被等待进程的pid,失败则返回-1
//status:输出型参数,获取子进程的退出状态,如果不关心,可以设置成NULL
####wait函数的效果
如果有子进程再运行,那么当前父进程就处于阻塞状态(可以理解为什么事情都没有做,一直在检测子进程是否在运行);
如果子进程都已经终止,那么wait可立即获得子进程的终止状态(退出码,退出信息),子进程的终止状态是体现在status参数上的,另外wait还会返回所终止的子进程的标识符;
如果当前进程没有任何子进程,那么wait会立即出错返回(此时返回值为-1);
如果有一个子进程终止,那么wait便返回;
###waitpid()
#include
#include
pid_t waitpid(pid_t pid, int *status, int options);
####waitpid函数的参数
###关于参数status
我们发现wait( )函数和waitpid( )函数中都有一个status参数.
这是一个输出型参数,获取子进程的退出状态,如果不关心,可以设置成NULL,否则,他将接收子进程的退出码并反馈给父进程,也就是说,父进程就是通过status这个参数来获得子进程的退出信息的,可见他的重要性.
虽然他的参数类型是int * ,但是真正有效的只有低16位,也就是低2个字节,在这低16位中:
高8为存储的是退出码,程序运行完毕退出才会有,此时低8位为0
低8位存储的是引起异常退出的信号值,当程序异常退出时,此时高8位为0
最后附上wait函数和waitpid函数的测试代码
###wait
1 #include
2 #include
3 #include
4 #include
5 #include
6
7
8 int main()
9 {
10 pid_t pid;
11 pid =fork();
12 if (pid < 0)
13 {
14 perror("fork error:");
15 return -1;
16 }
17 else if (pid==0)
18 {
19 //child
20 printf("child process:%d\n",getpid());
21 sleep(15);
22 exit (10);
23 }
24 else//父进程
25 {
26 int i;//i用来接收wait函数的返回值,用来判断是否等待到子进程
27 int j;//j用来接收子进程退出码,等同于wait函数的status参数
28
29 i=wait(&j);
30 if (i>0 && (j&0x7F)==0)//0x7F代表16进制的127,即二进制中低7为全为1,其余为0
31 {
32 //正常退出
33 printf("the child process pid is: %d\n",i);
34
35 printf("the exit num is :%d\n",(j>>8)&0x7F);
36
37 }
38 else if (i>0)//低8位不全为0,说明异常退出,此时打印退出码
39 {
40 printf("the child process exit error:%d\n",j&0x7F);
41
42 }
43
44 }
45
46 return 0;
47 }
###waitpid
*waitpid.c*/
#include
#include
#include
#include
#include
int main()
{
pid_t pc,pr;
pc=fork();
if(pc<0)
{
printf("error fork!\n");
exit(0);
}
else if(pc==0) /*子进程*/
{
/*子进程暂停5s*/
sleep(5);
/*子进程正常退出*/
exit(0);
}
else /*父进程*/
{
/*循环测试子进程是否退出*/
do
{
/*调用waitpid,且父进程不阻塞*/
pr=waitpid(pc,NULL,WNOHANG);/*pc是指定的子进程,只要指定的子进程没结束就会一直等下去*/
/*若子进程还未退出,则父进程暂停1s*/
if(pr==0)
{
printf("the child process has not exited\n");
sleep(1);
}
}while(pr==0);
/*若发现子进程退出,打印出相应信息*/
if(pr==pc)
{
printf("get child exit code:%d\n",pr);
}
else
printf("some error occured\n");
}
return 0;
}
运行一下: