4、vfork
vfork函数源于2.9BSD。有些人认为这个函数是多余的,但是本文讨论的系统都支持这个函数。实际上,BSD把这个函数从4.4BSD中删除了,但是所有从4.4BSD继承过来的开源BSD,又在它们的release中把这个函数添加进来了。vfork函数在Single UNIX Specification的version3中被标记成将被废弃的接口。
函数vfork和fork一样,返回值也一样,但是这两个函数有所不同。
vfork函数一般用于创建一个子进程,并且这个子进程的目的是进行exec(关于exec参见后面).
vfork和fork一样,创建新的进程,并且它不会复制父进程的地址空间的内容到子进程中,因为子进程是将要调用exec的而不是引用父进程地址空间的数据。子进程会一直在父进程的地址空间运行,直到它调用了exit或者exec.这样的优化是为了提高一些用虚拟内存页实现的unix系统的效率。(通过前面对fork的解释我们可以发现,实际上fork现在已经使用了copy-on-write技术来提高效率,但是没有拷贝始终是要比有一些拷贝的快一些)
fork和vfork另一个不同的地方是vfork保证了子进程先运行,直至子进程调用了exec或者exit.当子进程调用了exec或者exit的时候,父进程才重开始执行(如果子进程在调用这两个函数之前需要父进程的一些操作的话,这里可能会导致死锁)。
总之,就两点不同:
- vfork的子进程不拷贝父进程空间数据而是直接运行在父进程空间内(直到调用了exec或exit)。fork的子进程拷贝一份父进程的数据。
- vfork之后,子进程会先继续运行,直到子进程调用exec或者exit父进程才开始运行。fork的子进程不能确定谁先继续运行。
下面是使用vfork的一个例子:
int glob = 6; /* external variable in initialized data */
int main(void)
{
int var; /* automatic variable on the stack */
pid_t pid;
var = 88;
printf("before vfork\n"); /* we don't flush stdio */
if ((pid = vfork()) < 0) {
err_sys("vfork error");
} else if (pid == 0) { /* child */
glob++; /* modify parent's variables */
var++;
_exit(0); /* child terminates */
}
/*
* Parent continues here.
*/
printf("pid = %d, glob = %d, var = %d\n", getpid(), glob, var);
exit(0);
}
运行这个程序之后,结果如下:
$ ./a.out
before vfork
pid = 29039, glob = 7, var = 89
从例子可以知道,子进程修改了变量,也反应到父进程中了,因为两者共享进程空间而不是拷贝关系;另外子进程会先于父进程运行。需要注意的地方是:
我们结束子进程调用的是_exit而不是exit。因为,_exit不会做任何刷新标准输入/输出库的刷新操作;如果我们调用了exit那么,输出结果是不确定的,这依赖标准I/O库的实现,我们可能会看到和使用_exit没有什么区别,或者看到父进程printf的内容没有了。
如果子进程调用了exit,这样会对标准I/O流进行flush操作。如果这是标准I/O库的唯一动作,那么我们不会看到和_exit有什么不同;但是如果也关闭了标准I/O流,那么内存中表示标准输出的FILE对象会被清空,由于子进程和父进程共享空间,所以当父进程再次继续运行并执行printf的时候,将会不输出任何东西,同时printf会返回-1。注意,这时候父进程的STDOUT_FILENO还是合法的,就像子进程获得了父进程的文件描述符的副本。
大多数现代对exit的实现都不会自找麻烦地去关闭streams.因为如果进程将要退出的时候,内核会关闭所有在进程中打开的文件描述符号。如果在库中关闭他们,会增加额外的开销,没有一点好处。
译者注
原文参考
5、进程exit
前面已经说过,
(1)进程正常终止有五种方式:
- 在main函数中执行return,这实际和调用exit是一样的。
- 调用exit函数。这个函数由ISO C来定义,它会调用所有注册的“退出回调函数”,以及关闭所有的标准I/O流。因为ISO C没有处理文件描述符号,多进程,和作业控制,所以这个函数对于unix系统来说是不完整的。
- 调用_exit或者_Exit函数。ISO C定义了_Exit来为进程提供一种不运行“退出回调函数”以及信号处理的结束的方式,标准I/O流是否被flushed这取决于实现。在UNIX系统上,_Exit和_exit是同义的,它不会刷新标准I/O流。_exit函数被exit调用,并且会处理一些和UNIX系统相关的细节;_exit在POSIX.1中被定义。在大多数UNIX系统的实现中,exit(3)是一个标准C库函数,然而_exit(2)是一个系统调用。
- 在进程的最后一个线程中执行return.但是,线程return的值不会作为进程的return值。当最后一个线程return的时候,进程会以0来表示它的termination status.
- 在进程的最后一个线程中调用pthread_exit函数。和前面的情况类似,进程的exit status始终是0,而不考虑pthread_exit的参数值。
(2)还有三种非正常终止的形式:
- 调用abort.实际上这是下一个形式的特殊情况,调用abork会发送SIGABRT信号。
- 当进程接收到特定的信号的时候。信号可以被进程自己来产生(例如调用abort函数),可以由其他进程产生,或者由内核产生(例如进程引用了一个非法的内存地址空间,或者尝试除以0)。
- 最后一个线程响应取消请求。默认来说,取消会延迟发生:一个线程请求其他线程被取消,然后过一会目标线程才会终止。
不管进程是怎么终止的,在内核中执行的代码都是一样的。内核会关闭被终止进程的所有打开的文件描述符号,释放它使用的内存以及其他类似。
对于前面,我们一般希望系统会让终止进程通知父进程它是如何终止的。对于三种exit(exit,_Exit,_exit),这是通过传入exit的参数来表示的。对于非正常终止的方式,是内核而不是进程产生一个termination status来表示非正常终止的原因。无论怎样,被终止进程的父进程都是通过wait或者waitpid函数来获得终止状态的。
这里我们需要区分一下两种终止状态:如果是通过三种exit终止的(正常终止),那么终止状态叫做exit status,是exit函数的参数;如果非正常终止,叫做termination status.在调用_exit的时候,exit status会被内核转换成为termination status.如果进程正常结束了,那么父进程会获得子进程的exit status.
wait或者waitpid会把它们等待的在子进程退出时候得到的status返回并存储起来,然后用下面的宏来判断其含义:
-
WIFEXITED(status)
如果是子进程正常终止时产生的status,那么该宏返回True.这时候我们可以通过WEXITSTATUS(status)来获取子进程退出时,调用的三种exit函数参数的低八位。
-
WIFSIGNALED (status)
如果是子进程是由于接收到它没有捕获的信号而非正常终止的时候产生的status,该宏返回True.我们可以使用WTERMSIG (status)来获得导致子进程终止的signal number.另外一些系统定义了WCOREDUMP (status),如果终止进程会产生core file,该宏返回true.
-
WIFSTOPPED (status)
如果是发送信号导致当前子进程被stop的时候返回的status(此时子进程是终止还是stop状态???),那么该宏返回true。这是后我们可以使用WSTOPSIG (status)来获得导致子进程stop的信号。
-
WIFCONTINUED (status)
如果子进程在从job stop之后被continued导致返回了status,那么会返回True(对POSIX.1的XSI扩展,只对waitpid而言).
如果一个父进程在子进程结束之前结束了会怎样?实际上,如果发生这种情况,那么init进程会变成终止进程的所有子进程的父进程。我们已经说了,init是进程的父进程。一般来说当一个进程终止的时候,内核会遍历所有活动的进程,查看被终止的进程是否是其他还存在的某进程的父进程。如果存在被终止进程的子进程,那么存在的子进程的父进程id(ppid)被设置为1(也就是init进程的进程号).这样我们可以保证所有的进程都有父进程。
还有一个我们需要注意的地方,就是如果一个子进程先于父进程终止。如果子进程完全地消失了,如果当父进程最后想要检查子进程是否terminated的时候,父进程不能获得它的termination status.内核会保存每一个终止进程的少量信息,这样当终止的进程的父进程调用wait或者waitpid的时候会用到它们。至少,这些信息里要包含PID以及进程的termination status,以及进程占用的CPU time.内核能够丢弃进程使用过的所有内存以及关闭它打开的文件。在UNIX 系统的属于中,进程如果terminated了,但是它的父进程没有wait它,那么这个进程就会被叫做zombie(僵尸进程)。使用ps(1)命令可以打印进程的状态,僵尸进程的状态是Z.如果我们些一个很长的程序,fork了许多的子进程,那么除非我们调用wait等它们来获取他们的termination status,否则它们会变成zombies.有些系统提供可以阻止创建僵尸进程的方法。
最后我们还需要考虑的是:如果一个进程是init的子进程,那么如果它terminate的话会怎样?它会成为一个zombie吗?答案是“不”,因为init本身必然会调用wait函数来获得termination status,它就是那么设计的。通过这样,init可以防止系统被过多的僵尸进程占用资源。当我们说一个init的子进程的时候,我们的意思是这个进程要么就是直接从init那里继承的,要么就是它的父亲在它之前终止了,它被设置成从init继承。
网友评论