Linux系统编程之进程

进程的创建

    系统调用fork()可以从一个父进程中创建一个子进程,子进程的资源同父进程几乎一样:子进程获取父进程的栈、数据段、堆和代码段(执行文本段)的拷贝。执行fork()之后,每个进程都可以修改自己栈数据、堆中的变量且并不会影响另一个进程。执行fork()时,子进程会获得父进程所有的文件描述符副本,类似于dup(),意味着父进程和子进程对应的文件描述符指向同一个打开文件句柄(共享当前文件的偏移量、文件状态标志等)

    程序代码通过fork()的返回值来区分父子进程。子进程中返回0,父进程中返回子进程的id。但是在调用了fork()之后,cpu无法确定先执行哪一个进程。如果想要确保子进程先执行,可以使用sleep()或者父进程调用wait()系统调用暂停运行等待子进程退出。除此以外,还可以采用某种同步技术(信号量、文件锁、管道、信号等)

    虽然从概念上来说可认为fork()是对父进程程序段、数据段、堆段、栈段创建拷贝,但是由于fork()之后通常会伴随着exec(),这会用新程序替换进程的代码段,并重新初始化其数据段、堆段、栈段,由此会造成大量的浪费,两个方法来解决此类问题

  1. 内核将每一进程的代码段标记为只读,从而使进程无法修改自身代码,这样父子进程可共享同一段代码。在系统调用fork()之后,子进程所构建的一系列进程级页表项均指向父进程相同的物理内存页帧。
  2. 对付进程数据段、堆段、栈段中的各页,内核采用写时复制技术来处理。(只有在修改页面的时候才对相应的页表项做调整)

Linux系统编程之进程_第1张图片

    fork()、vfork()、clone()之间的区别和联系:

  1. fork()就是把父进程的几乎所有资源复制或拷贝到子进程中。
  2. vfork()也可以为调用的进程创建一个子进程,区别在于:①无需为子进程复制虚拟内存或页表,相反,子进程与父进程共享父进程的内存;②在子进程调用_exit()或exec()之前,父进程暂停(即系统保证子进程优先于父进程使用CPU)。由于较乱不推荐使用。vfork()较fork()较快。
  3. clone()主要是在进程创建期间对步骤控制更加精确,主要用于线程库的实现。与fork()不同的是,其生成的子进程继续执行时不以调用处为起点,而是调用以参数func的子函数为起点。子进程也可能共享父进程的内存,所以不能使用父进程的栈,调用者需分配一块内存空间供子进程栈使用。

 进程的终止

    通常进程有两种终止方式。一为异常终止(对某个信号的接收而引发,该信号默认动作为终止当前进程,也可以产生核心转储),另一种方式调用_exit()系统调用正常终止。_exit()的status参数定义进程的终止状态,父进程可以调用wait()获取该状态。一般而言,子进程调用_exit(),而父进程调用exit(),库函数exit()相比于系统调用多了一些动作,具体如下:

  1. 调用退出处理程序(通过atexit()和on_exit()注册的函数),其执行顺序与注册顺序相反。
  2. 刷新stdio流缓冲区
  3. 使用由status提供的值执行_exit()系统调用

   进程终止的细节,无论是否正常终止,都会发生以下动作:

  1. 关闭所有打开的文件描述符、目录流、信息目录描述符、转换描述符
  2. 释放该进程所持有的任何文件锁
  3. 分离任何已连接的共享内存段,且对应于各段的shm_nattch计数器值减一
  4. 关闭该进程任何POSIX信号量、POSIX消息队列
  5. 如果该进程组成为孤儿,则进程组中所有进程收到SIGHUP信号,随之SIGCONT信号
  6. 解除内存锁
  7. 取消该进程调用mmap()所创建的任何内存映射

    退出处理程序是一个由程序设计者提供的函数,可于进程生命周期任意时间节点注册,并在该进程调用exit()正常终止时自动执行,若程序调用_exit()或因信号异常终止,则不会调用。可采用atexit()或on_exit()注册退出处理函数。

监控子进程

    系统调用wait()等待调用进程的任一子进程终止,同时在参数status所指向的缓冲区中返回改子进程的终止状态,终止子进程的id作为返回值。waitpid()相比于wait()的优点:

  1. 如果父进程创建多个子进程,wait()无法等待某个特定的子进程的完成,只能按顺序等待下一个子进程终止
  2. 如果没有子进程退出,wait()总是保持阻塞(即一直等待)
  3. wait()只能发现已经终止的进程,对于进程停止(SIGSTOP)或进程恢复(SIGCONT)无能为力

    系统调用waitpid()和waittid()最显著的区别在于,对于应该等待的子进程事件,waittid()可以更加精准控制。而wait3()和wait()也和waitpid()差不多,只是这两个在参数所指向的结构中返回终止子进程的资源使用情况。

孤儿进程与僵尸进程

    某一子进程的父进程终止后,子进程就变成孤儿进程,其父进程为init进程。

    在父进程执行wait()之前,子进程就终止了,内核将子进程转为僵尸进程(即释放子进程大部分资源,唯一保留内核进程表中的一条记录,其中包含子进程id,终止状态,资源使用数据等),并且僵尸进程无法通过信号杀死,当父进程执行wait()后,由于不再需要子进程剩余的最后信息,故内核删除僵尸进程,若父进程未执行wait()并退出,那么init进程将接管子进程并自动条用wait()。

    当在设计生命周期长的父进程时(如网络服务器,长时间不退出),要注意执行wait()方法,以确保系统能杀死子进程,避免成为大量的僵尸进程。

SIGCHLD信号

    子进程的终止属于异步过程,父进程无法预知子进程何时终止。父进程可以用wait()或类似的系统调用来防止僵尸进程,以及如下两种方法:

  1. 父进程调用不带WNOHANG标志的wait(),或waitpid()方法,此时如果尚无终止的子进程,方法将会阻塞。
  2. 父进程周期性地调用带有WNOHANG标志地waitpid(),执行针对已经种植的子进程非阻塞式(轮询)检查

    其实这两种方法都不是最优秀的方法,一方面不希望父进程能以阻塞式的方式等待子进程终止,另一方面反复调用waitpid()以轮询的方式会造成CPU资源浪费。所以可以采用SIGCHLD信号处理程序来解决这两个问题。

    子进程无论何时终止都会向父进程发送SIGCHLD信号,对这个信号的默认处理方式是忽略,但是我们也可以用信号处理程序捕获其,用来处理僵尸进程。为了防止程序在进行信号处理程序时,同时又来多个SIGCHLD信号,但是由于无排队,只能捕获一个信号的情况出现,我们可以在SIGCHLD处理程序内部循环以WNOHANG标识调用waitpid(),直到再无其他终止的子进程需要处理为止。

 

 

参考 《APUE》、《TLPI》

你可能感兴趣的:(Linux)