关闭

并发编程01-基于进程的并发编程

标签: fork并发编程UNIX
756人阅读 评论(0) 收藏 举报
分类:

阅读完本文你可以学到:

(1)基于进程的并发编程的核心思想或出发点,理解这种思想是极其必要的(如何正确使用 fork 进行并发编程只是知识技能,或许我们从不会使用到这种并发模型,但它背后涵盖的思想却会贯穿实际的编程过程)。文中通过列举项目中碰到的一个问题,将进一步证实其重要性。

(2)父进程利用 fork 系统调用进行并发编程需要做哪些工作,其中包括清除僵尸子进程的必要性,正确处理僵尸子进程的方法,及时关闭套接口以防止内存泄露等等。

(3)介绍了 fork 被调用时到底会发生什么,这很自然地会涉及到底层的与操作系统相关的内容。理解了它,我们将更容易地理解基于进程的并发编程是如何工作的。


一、基于进程的并发编程的核心思想

(1)核心思想:若所从事的工作可以很容易地划分成若干个相关但未相互作用的进程,则创建新的进程特别有效。

(2)项目中所遇到的问题

问题描述:系统初始化过程特别慢。

产生原因:在系统初始化时,某个函数中存在“界面逻辑控制操作和访问数据库操作处于同一工作流”的问题,而且该函数在系统初始化时会被频繁调用。

解决方案(这里只提供思路):将界面逻辑控制操作和访问数据库操作分成两个工作流(我们是通过创建线程做到的)。这两个操作的相关性体现在:它们使用同一数据,界面逻辑控制操作利用该数据进行界面的更新,数据库利用该数据将其存储到数据库中。很自然地,如果这两个操作各持有同一数据的一份拷贝,那么这两个操作之间是不存在相互作用的,它们可以根据各自拥有的数据拷贝进行各自应该做的工作。

二、利用 fork 进行并发编程

1.利用 fork 进行并发编程所需要做的工作,包括三方面:避免内存泄露,防止出现 TCP “伪连接”,处理僵尸子进程。

对于处理僵尸进程方面又具体分为以下三个工作:

(1)当派生子进程时,必须捕获信号 SIGCHLD。

(2)当捕获信号时,必须处理被中断的系统调用。

(3)SIGCHLD 的信号处理程序必须正确编写,应使用函数 waitpid 以免留下僵尸进程。

2. 调用 fork 系统调用会发生什么?

简单地说,父进程利用 fork 系统调用创建一个子进程,这个子进程由父进程的地址空间的一个拷贝构成。

我们必须理解这句话中涵盖的两个要点:

要点1:在进程创建之后,父进程和子进程拥有各自独立的地址空间。这意味着:如果其中某个进程在其地址空间修改了一个字,则这一改变对另一进程是不可见的。

要点2:对于父、子进程间共享状态信息,进程有一个非常清晰的模型:共享文件表,但是不共享用户地址空间。可写的地址空间是不共享的( 在UNIX 实现中,可以在父子进程间共享代码段,因为它是不可修改的。即在 UNIX 中,父子进程可以运行同一代码段,但该代码段在存储器中只有一个副本)。但是,一个新建的进程有可能共享像打开文件之类的资源。

对“共享文件表”的理解:

我们必须了解每个文件或套接口都有一个访问计数,该访问计数在文件表项中维护(APUE 第 58~59 页),它表示当前指向该文件或套接口的打开的描述字个数。描述字只在访问计数值达到 0 时才真正关闭。当 fork 返回后,监听描述字和已连接套接字在父进程与子进程间共享,所以,与两个套接口相关联的文件表项访问计数值均为 2。更深刻地说,当 fork 返回后,子进程拥有父进程所有打开的文件描述符的副本,对于相同的文件描述符来说,其在父进程和子进程占用的物理存储空间是不同的,但该文件描述符所指向的打开的文件或设备却是同一个,即该打开的文件或设备在存储空间中仅存在一个副本。直观地,见图 2-1 fork 之后父、子进程之间对打开文件的共享:


图 2-1 fork 之后父、子进程之间对打开文件的共享


另外,需要注意的是:

现在很多的实现并不做一个父进程数据段和堆的完全拷贝,因为在 fork 之后经常跟随者 exec。作为替代,使用了在写时复制(Copy-On-Write,COW)的技术。这些区域由父、子进程共享,而且内核将它们的存取许可权改变为只读的。如果有进程试图修改这些区域,则内核为有关部分,典型的是虚存系统中“页”,做一个拷贝。

3. fork 的返回值。

fork 调用一次却返回两次。在调用进程(称为父进程),它返回一次,返回值是新派生进程(称为子进程)的进程 ID 号,在子进程它还返回一次,返回值为 0。因此,可通过返回值来判断当前进程是子进程还是父进程。

fork 在子进程返回 0 而不是父进程 ID,原因是:子进程只有一个父进程,它总可以调用 getppid 来得到(进程 ID 0 总是由调度进程使用,并且进程 ID 标识符总是唯一的,所以一个子进程的进程 ID 不可能为 0);而父进程有许多子进程,它没有办法来得到各子进程的 ID。如果父进程想跟踪所有子进程的 ID,它必须记住 fork 的返回值。

4. 父进程必须显式关闭连接套接口。原因?

我们必须意识到,如果父进程不对每个由 accept 返回的已连接套接口调用 close,并发服务器将会发生什么。首先,父进程最终将耗尽可用描述字,因为任何进程在某时刻打开的描述字数总是有限的,由此引起的内存泄露将最终消耗可用的内存空间,并摧毁系统。但更重要的是,没有一个客户连接被终止。当子进程关闭已连接套接口时,它的访问计数值由 2 减为 1 且保持为 1,因为父进程从未关闭已连接套接口,这将妨碍 TCP 连接终止序列的执行,从而连接永远保持开放。也就是说,直到父子进程的已连接套接口都关闭了(即该已连接套接口的访问计数值为 0),服务端与该客户端的连接才会终止。

5. 子进程中,显式地关闭监听套接口和已连接套接口并不是必须的。原因?

子进程调用 exit,而进程结束的部分处理就是关闭所有由内核打开的描述字(实际上是将描述字的访问计数值减 1,当其访问计数值减为 0 时就销毁它)。是否需显示地调用 close,由个人的编程风格而定。

6. 处理僵尸进程

(1)僵尸进程存在的原因

设置僵尸状态的目的就是维护子进程的信息,以便父进程在稍后的某个时候取回。此信息包括子进程的 ID、终止状态以及子进程的资源利用信息(CPU 时间、内存等等)、如果一个进程终止,且该进程有子进程处于僵尸状态,则所有僵尸子进程的父进程 ID 均置为 1(init 进程),从而子进程仍旧有一个父进程来维护他们的状态和运行统计数据。init 进程将作为这些子进程的继父并负责清除它们(也就是说,init 进程将 wait 它们,从而去除僵尸进程)。有些 Unix 系统给僵尸进程输出的 COMMAND 列为 <defunct>(ps 命令输出)。

(2)处理僵尸进程的必要性

从进程控制的角度看:每个进程都有一个非负整型的唯一进程 ID,并且进程 ID 标识符总是唯一的。从上文我们知道,僵尸状态的进程会维护一系列信息,其中一项就是进程 ID,也就是说,该僵尸进程仍占据着被创建时系统分配给它的进程 ID。通常我们的服务器处于长时间运行,当负责与某个客户端通信的子进程运行结束后,在默认情况下,该子进程就变成了僵尸进程。总有一天(或许那一天会很快到来,这主要取决于客户请求量)服务器端所在的主机将“无可分配的进程 ID”。届时,任何创建进程的操作都将失败,此时的服务端再也不能处理与新客户间的通信了。

从内存管理的角度看:僵尸进程占用内核空间,最终导致我们无法正常工作。

对“僵尸进程占用内核空间”的理解:

本例中的子进程是调用 exit 系统调用结束运行的。但不管进程是如何终止的,最后都会执行内核中的同一段代码。这段代码为相应进程关闭所有打开描述符,释放它所使用的存储器空间(或物理空间)等等。那么处于僵尸转态的进程所包含的一系列信息是由谁维护的?要知道,在许多的操作系统中,一个进程的所有信息(除了它地址空间中的内容)均存放在操作系统的一张表中,该表称为进程表,它实际上是一个结构数组(或链表),系统中的每个进程都要占用其中的一项。进程表的每一项为内核所需信息提供了存储空间。在 UNIX 中,处于僵尸状态的进程的信息就存储在进程表的表项中。这就是所谓的“僵尸进程占用内核空间”了。

(3)正确处理僵尸进程的一种方法

方法原理:无论何时我们创建子进程都必须等待,以免变成僵尸进程。为此,我们建立了一个信号处理程序来捕获信号 SIGCHLD,在处理程序中我们调用 waitpid。

注意事项:在处理 SIGCHLD 信号的函数中调用 waitpid 而不是 wait。(具体原因可参考 《UNP 卷一》5.10 wati 和 waitpid 函数)

操作步骤(具体地可参考 《UNP 卷一》5.8~5.10 ):

在调用 listen 之后,增加函数调用:Signal(SIGCHLD, sig_child); 建立 SIGCHLD 信号处理程序(这必须在创建第一个子进程之前完成,且只做一次)。

定义信号处理程序即函数 sig_chld。

处理一个被中断的 accept。


友情链接:Fork(System Call)

References

1. UNP 卷一

2. APUE

3. 深入理解计算机系统

4. 操作系统概念

5. 操作系统的设计与实现


0
0

查看评论
* 以上用户言论只代表其个人观点,不代表CSDN网站的观点或立场
    个人资料
    • 访问:22205次
    • 积分:394
    • 等级:
    • 排名:千里之外
    • 原创:18篇
    • 转载:0篇
    • 译文:0篇
    • 评论:0条