进程以及进程间通讯
1 进程
1.1 进程的“生老病死”
1.1.1 进程状态
(1)进程刚被创建出来时,处于TASK_RUNNING状态,此状态可以是正在队列排队等待执行也可以是占用CPU正在运行。
(2)刚被创建的进程处于“就绪”状态,等待系统调度,内核中的函数sched()称为调度器,会根据各种参数选择一个等待的进程去占用CPU。
(3)进程处于“执行”状态时,可能会由于某些资源不可得而被置为“睡眠态/挂起态”,有TASK_INTERRUPIBLE或TASK_UNINTERRUPIBLE,后者是深度睡眠,不能响应信号。
(4)当进程收到SIGSTOP或SIGTSTP中的一个信号时,状态会变为“暂停态”,该状态下的进程不参与调度,但系统不会释放资源。
(5)“僵尸态”即死亡后携带死亡信息,系统不会回收资源。父进程会关心子进程的死亡信息,因为父进程可以看到交代子进程办的事办的怎么样。
(6)父进程电泳wait()/waitpid()来查看孩子的“死亡信息”,顺便将该孩子的状态设置为EXIT_DEAD,即死亡态。
(7)父进程并不能保证准时的去收尸,因为父进程可能还有其他的事要做,或者父进程还有可能先于子进程挂掉。前一种情况,可能让祖先进程会收养这些孤儿进程。后一种情况需要使用信号异步通知机制,让一个孩子在变成僵尸时给父进程发送一个信号,父进程接收到信号就立处理。(如果两个孩子同时死亡能?)
1.1.2 进程的祖先
init.是所有进程的祖先,从内存空间的角度看,进程之间是相互独立的。
1.2 进程的语言
有如下几种。
(1)无名管道(PIPE)和有名管道(FIFO)
(2)信号(signal)
(3)system V-IPC之共享内存
(4)system V-IPC之消息队列
(5)system V-IPC之信号量
(6)套接字
1.2.1 管道
无名管道(PIPE)
无名管道的特征如下。
(1)没有名字,因此无法使用open()
(2)只能用于亲缘进程间通信
(3)半双工工作方式:读写端分开
(4)写入操作不具有原子性,因此只能用于一对一的简单通信情形
(5)不能使用lseek()定位
有名管道(FIFO)
有名管道的特征如下。
(1)有名字,存储于普通文件系统之中
(2)任何具有相应权限的进程都可以使用open()来获取FIFO的文件描述符
(3)跟普通文件一样,使用统一的read()/write()来读/写
(4)跟普通文件不一样,不能使用lseek()定位
(5)具有写入原子性,支持多写者同时进行写操作而数据不会互相践踏。
(6)First In First Out,最先被写入的数据,最先被读出来
小结
PIPE只能亲缘通信的原因,PIPE是一种特殊的文件,但虽然它是一种文件,却没有名字!因此,一般进程无法使用open()来获取它的描述符,它只能在一个进程中被创建出来,然后通过继承的方式将它的文件描述符传递给子进程。
PIPE与FIFO、socket一样,这些管道文件都不能使用lseek()来进行所谓的定位,因为它们的数据不像普通文件那样按块的方式存放在块设备上,而更像一个看不见源头的水龙头,无法定位。
管道文件不可以只在有读端或者写端的情况下被打开。
1.2.2 信号
重要概念
设置阻塞、解开阻塞、信号相应函数、设置信号、信号集、实时信号、非实时信号、普通响应函数、响应扩展函数
信号SIGKILL和SIGSTOP是两个特殊的信号,不能被忽略、阻塞或捕捉,只能按默认动作来响应。
非实时信号不排队、会丢失
实时信号排队,不会丢失
发送信号不携带消息 kill()和signal()
发送信号携带消息sigqueue()和sigaction()
在响应函数内部访问任何共享资源,都必须和多线程一样,使用同步互斥机制来确保访问的安全性。
1.2.3 消息队列
前述的管道,这种通信机制的一个弊端是:无法在管道中读取一个“指定”的数据,因为这些数据没有做任何标记,进程只能按次序地逐个读取。而消息队列提供一种带有数据标识的特殊管道。
消息队列的使用简单,但它和管道一样,都需要“代理人”的进程通信机制:内核充当了这个代理人,内核为使用者分配内存、检查边界、设置阻塞,以及各种权限监控。但是代理人机制的效率都不高,因为两个进程的数据传递并不是直接的,而是通过内核,因此它们都不适合传输海量数据。
1.2.4 共享内存
共享内存是效率最高的IPC, 因为它抛弃了内核这个“代理人”,直截了当地将一块裸露的内存放在需要数据传输的进程面前,让它们自己做,代价是:这些进程必须小心谨慎地操作这块裸露的共享内存。
使用共享内存的一般步骤如下。
(1)获取共享内存对象的ID。int shmget(key_t key, size_t size, int shmflg)
(2)将共享内存映射至本进程虚拟内存空间的某个区域。void *shmat(int shmid, const void *shmaddr, int shmflg)
(3)当不再使用时,解除映射关系。 int shmdt(const void *shmaddr)
(4)当没有进程再需要这块共享内存时,删除它。int shmctl(int shmid, int cmd, struct shmid_ds *buf)
注意:
(1)共享内存只能以只读或者可读写方式映射,无法以只写方式映射。
(2)解除映射后,进程不能再允许访问SHM。
1.2.5 信号量
不是用来传输数据的,而用来协调各进程或线程工作的。
一些基本概念如下:
(1)多个进程或线程有可能同时访问的资源称为共享资源,也称临界资源。
(2)访问这些资源的代码称为临界代码,这些代码区称为临界区。
(3)程序进入临界区之前必须对资源进行申请,这个动作称为P操作。
(4)程序离开临界区之后必须释放相应地资源,这个动作称为V操作。