进程管理之fork函数

fork函数的定义

#include <unistd.h>
#include <sys/types.h>
pid_t fork(void);

fork函数在父进程中返回子进程的pid,在子进程中返回0。注意在子进程中返回的0,并不是子进程的pid,子进程的pid在父进程的返回值中保存。而子进程的返回值是为了标识它是子进程,用来区分父子进程的。那么为什么这样设计父子进程的返回值呢?我的理解是这样的:第一,对于父进程来说,它可能同时有多个子进程,并且没有一个函数可以获得所有子进程的pid,所以它需要知道每个子进程的pid,这样便于管理各个子进程;第二,对于子进程来说,它父进程只能有一个,所以它可以不必实现和父进程类似的机制,获取父进程的pid,它可以通过getpid函数获取父进程的pid。而在内核中,进程pid为0,总是在内核交换进程时使用,所以进程的真实pid不可能为0。

资源相关问题

读时共享,写时复制

fork函数并不是在被调用时,为子进程copy一份父进程的副本,而是将父进程数据段、堆区和栈区的权限改变为只读。当父子进程中任何一个进程要修改该区域的值时,内核只会为修改区域的那块内存制作一个副本,通常是虚拟内存中的一页。这样的机制无需copy所有的父进程资源(这些资源在子进程中不一定有用),提高了程序的效率。但要注意,父子进程共享代码段。

复制的资源

  • 能够复制fork函数会复制数据段、堆区和栈区的资源
  • 能够复制参数表和环境表
  • 能够复制各个ID,但不包括PID
  • 不能够继承文件锁
1)数据段、堆区、栈区、参数表和环境表和上文说的一样,遵循读时共享,写时复制的机制;
2)环境表中只能改变其本身进程和其子进程的环境变量,无法改变其父进程的环境变量;
3)像是实际用户ID、实际组ID、有效用户ID、有效组ID、进程组ID和附属ID,这些都是进程的属性,所以存在于PCB中,每个进程都有自己的PCB节点,其中部分数据是继承父进程的。当然pid不会。
4)对于文件锁,子进程是不会被继承的。子进程虽然复制了文件描述符表,但都指向同一个文件表,当然了,文件表也就指向了同一个i节点。在i节点中保存着和文件锁相关的链表头节点(struct lockf)的地址。i节点资源不属于进程资源,而是文件资源,所以不会继承文件锁。

调用使用

常见使用方法

  • if...else结构:最常见的使用方法,很简单。
  • 和管道结合使用:主要用来父子进程间通信,使用时要注意,子进程会复制父进程的管道文件描述符,因此,都可以对对管道进行读写操作。需要注意的是,在使用管道这个临界资源时,不要忘记在软件逻辑上避免进程饥饿现象的产生。
  • 子进程的管理:别忘了了父进程能返回子进程的pid。当有多个子进程时,如果会用到各个子进程的pid的话,不要只是定义一个 pid_t pid 的变量。这样的话,只能管理一个子进程哟!

避免僵尸进程的方法

  • 使用wait和waitpid函数:阻塞等待子进程结束,回收进程表资源
  • 安插信号:利用signal函数安插SIGCHLD信号。因为在子进程结束后,父进程会收到该信号。再自己写个回调函数,在函数中调用wait或waitpid函数,回收进程表资源;如果父进程对子进程结束不感兴趣,则可以利用“signal(SIGCHLD,SIG_IGN)”,将回收子进程资源的工作交给内核来做;
  • 利用孤儿进程的机制:在创建的子进程中在调用fork函数,创建一个孙子进程,然后子进程终止,那么孙子进程就会被init进程收养。再当孙子进程结束时,回收进程资源的工作就交由了init进程去做。当然,有人会问,那么子进程结束后它的进程资源谁去回收?我说,那当然是父进程回收。这个时候能用父进程去回收子进程资源,是因为这是父子进程已经不在时异步的关系了。换句直白点的话说就是,父进程知道子进程在不久的将来一定会结束。回头想想,安插信号的方法的本质,不就是通过信号捕捉,确定了子进程会在不久的将来会结束吗?即在调用wait和外套pid时,不会阻塞等待很长时间就能返回,不会影响父进程自身的工作。
注意:安插信号和利用孤儿进程的机制来避免僵尸进程的方法,不仅仅能避免僵尸进程,还能不影响父进程本身的工作任务,这一点是非常有用的。

相关推荐