一、概念介绍

1、任务:

任务是一个逻辑概念,指由一个软件完成的任务,或者是一系列共同达到某一目的的操作。

2、进程:

进程是指一个具有独立功能的程序在某个数据集上的一次动态执行过程,它是系统进行资源分配和调度的最小单元。从内核的观点看,进程的目的就是担当分配系统资源(CPU时间、内存等)的基本单位

3、线程:

  是进程的一个执行流,是CPU调度和分派的基本单位,它是比进程更小的能独立运行的基本单位。一个进程由几个线程组成(拥有很多相对独立的执行流的用户程序共享应用程序的大部分数据结构),线程与同属一个进程的其他的线程共享进程所拥有的全部资源。

"进程——资源分配的最小单位,线程——程序执行的最小单位"

任务、进程和线程之间的关系如图1所示

155237936.png

4、关系与区别:

   进程有独立的地址空间,一个进程崩溃后,在保护模式下不会对其它进程产生影响,进程具有并发性、动态性、交互性、独立性和异步性等主要特性。而线程只是一个进程中的不同执行路径。线程有自己的堆栈和局部变量,但线程没有单独的地址空间,一个线程死掉就等于整个进程死掉,所以多进程的程序要比多线程的程序健壮,但在进程切换时,耗费资源较大,效率要差一些。但对于一些要求同时进行并且又要共享某些变量的并发操作,只能用线程,不能用进程。


总的来说就是:进程有独立的地址空间,线程没有单独的地址空间(同一进程内的线程共享进程的地址空间)。(下面的内容摘自Linux下的多线程编程

使用多线程的理由之一是和进程相比,它是一种非常"节俭"的多任务操作方式。我们知道,在Linux系统下,启动一个新的进程必须分配给它独立的地址空间,建立众多的数据表来维护它的代码段、堆栈段和数据段,这是一种"昂贵"的多任务工作方式。而运行于一个进程中的多个线程,它们彼此之间使用相同的地址空间,共享大部分数据,启动一个线程所花费的空间远远小于启动一个进程所花费的空间,而且,线程间彼此切换所需的时间也远远小于进程间切换所需要的时间。据统计,总的说来,一个进程的开销大约是一个线程开销的30倍左右,当然,在具体的系统上,这个数据可能会有较大的区别。

使用多线程的理由之二是线程间方便的通信机制。对不同进程来说,它们具有独立的数据空间,要进行数据的传递只能通过通信的方式进行,这种方式不仅费时,而且很不方便。线程则不然,由于同一进程下的线程之间共享数据空间,所以一个线程的数据可以直接为其它线程所用,这不仅快捷,而且方便。当然,数据的共享也带来其他一些问题,有的变量不能同时被两个线程所修改,有的子程序中声明为static的数据更有可能给多线程程序带来灾难性的打击,这些正是编写多线程程序时最需要注意的地方。

除了以上所说的优点外,不和进程比较,多线程程序作为一种多任务、并发的工作方式,当然有以下的优点:

  • 提高应用程序响应。这对图形界面的程序尤其有意义,当一个操作耗时很长时,整个系统都会等待这个操作,此时程序不会响应键盘、鼠标、菜单的操作,而使用多线程技术,将耗时长的操作(time consuming)置于一个新的线程,可以避免这种尴尬的情况。

  • 使多CPU系统更加有效。操作系统会保证当线程数不大于CPU数目时,不同的线程运行于不同的CPU上。

  • 改善程序结构。一个既长又复杂的进程可以考虑分为多个线程,成为几个独立或半独立的运行部分,这样的程序会利于理解和修改。

=============================

从函数调用上来说,进程创建使用fork()操作;线程创建使用clone()操作。


二、进程具体介绍:

1、Linux下的进程结构

进程不但包括程序的指令和数据,而且包括程序计数器和处理器的所有寄存器及存储临时数据的进程堆栈,因此,正在执行的进程包括处理器当前的一切活动。

因为linux是一个多任务多进程的操作系统,所以其他的进程必须等到系统将处理器使用权分配给自己之后才能运行。当正在运行的进程等待其他的系统资源时,linux内核将取得处理器的控制权,并将处理器分配给其他正在等待的进程,他按照内核中的调度算法决定将处理器分配给哪一个进程,也就是说,内核不会让处理器闲着。

内核将所有进程存放在双向循环链表(进程链表)中,其中链表的头是 init_task 描述符。链表的每一项都是类型为 task_struct,称为进程描述符的结构,该结构包含了一个进程相关的所有信息,定义在<include/linux/sched.h>文件中。task_struct内核结构比较大,它能完整的描述一个进程,如进程的状态、进程的基本信息、进程标识符、内存相关信息、父进程相关信息、与进程相关的终端信息、当前工作目录、打开的文件信息、所接收的信号信息等。


下面详细讲解task_struct结构中最为重要的两个域:state(进程状态)和pid(进程标识符)。如果想具体了解task_struct,请点这里。

(1)进程状态

Linux中的进程有以下几种状态。
● 运行状态(TASK_RUNNING):进程当前正在运行,或者正在运行队列中等待调度。
● 可中断的阻塞状态(TASK_INTERRUPTIBLE):进程处于阻塞(睡眠)状态,正在等待某些事件发生或能够占用某些资源。处在这种状态下的进程可以被信号中断。接收到信号或被显式的唤醒呼叫(如调用 wake_up 系列宏:wake_up、wake_up_interruptible等)唤醒之后,进程将转变TASK_RUNNING 状态。
● 不可中断的阻塞状态(TASK_UNINTERRUPTIBLE):此进程状态类似于可中断的阻塞状态(TASK_INTERRUPTIBLE),只是它不会处理信号,把信号传递到这种状态下的进程不能改变它的状态。在一些特定的情况下(进程必须等待,直到某些不能被中断的事件发生),这种状态是很有用的。只有在它所等待的事件发生时,进程才被显示的唤醒呼叫唤醒。
● 可终止的阻塞状态(TASK_KILLABLE):该状态的运行机制类似于TASK_UNINTERRUPTIBLE,只不过处在该状态下的进程可以响应致命信号。它可以替代有效但可能无法终止的不可中断的阻塞状态(TASK_UNINTERRUPTIBLE),以及易于唤醒但安全性欠佳的可中断的阻塞状态TASK_INTERRUPTIBLE)。
● 暂停状态(TASK_STOPPED):进程的执行被暂停,当进程收到 SIGSTOP、SIGSTP、SIGTTIN、SIGTTOU等信号时,就会进入暂停状态。
● 跟踪状态(TASK_TRACED):进程的执行被调试器暂停。当一个进程被另一个监控时(如调试器使用ptrace()系统调用监控测试程序),任何信号都可以把这个进程置于跟踪状态。
● 僵尸状态(EXIT_ZOMBIE):进程运行结束,父进程尚未使用 wait 函数族(如调用 waitpid()函数)等系统调用来“收尸”,即等待父进程销毁它。处在该状态下的进程“尸体”已经放弃了几乎所有的内存空间,没有任何可执行代码,也不能被调度,仅仅在进程列表中保留一个位置,记载该进程的推出状态等信息供其他进程收集。
● 僵尸撤销状态(EXIT_DEAD):这是最终状态,父进程调用 wait 函数族“收尸”后,进程彻底由系统删除。

它们之间的转换关系如图2所示:

161330655.png

进程可以使用 set_task_state 和 set_current_state 宏来改变指定进程的状态信息和当前进程的状态。

(2)进程标识符

Linux内核通过唯一的进程标识符 PID 来标识每个进程(就和文件描述符一样)。PID存放在进程描述符的 pid 字段中,新创建的 PID 通常是前一个进程的 PID 加1,不过PID的值有上限(最大值=PID_MAX_DEFAULT-1,通常为32767),读者可以查看/proc/sys/kernel/pid_max 来确定该系统的进程数上限。

当系统启动后,内核通常作为某一个进程的代表。一个指向task_struct的宏current用来记录正在运行的进程。current经常作为进程描述符结构指针的形式出现在内核代码中,例如,current->pid 表示处理器正在执行的进程的PID。当系统需要查看所有的进程时,则调用for_each_process()宏,这将比系统搜索数组的速度要快的多。

在Linux中获得当前进程号的(PID)和父进程号(PPID)的系统调用函数分别为getpid() 和 getppid()


2、进程的创建、执行、终止

(1)进程的创建和执行

咱们首先得知道啥是创建,啥是执行哈!我刚开始看的时候没懂。创建进程就是产生一个新的进程,这个大家都知道。而进程的执行,前边讲进程的的定义的时候,就说了正在运行的子任务,说白了,进程执行也就是让产生的这个进程干点什么事,别占着那啥不拉那啥。

许多操作系统提供的都是产生进程的机制,也就是说,首先在新的地址空间里创建进程、读入可执行文件,最后再开始执行。Linux 中进程的创建很特别,它把上述的步骤分解到两个单独的函数中去执行:fork()函数和exec函数族。首先,fork()函数通过复制当前进程创建一个子进程(注意此时资源还没有被复制过来,去了解一下写时复制页技术吧),子进程于父进程的区别仅仅在于不同的PID、PPID和某些资源及统计量。exec函数族负责读取可执行文件并将其载入地址空间开始运行。

(2)进程的终止

进程终结也需要很多繁琐的工作,系统必须保证回收进程所占用的资源,并通知父进程。Linux首先把终止的进程设置为僵尸状态,这时,进程无法投入运行,它的存在只为父进程提供信息,申请死亡。父进程得到信息后,开始调用 wait 函数族,最后终止子进程,子进程占用的所有资源被全部释放。


3、进程的内存结构

Linux操作系统采用虚拟内存管理技术,使得每个进程都有各自互不干涉的进程地址空间。该地址空间是大小为 4GB的线性虚拟空间,用户所看到和接触到的都是该虚拟地址,无法看到实际的物理内存地址。利用这种虚拟地址不但能起到保护操作系统的效果(用户不能直接访问物理地址),而且,更重要的是,用户程序可以使用比实际物理内存更大的地址空间。

4GB的进程地址空间会被分成两个部分:用户空间与内核空间。用户地址空间是从0到3GB(0xC000 0000),内核地址空间占据3GB到4GB。用户进程通常情况下只能访问用户空间的虚拟地址,不能访问内核空间的虚拟地址。只有用户进程使用系统调用(代表用户进程在内核态执行)时可以访问到内核空间。每当进程切换时,用户空间就跟着变化;而内核空间由内核负责映射,它不会跟着进程改变,是固定的。内核空间地址有自己对应的页表,用户进程各自有不同的页表。每个进程的用户空间都是完全独立、互不相干的。进程的虚拟内存空间如图3所示,其中用户空间包括以下几个功能区域:

164731554.png



● 只读段: 包含程序代码(.init和.text)和只读数据(.rodata)。

● 数据段: 存放的是全局变量和静态变量。其中可读可写数据段(.data)存放已初始化的全局变量和静态变量,BSS数据段(.bss)存放未初始化的全局变量和静态变量。

● 堆: 由系统自动分配释放,存放函数的参数值、局部变量的值、返回地址等。

● 堆栈: 存放动态分配的数据,一般由程序员动态分配和释放。若程序员不释放,程序结束时可能由操作系统回收。

● 共享库的内存映射区域: 这是Linux动态链接器和其他共享代码库代码的映射区域。


由于在Linux系统中每一个进程都会有/proc文件系统下与之对应的一个目录(如将init进程的相关信息在/proc/1 目录下的文件中描述),因此通过 proc 文件系统可以查看某个进程的地址空间的映射情况。例如,运行一个应用程序,如果它的进程号为13703,则输入“ cat /proc/13703/maps”命令,可以查看该进程的内存映射情况。


三、线程介绍:

前面已经讲到,进程是系统中程序执行和资源分配的基本单位。每个进程都拥有自己的数据段、代码段和堆栈段,这就造成了进程在进行切换等动作时需要较复杂的上下文切换等动作。为了进一步减少处理机的空闲时间,支持多处理器及减少上下文切换开销,进程在演化中出现了另一个概念---线程。它是进程内独立的一条运行路线,是处理器调度的最小单元,也可以称为轻量级线程。线程可以对进程的内存空间和资源分配进行访问,并与同一进程中的其他线程共享。因此,线程的上下文切换的开销比创建进程小得多。

一个进程可以拥有多个线程,每个线程必须有一个父进程。线程不拥有系统资源,它只具有运行时所必须的一些数据结构,如堆栈/寄存器与线程控制块(TCB),线程与其父进程的其他线程共享该进程所拥有的全部资源。要注意的是,由于线程共享了进程的资源和地址空间,因此,任何线程对系统资源的操作都会给其他进程带来影响。由此可知,多线程中的同步是一个非常重要的问题。在多线程系统中,进程与线程的关系如图4所示

165436586.png

在Linux系统中,线程分为3种:①用户线程 ②轻量级线程 ③内核线程


四、函数介绍:

1、fork()函数

在 Linux 中创建一个新进程的唯一方法是使用fork()函数。fork()函数是 Linux 系统中一个非常重要的函数,和咱们以前遇到过的函数由一些区别,因为它看起来执行一次却返回两个值,这又作何解释?不着急,慢慢看。

函数说明

fork()函数用于从已存在的一个进程中创建一个新的进程,新进程称为子进程,而原进程称为父进程。使用fork()函数得到的子进程是父进程的一个复制品,它从父进程处继承了整个进程的地址空间,包括进程上下文、代码段、进程堆栈、内存信息、打开的文件描述符、信号控制设定、进程优先级、进程组号、当前工作目录、根目录、资源限制和控制终端等,而子进程所独有的只有它的进程号、资源使用和计时器等。

因为子进程几乎是父进程的完全复制,所以父子进程会运行同一个程序。这就需要用一种方式来区分它们,并使它们照此运行,否则,这两个进程不可能做不同的事。

实际上是在父进程中执行fork()函数时,父进程会复制出一个子进程,而且父子进程的代码从fork()函数的返回开始分别在两个地址空间中同时运行,从而使两个进程分别获得其所属fork()函数的返回值,其中在父进程中的返回值是子进程的进程号,而在子进程中返回0。因此,可以通过返回值来判定该进程是父进程还是子进程。

同时可以看出,使用fork()函数的代价是很大的,它复制了父进程中的代码段、数据段和堆栈段里的大部分内容,使得 fork()函数的系统开销比较大,而且执行速度页不是很快。

函数语法

下表列出了 fork() 函数的语法要点

172928443.png


2、exec函数族


函数族说明

fork() 函数用于创建一个新的子进程,该子进程几乎复制了父进程的全部内容,但是,这个新创建的子进程如何执行呢?exec 函数族就提供了一个在进程中启动另一个程序执行的方法。它可以根据指定的文件名或目录名找到可执行文件,并用它来取代原调用进程的数据段、代码段和堆栈段,在执行完之后,原调用进程的内容除了进程号外,其他全部被新的进程替换了。另外,这里的可执行文件既可以是二进制文件,也可以是Linux下任何可执行的脚本文件。

在 Linux 中使用exec函数族主要有两种情况:

● 当进程认为自己不能再为系统和用户做出任何贡献时,就可以调用 exec 函数族中的任意一个函数让自己重生。

● 如果一个进程想执行另一个程序,那么它就可以调用 fork() 函数新建一个进程,然后调用 exec 函数族中的任意一个函数,这样看起来就像通过执行应用程序而产生了一个新进程(这种情况非常普遍)。

函数族语法

实际上,在Linux中并没有exec()函数,而是由6个以 exec 开头的函数,它们之间的语法有细微差别。下表列出了 exec 函数族的6个成员函数的语法:

182815931.jpg



● 查找方式:表1中的前4个函数的查找方式都是完整的文件目录路径,而最后两个函数(也就是以 p 结尾的两个函数)可以只给出文件名,系统就会自动按照环境变量“$PATH” 所指定的路径进行查找。

● 参数传递方式:exec函数族的参数传递有两种:一种是逐个列举的方式,而另一种则是将所有参数整体构造指针数组传递。在这里是以函数名的第5位字母来区分的,字母为 "l"(list)的表示逐个列举参数的方式,其语法为const char *arg;字母为“v”(vector)的表示将所有参数整体构造指针数组传递,其语法为 char *const argv[]。这里的参数实际上就是用户在使用这个可执行文件时所需的全部命令选项字符串(包括该可执行程序命令本身)。要注意的是,这些参数必须以NULL结束。

● 环境变量: exec函数族可以默认系统的环境变量,也可以传入指定的环境变量。这里以 “e”(environment)结尾的两个函数 execle()和 execve()就可以在 envp[]中指定当前进程所使用的环境变量。

表2再对这6个函数中的函数名和对应语法做了一个小结,主要指出了函数名中每一位对应所表明的含义,以此表加以记住这6个函数。

182815793.jpg

事实上,这6个函数中真正的系统调用只有execve(),其他5个都是库函数,它们最终都会调用execve()这个系统调用。在使用exec函数族时,一定要加上错误判断语句。exec 很容易执行失败,其中最常见的原因有:

① 找不到文件或路径,此时 errno 被设置为 ENOENT。

② 数组argv 和envp 忘记用NULL结束,此时,errno被设置为 EFAUL。

③ 没有对应可执行文件的运行权限,此时 errno 被设置为EACCES。


3、exit()和_exit()函数

函数说明

创建进程使用fork()函数,执行进程使用exec函数族,终止进程则使用exit()和_exit()函数。当进程执行到exit()或_exit()函数时,进程会无条件的停止剩下的所有操作,清除各种数据结构,并终止本进程的运行。但是,这两个函数还是有区别的,其调用过程如图1所示:


183055655.png



从图1可以看出,_exit()函数的作用是:直接使进程停止运行,清除其使用的内存空间,并清除其在内核中的各种数据结构;而exit()函数则在这些基础上做了一些包装,在执行退出之前加了若干道工序。exit()函数和_exit()函数的最大区别就在于exit()函数在终止当前进程之前要检查该进程打开过哪些文件,把文件缓冲区中的内容写回文件,也就是图1中的“清理I/O缓冲”一项。

在Linux的标准函数库中,有一种被称作“缓冲I/O(buffered I/O)”的操作,其特征就是对应每一个打开的文件,在内存中都有一片缓冲区。

每次读文件时,会连续读出若干条记录,这样在下次读文件时就可以直接从内存的缓冲区中读取;同样,每次写文件时,也仅仅是写入内存中的缓冲区,等满足了一定的条件(如达到一定数量或遇到特定字符等,最典型的就是咱们的vim中使用的:w命令),再将缓冲区中的内容一次性写入文件。

这种技术大大增加了文件读写的速度,但也给咱们的编程带来了一些麻烦。比如有些数据你认为已经被写入到文件中,实际上因为没有满足特定的条件,它们还只是被保存在缓冲区内,这时用_exit()函数直接将进程关闭掉,缓冲区中的数据就会丢失。因此,若想保证数据的完整性,最好使用exit()函数。

函数语法

下表列出了exit()和_exit()函数的语法要点:

183055638.png


4、wait()和waitpid()

函数说明

wait()函数用于使父进程(也就是调用wait()的进程)阻塞,直到一个子进程结束或者该进程接收到了一个指定的信号为止。如果该父进程没有子进程或者它的子进程已经结束,则wait()函数就会立即返回。

waitpid()的作用和wait()一样,但它并不一定要等待第一个终止的子进程(它可以指定需要等待终止的子进程),它还有若干选项,如可提供一个非阻塞版本的 wait()功能,也能支持作业控制。实际上,wait()函数只是 waitpid()函数的一个特例,在Linux 内部实现 wait()函数时直接调用的就是waitpid()函数。

函数格式

下图为wait()函数的格式

183321574.png

下图为waitpid()函数的格式

183321816.png

五、僵尸进程和孤儿进程

1、基本概念

  我们知道在unix/linux中,正常情况下,子进程是通过父进程创建的,子进程在创建新的进程。子进程的结束和父进程的运行是一个异步过程,即父进程永远无法预测子进程 到底什么时候结束。 当一个 进程完成它的工作终止之后,它的父进程需要调用wait()或者waitpid()系统调用取得子进程的终止状态。

  孤儿进程:一个父进程退出,而它的一个或多个子进程还在运行,那么那些子进程将成为孤儿进程。孤儿进程将被init进程(进程号为1)所收养,并由init进程对它们完成状态收集工作。

  僵尸进程:一个进程使用fork创建子进程,如果子进程退出,而父进程并没有调用wait或waitpid获取子进程的状态信息,那么子进程的进程描述符仍然保存在系统中。这种进程称之为僵死进程。

     2、问题及危害

  unix提供了一种机制可以保证只要父进程想知道子进程结束时的状态信息, 就可以得到。这种机制就是: 在每个进程退出的时候,内核释放该进程所有的资源,包括打开的文件,占用的内存等。 但是仍然为其保留一定的信息(包括进程号the process ID,退出状态the termination status of the process,运行时间the amount of CPU time taken by the process等)。直到父进程通过wait / waitpid来取时才释放。 但这样就导致了问题,如果进程不调用wait / waitpid的话, 那么保留的那段信息就不会释放,其进程号就会一直被占用,但是系统所能使用的进程号是有限的,如果大量的产生僵死进程,将因为没有可用的进程号而导致系统不能产生新的进程. 此即为僵尸进程的危害,应当避免。

  孤儿进程是没有父进程的进程,孤儿进程这个重任就落到了init进程身上,init进程就好像是一个民政局,专门负责处理孤儿进程的善后工作。每当出现一个孤儿进程的时候,内核就把孤 儿进程的父进程设置为init,而init进程会循环地wait()它的已经退出的子进程。这样,当一个孤儿进程凄凉地结束了其生命周期的时候,init进程就会代表党和政府出面处理它的一切善后工作。因此孤儿进程并不会有什么危害。

  任何一个子进程(init除外)在exit()之后,并非马上就消失掉,而是留下一个称为僵尸进程(Zombie)的数据结构,等待父进程处理。这是每个 子进程在结束时都要经过的阶段。如果子进程在exit()之后,父进程没有来得及处理,这时用ps命令就能看到子进程的状态是“Z”。如果父进程能及时 处理,可能用ps命令就来不及看到子进程的僵尸状态,但这并不等于子进程不经过僵尸状态。  如果父进程在子进程结束之前退出,则子进程将由init接管。init将会以父进程的身份对僵尸状态的子进程进行处理。

  僵尸进程危害场景:

  例如有个进程,它定期的产 生一个子进程,这个子进程需要做的事情很少,做完它该做的事情之后就退出了,因此这个子进程的生命周期很短,但是,父进程只管生成新的子进程,至于子进程 退出之后的事情,则一概不闻不问,这样,系统运行上一段时间之后,系统中就会存在很多的僵死进程,倘若用ps命令查看的话,就会看到很多状态为Z的进程。 严格地来说,僵死进程并不是问题的根源,罪魁祸首是产生出大量僵死进程的那个父进程。因此,当我们寻求如何消灭系统中大量的僵死进程时,答案就是把产生大 量僵死进程的那个元凶枪毙掉(也就是通过kill发送SIGTERM或者SIGKILL信号啦)。枪毙了元凶进程之后,它产生的僵死进程就变成了孤儿进 程,这些孤儿进程会被init进程接管,init进程会wait()这些孤儿进程,释放它们占用的系统进程表中的资源,这样,这些已经僵死的孤儿进程 就能瞑目而去了。


3、如何杀死僵尸进程呢?

一般僵尸进程很难直接kill掉,不过您可以kill僵尸爸爸。父进程死后,僵尸进程成为”孤儿进程”,过继给1号进程init,init始终会负责清理僵尸进程.它产生的所有僵尸进程也跟着消失。

ps -e -o ppid,stat | grep Z | cut -d” ” -f2 | xargs kill -9



kill -HUP `ps -A -ostat,ppid | grep -e ’^[Zz]‘ | awk ’{print $2}’`

当然您可以自己编写更好的shell脚本,欢迎与大家分享。

我将nova-novncproxy stop后再start,僵尸进程即消失,问题解决。

另外子进程死后,会发送SIGCHLD信号给父进程,父进程收到此信号后,执行waitpid()函数为子进程收尸。就是基于这样的原理:就算父进程没有调用wait,内核也会向它发送SIGCHLD消息,而此时,尽管对它的默认处理是忽略,如果想响应这个消息,可以设置一个处理函数。

    4、如何避免僵尸进程呢?

处理SIGCHLD信号并不是必须的。但对于某些进程,特别是服务器进程往往在请求到来时生成子进程处理请求。如果父进程不等待子进程结束,子进程将成为僵尸进程(zombie)从而占用系统资源。如果父进程等待子进程结束,将增加父进程的负担,影响服务器进程的并发性能。在Linux下 可以简单地将 SIGCHLD信号的操作设为SIG_IGN。
signal(SIGCHLD,SIG_IGN);
这样,内核在子进程结束时不会产生僵尸进程。这一点与BSD4不同,BSD4下必须显式等待子进程结束才能释放僵尸进程或者用两次fork(),而且使紧跟的子进程直接退出,是的孙子进程成为孤儿进程,从而init进程将负责清除这个孤儿进程。

六、Linux进程间通信(IPC)


linux下的进程通信手段基本上是从Unix平台上的进程通信手段继承而来的。而对Unix发展做出重大贡献的两大主力AT&T的贝尔实验室及BSD(加州大学伯克利分校的伯克利软件发布中心)在进程间通信方面的侧重点有所不同。前者对Unix早期的进程间通信手段进行了系统的改进和扩充,形成了“system V IPC”,通信进程局限在单个计算机内;后者则跳过了该限制,形成了基于套接口(socket)的进程间通信机制。Linux则把两者继承了下来,如图示:


1.gif

其中,最初Unix IPC包括:管道、FIFO、信号;System V IPC包括:System V消息队列、System V信号灯、System V共享内存区;Posix IPC包括:Posix消息队列、Posix信号灯、Posix共享内存区。有两点需要简单说明一下:1)由于Unix版本的多样性,电子电气工程协会(IEEE)开发了一个独立的Unix标准,这个新的ANSI Unix标准被称为计算机环境的可移植性操作系统界面(PSOIX)。现有大部分Unix和流行版本都是遵循POSIX标准的,而Linux从一开始就遵循POSIX标准;2)BSD并不是没有涉足单机内的进程间通信(socket本身就可以用于单机内的进程间通信)。事实上,很多Unix版本的单机IPC留有BSD的痕迹,如4.4BSD支持的匿名内存映射、4.3+BSD对可靠信号语义的实现等等。

图一给出了linux 所支持的各种IPC手段,在本文接下来的讨论中,为了避免概念上的混淆,在尽可能少提及Unix的各个版本的情况下,所有问题的讨论最终都会归结到Linux环境下的进程间通信上来。并且,对于Linux所支持通信手段的不同实现版本(如对于共享内存来说,有Posix共享内存区以及System V共享内存区两个实现版本),将主要介绍Posix API。

linux下进程间通信的几种主要手段简介:

  1. 管道(Pipe)及有名管道(named pipe):管道可用于具有亲缘关系进程间的通信,有名管道克服了管道没有名字的限制,因此,除具有管道所具有的功能外,它还允许无亲缘关系进程间的通信;

  2. 信号(Signal):信号是比较复杂的通信方式,用于通知接受进程有某种事件发生,除了用于进程间通信外,进程还可以发送信号给进程本身;linux除了支持Unix早期信号语义函数sigal外,还支持语义符合Posix.1标准的信号函数sigaction(实际上,该函数是基于BSD的,BSD为了实现可靠信号机制,又能够统一对外接口,用sigaction函数重新实现了signal函数);

  3. 报文(Message)队列(消息队列):消息队列是消息的链接表,包括Posix消息队列system V消息队列。有足够权限的进程可以向队列中添加消息,被赋予读权限的进程则可以读走队列中的消息。消息队列克服了信号承载信息量少,管道只能承载无格式字节流以及缓冲区大小受限等缺点。

  4. 共享内存:使得多个进程可以访问同一块内存空间,是最快的可用IPC形式。是针对其他通信机制运行效率较低而设计的。往往与其它通信机制,如信号量结合使用,来达到进程间的同步及互斥。

  5. 信号量(semaphore):主要作为进程间以及同一进程不同线程之间的同步手段。

  6. 套接口(Socket):更为一般的进程间通信机制,可用于不同机器之间的进程间通信。起初是由Unix系统的BSD分支开发出来的,但现在一般可以移植到其它类Unix系统上:Linux和System V的变种都支持套接字。


七、System v IPC 与 Posix IPC

1、System v IPC

消息队列、信号灯、共享内存常用在Linux服务端编程的进程间通信环境中。而此三类编程函数在实际项目中都是用System V IPC函数实现的。System V IPC函数名称和说明如下表15-1所示。

System V IPC函数


消息队列

信号量

共享内存区

头文件

<sys/msg.h>

<sys/sem.h>

<sys/shm.h>

创建或打开IPC函数

msgget

semget

shmget

控制IPC操作的函数

msgctl

semctl

shmctl

IPC操作函数

msgsnd

msgrcv

semop

shmat

shmdt



2、Posix IPC

Posix IPC 函数


消息队列

信号量

共享内存区

头文件

<mqueue.h>

<semaphore.h>

<sys/mman.h>

创建、打开或删除IPC函数

mq_open

mq_close

mq_unlink

sem_open

sem_close

sem_unlink

shm_open

shm_unlink

sem_init

sem_destory

控制IPC操作的函数

mq_getattr

mq_setattr


ftruncate

fstat

IPC操作函数

mq_send

mq_receive

mq_notify

sem_wait

sem_trywait

sem_post

sem_getvalue

mmap

munmap