一、进程的创建、终止和等待。
1.进程创建
fork():进程是处于执行期的程序以及相关资源的总称,进程在创建它的时候开始存活,在linux系统中,通过使用fork()函数复制一个现有进程来创建一个新进程,调用fork()函数的进程称为父进程,新产生的进程称为子进程,子进程几乎完整的复制了父进程,但至少pid不一样。调用fork()函数系统从内核返回两次:一次返回到父进程,另一次返回到新的子进程。
fork()函数的头文件:
#include<unistd.h>
fork()函数的函数原型:
pid_t fork(void);子进程返回0,父进程返回子进程的pid,fork()函数一次创建两次返回。
exec():创建新的进程之后会立即执行新的进程,接着调用exec()这组函数就可以创建新的地址空间,并把新的地址空间载入其中。
fork()函数的实现
- fork()函数采用了写时拷贝(copy-on-write)的技术,刚创建子进程的时候父进程和子进程拥有相同的地址空间(这些区域被设定为只读),只有在子进程或者父进程要写入数据的时候,父进程相关的地址空间才会被拷贝,父子进程指向相同的物理内存。
- fork()之后是父进程先执行还是子进程先执行时不确定的,通过fork()函数创建的父子进程是共享一个文件表项的,linux通过clone()系统调用实现fork()函数。
- 另外一个创建子进程的方法是调用vfork()函数,vfork()和fork()函数的区别是vfork()函数创建的子进程不拷贝父进程的页表,子进程在父进程的地址空间先运行,直到子进程退出后,父进程才可以继续运行。但是使用vfork()函数的时候,如果调用这两个函数之前子进程依赖于父进程的进一步动作,就会导致死锁。
fork()调用失败的原因:
- 系统中有太多进程
- 实际用户的进程数超过了限制
fork()与vfork()的区别:
fork():子进程拷贝父进程的数据段,代码段
vfork():子进程和父进程共享数据段fork ()父子进程的执行次序不确定
vfork ()保证子进程先运行,在调用exec 或exit 之前与父进程数据是共享的,在它调用exec或exit 之后父进程才可能被调度运行。vfork ()保证子进程先运行,在她调用exec 或exit 之后父进程才可能被调度运行。如果在用这两个函数之前子进程依赖于父进程的进一步动作,则会导致死锁。
代码实现:
先看一个子进程拷贝父进程的代码段的例子:
运行结果:
fork的返回值有两个,子进程返回0,父进程返回子进程的进程号,进程号都是大于0的正整数,所以父进程的返回值一定大于0,在pid = fork();语句前只有父进程在运行,在pid=fork();之后父进程和新创建的子进程都在运行。而我们知道fork()函数子进程是拷贝父进程的代码段的,所以子进程中同样执行fork();以下的代码,这段代码会被父进程和子进程各执行一次。
再看一个拷贝数据段的例子:
运行结果:
那么为什么count不是2呢?
因为fork()函数子进程拷贝父进程的数据段代码段,所以count++;将被父子进程各自执行一次,子进程执行时是使自己的数据段里的count+1;父进程执行时是使自己的数据段里面的count+1,它们互相独立,互相不影响。
那么再看一个vfork()的例子:
vfork()是共享数据段的,所以count应该是2(前提是子进程先执行,执行完后要调用exec或者exit之后父进程才能被调度运行)
运行结果:
注意:在子进程调度完成时。后面必须加上exit();或者exec();父进程才能执行,负责会产生死锁。子程序调用exit();之前与父进程数据是共享的,所以子程序退出后把父进程的数据段count改成了1,子进程退出后父进程继续执行,最终将count变为2。
2.进程终止
进程退出场景:
- 代码运行完毕,结果正确。
- 代码运行完毕,结果不正确。
- 代码异常终止。
进程的终止方式:
正常退出:
- 在main函数中执行return。
- 调用exit函数,并不处理文件描述符,多进程。
- 调用_exit或者 _Exit。
异常退出:
1. 调用abort,产生SIGABRT信号。
2. 由信号终止ctrl+c/SIGINT
exit和_exit的区别
1.清空缓冲区的操作。
int main(void)
{
printf("hello world");
//exit(0);//不需要程序员手工刷新缓冲区
fflush(stdout);//不加这句话-不会刷新缓冲区--不会打印
_exit(0);
}
2.exit会调用终止处理程序。
关于终止处理程序:
- atexit可以注册终止处理程序,ANSI C规定最多可以注册32个终止处理程序。
- 终止处理程序的调用与注册次序相反。
- 函数原型:
`int atexit(void(*function)(void));
总结exit与_exit的区别:
1)_exit是一个系统调用,exit是一个c库函数。
2)exit会执行刷新I/O缓存。
3)exit会执行调用终止处理程序。
return和exit的区别:
- exit用于结束正在运行的整个程序,它将参数返回给操作系统,把控制权交给操作系统;而return是退出当前函数,返回函数值,把控制权交给调用函数。
- exit是系统调用级别,它表示一个进程的结束;而return是语言级别的,它表示调用堆栈的返回。
- 在main函数结束时,会隐式地调用exit函数,所以一般程序执行到main()结尾时,则结束主进程。exit将删除进程使用的内存空间,同时把错误信息返回给父进程,exit函数运行时会先执行由atexit()函数登记的函数,然后会做一些自身的清理工作,同时刷新所以输出流,关闭所有打开的流并且关闭通过标准I/O函数tempfile()创建的临时文件。
- void exit(int status);一般status为0,表示正常退出,非0表示异常退出。
3.进程等待
1).进程为什么等待?
首先,子进程退出,父进程如果不管不顾,就可能造成“僵尸进程”的问题,进而造成内存泄露。其次,我们需要知道父进程派给子进程的任务完成的如何。最后,父进程通过进程等待的方式,回收子进程资源,获得子进程退出信息。
2).进程怎么等待?
进行的等待就需要看到linux中两个接口
1.pid_t wait(int* status);
2.pid_t waitpid(pid_t pid,int* status,int options);
wait函数原型:
#include<sys/wait.h>
pid_t wait(int* status);
说明:wait函数会暂时停止目前进程的执行,直到有信号来到或者子进程结束。如果在调用wait时,子进程已经结束,则wait会立即返回子进程的结束状态值。如果不在意子进的返回状态,可将参数status设置为NULL。若成功返回子进程识别码(pid),若有错误发生则返回-1。
waitpid函数原型:
pid_t waitpid(pid_t pid,int* status,int options);
说明:
返回值:若成功,返回进程id,若出错,返回-1.waitpid会暂停目前的进程执行,直到有信号或者子进程结束。如果不在意结束时状态值,则status可以设置为NULL。参数pid为欲等待的子进程识别码。
pid<-1等待进程组识别码为pid绝对值的任何子进程;pid=-1等待任何子进程,此时就相当于wait();pid=0等待进程组识别码与目前进程相同的任何子进程;pid>0等待任何子进
程识别码为pid的子进程。
参数options可以设置为0或者与下面的组合:
(1)WNOHANG若没有任何已经结束的子进程则马上返回,不等待。
(2)WUNTRACED如果子进程进入暂停执行情况马上返回,但结束的状态不予理会。
代码实现:
使用wait进行等待(阻塞式):
结果:
使用waitpid进行等待(非阻塞式):
运行结果:
关于WIFEXITED和WEXITSTATUS這两个宏:
(注意:這两个宏对于wait和waitpid都是可用的)
可以用這两个宏来检查wait和waitpid所返回的子进程的退出状态:
(1)如果WIFEXITED(status) 非0,则说明进程是正常退出的,不是异常退出的,那么我们可以使用WEXITSTATUS(status)来查看或者提取进程退出的返回值,例如:子进程要是调用exit(3),那么WEXITSTATUS(status)返回值就是3。
注意:如果如果WIFEXITED(status)为0,wait或者waitpid等待的那个子进程是异常退出,收到了信号而终止了运行。也只能说明這一点,這个时候的WEXITSTATUS(status),是毫无意义的,它只能返回正常退出的子进程的退出码,這一点要清楚。
3.wait和waitpid的区别:
(1).在任何一个子进程终止前,wait使其调用者阻塞,而waitpid有一个选项,可以使调用者不阻塞。
(2).waitpid并不等待在其调用之后的第一个终止的子进程。它有若干个选项,可以控制它所等待的进程。如果一个子进程已经终止,并且是僵尸进程,wait函数立即返回并取得该子进程的状态,否则wait使其调用者阻塞直到一个子进程终止。如果调用者阻塞并有多个子进程时,则在一个子进程终止时,wait立即返回,因为wait返回终止子进程的ID.
二、popen/system/fork/exec函数的区别
1.fork()
一个程序一但调用fork函数,系统就为一个新的进程准备了以下三个阶段。
(1)首先,系统让新的进程与旧的进程使用同一个代码段,因为它们的程序还是相同的,对于数据段和堆栈段,系统则复制一份给新进程,这样,父进程的所以数据都可以留给子进程,但是,子进程一旦开始运行,虽然它继承了父进程的一切数据,但实际上数据却已经分开,相互之间不再有影响了,也就是说,它们之间不再共享任何数据了。已经是两个进程了,对于父进程,fork函数返回了子程序的进程号,而对于子程序,fork函数则返回0.
(2)一般的,cpu都是以“页”为单位分配空间的,像INTEL的cpu,其一页在通常情况下是4k字节大小,而无论是数据段还是堆栈段都是由许多“页”构成的,fork函数复制这两个段,只是“逻辑”上的,并非是“物理”上的。
(3)实际执行fork时,物理空间上两个进程的数据段和堆栈段都还是共享的,当有一个进程写了某个数据时,这时两个进程之间的数据才有了区别,系统就将有区别的“页”从物理上也分开。系统在空间上的开销可以达到最小。
2.exec系列函数
一个进程一旦调用exec类函数,它本身就“死亡”了,系统把代码段替换成新的程序代码,废弃原有的数据段和堆栈段,并为新程序分配新的数据段与堆栈段,唯一留下的就是进程号。
3.popen函数
它会通过command参数重新启动shell命令,并建立进程间的管道通信。
4.system函数
它会重新启动shell命令,当执行完毕后,程序会继续system下一行代码执行。
三、调研vfork创建的子进程, 直接return为什么会出现崩溃?
从前面我们知道,结束子进程的调用时exit()而不是return,如果你在vfork中return了,那么这就意味着main()函数return了,注意因为函数栈父子进程共享,所以整个程序的栈就出现问题了。如果你在子进程中return,那么基本就是下面的过程:
(1)首先子进程的main()函数return了。
(2)而mian()函数return后,通常会调用exit()或相似函数(如:exitgroup())。
(3)这时,父进程收到子进程exit(),开始从vfork返回,但是父进程的栈已经被子进程销毁了,因此程序就会直接崩溃。