Note6:进程间通信(IPC)

首先,先来讲一下fork之后,发生了什么事情。由fork创建的新进程被称为子进程(child process)。该函数被调用一次,但返回两次。两次返回的区别是子进程的返回值是0,而父进程的返回值则是新进程(子进程)的进程 id。将子进程id返回给父进程的理由是:因为一个进程的子进程可以多于一个,没有一个函数使一个进程可以获得其所有子进程的进程id。对子进程来说,之所以fork返回0给它,是因为它随时可以调用getpid()来获取自己的pid;也可以调用getppid()来获取父进程的id。(进程id 0总是由交换进程使用,所以一个子进程的进程id不可能为0 )。

å¨è¿éæå¥å¾çæè¿°

 fork之后,操作系统会复制一个与父进程完全相同的子进程,虽说是父子关系,但是在操作系统看来,他们更像兄弟关系,这 2个进程共享代码空间,但是数据空间是互相独立的,子进程数据空间中的内容是父进程的完整拷贝,指令指针也完全相同,子进程拥有父进程当前运行到的位置(两进程的程序计数器pc值相同,也就是说,子进程是从fork返回处开始执行的),但有一点不同,如果fork成功,子进程中fork的返回值是0,父进程中fork的返回值是子进程的进程号,如果fork不成功,父进程会返回错误。

可以这样想象,两个进程一直同时运行,而且步调一致,在fork之后,他们分别作不同的工作,也就是分岔了。这也是fork 为什么叫fork的原因。至于哪一个最先运行,这个与操作系统进程调度算法有关,而且这个问题在实际应用中并不重要,如果需要父子进程协同,可以通过原语的办法解决。

那么如果多个进程之间需要协同处理某个任务时,这时就需要进程间的同步和数据交流。常用的进程间通信(IPC,InterProcess Communication)的方法有:

1. 信号 ( Sinal ) : 信号是一种比较复杂的通信方式,用于通知接收进程某个事件已经发生;

2. 管道(Pipe):管道是一种半双工的通信方式,数据只能单向流动,而且只能在具有亲缘关系(通常是指父子进程关系)的进程间使用;

3. 命名管道FIFO:命名管道(Named Pipe)也是半双工的通信方式,但是它允许无亲缘关系进程间的通信;

4. 命名socket或UNIX域socket(Named Socket或Unix Domain Socket):socket也是一种进程间通信机制,与其他通信机制不同的是,它可用于不同进程间的进程通信;

5. 信号量(Semaphore):信号量是一个计数器,可以用来控制多个进程对共享资源的访问。它常作为一种锁机制,防止某进程正在访问共享资源时,其他进程也访问该资源。因此,主要作为进程间以及同一进程内不同线程之间的同步手段;

6. 共享存储(Shared Memory):共享内存就是映射一段能被其他进程所访问的内存,这段共享内存由一个进程创建,但多个进程都可以访问。共享内存是最快的 IPC 方式,它是针对其他进程间通信方式运行效率低而专门设计的。它往往与其 他通信机制,如信号两,配合使用,来实现进程间的同步和通信;

7. 消息队列(Message Queue):消息队列是由消息的链表,存放在内核中并由消息队列标识符标识。消息队列克服了信号传递信息少、管道只能承载无格式字节流以及缓冲区大小受限等缺点;

 

一、信号

信号是Linux系统中用于进程之间通信或操作的一种机制,信号可以在任何时候发送给某一进程,而无须知道该进程的状态。如果该进程并未处于执行状态,则该信号就由内核保存起来,直到该进程恢复执行并传递给他为止。如果一个信号被进程设置为阻塞,则该信号的传递被延迟,直到其阻塞被取消时才被传递给进程。

Linux提供了几十种信号,分别代表着不同的意义。信号之间依靠他们的值来区分,但是通常在程序中使用信号的名字来表示一个信号。信号是在软件层次上对中断机制的一种模拟,是一种异步通信方式,信号可以在用户空间进程和内核之间直接交互。内核也可以利用信号来通知用户空间的进程来通知用户空间发生了哪些系统事件。

我们知道,父进程在创建子进程之后,究竟是父进程还是子进程先运行没有规定,这由操作系统的进程调度策略决定,而如果在某些情况下我们需要确保父子进程运行的先后顺序,则可以使用信号来实现进程间的同步。下面是一个父子进程之间使用信号进行同步的例程。在下面的这个程序中,如果父进程先执行则进入到循环休眠等待状态,直到子进程给他发送信号之后才能跳出循环继续运行,这就可以确保子进程先执行它的任务。同样子进程在执行完成任务之后,就等待父进程给他发送信号之后才能退出,而父进程则通过调用wait()系统调用等待子进程退出后,父进程再退出。

二、管道

管道是UNIX系统IPC的最古老的形式,所有的UNIX系统都提供此种通信机制。管道的实质是一个内核缓冲区,进程以先进先出(FIFO, First In First Out)的方式从缓冲区存取数据:管道一端的进程顺序地将进程数据写入缓冲区,另一端的进程则顺序地读取数据,该缓冲区可以看做一个循环队列,读和写的位置都是自动增加的,一个数据只能被读一次,读出以后再缓冲区都不复存在了。当缓冲区读空或者写满时,有一定的规则控制相应的读进程或写进程是否进入等待队列,当空的缓冲区有新数据写入或慢的缓冲区有数据读出时,就唤醒等待队列中的进程继续读写。

1、两个局限性:     

(1)半双工,数据只能在一个方向流动,现在有些系统可以支持全双工管道,但是为了最佳的可移植性,应认为系统不支持全双工管道;     

(2)管道只能在具有公共祖先之间的两个进程之间使用;   

2、管道的创建:    

管道可以看成是一种特殊的文件,对于它的读写也可以使用普通的read、write 等函数。但是它不是普通的文件,并不属于其他任何文件系统,并且只存在于内存中。管道是通过调用pipe函数创建的。

#include <unistd.h>
int pipe(int fd[2]); //返回值:若成功,返回0,若出错,返回-1.

经由参数fd返回的两个文件描述符:fd[0]为读而打开,fd[1]为写而打开,fd[1]的输出是fd[0]的输入。 

三、命名管道

前面讲到的未命名的管道只能在两个具有亲缘关系的进程之间通信,通过命名管道(Named PiPe)FIFO,不相关的进程也能交换数据。FIFO不同于管道之处在于它提供一个路径与之关联,以FIFO的文件形式存在于系统中。它在磁盘上有对应的节点,但没有数据块——换言之,只是拥有一个名字和相应的访问权限,通过mknode()系统调用或者mkfifo()函数来建立的。一旦建立,任何进程都可以通过文件名将其打开和进行读写,而不局限于父子进程,当然前提是进程对FIFO有适当的访问权。当不再被进程使用时,FIFO在内存中释放,但磁盘节点仍然存在。

四、命名socket

使用socket除了可以实现网络间不同主机间的通信外,还可以实现同一主机的不同进程间的通信,且建立的通信是双向的通信,这种方式就是命名socket(Named Socket)又叫Unix域socket(Unix Domain Socket)。Unix域协议并不是一个实际的协议族,而是在单个主机上执行客户/服务通信的一种方式,也是进程间通信(IPC)的一种方式。UNIX域数据报服务是可靠的,不会丢失消息,也不会传递出错。它也提供了两类套接字:字节流套接字(有点像TCP)和数据报套接字(有点像UDP)。

五、信号量

在创建子进程后,究竟是父进程还是子进程先运行这由操作系统调度策略决定,而如果要保证父子进程执行的先后顺序呢?在以前的代码中我们是通过sleep()函数来实现的,比如我想让子进程先运行再让父进程运行,那么我就在父进程的程序中加一个sleep()函数,让父进程先睡眠,这样子就能先执行子进程了。有的时候咱们事先无法知道父进程和子进程哪一个先执行,但是要 向我那样使用sleep()函数,只能保证先执行子进程,但是不能保证子进程执行完后再执行父进程。所以如果我们想要子进程完全 执行完后再执行父进程,就可以利用信号量(Semaphore)来解决它们之间的同步问题。在前面我们讲过信号(Signal),其实信号和信号量是两个不同的东西,它们虽然都可以实现同步和互斥,但是前者是使用信号处理器来进行的,而后者是使用P,V 操作来实现的。

在讲信号量之前,先了解两个概念同步和互斥:一条食品生产线上,假设A、B共同完成一个食品的包装任务,A负责将食品放到盒子里,B和C负责将盒子打包。必须得是A先装食品B再打包吧,要是B不按规则先打包,那A还装啥,所以就需要一种机制方法保证A先进行B再进行,“信号量”就是这种机制方法,AB之间的关系就是同步关系;再假设打包要用到刀子,而车间就有一把刀子,这时候B和C就构成了互斥关系。 在多任务操作系统环境下,多个进程会同时运行,并且一些进程间可能会存在一定的关联。多个进程可能为了完成同一个任务相互协作,这就形成了进程间的同步关系。而且在不同进程间,为了争夺有限的系统资源(硬件或软件资源)会进入竞争状态,这就是进程间的互斥关系。

进程间的互斥关系与同步关系存在的根源在于临界资源。临界资源是在同一时刻只允许有限个(通常只有一个)进程可以访问(读)或修改(写)的资源,通常包括硬件资源(处理器、内存、存储器及其它外围设备等)和软件资源(共享代码段、共享结构和变量等)。访问临界资源的代码叫做临界区,临界区本身也会称为临界资源。信号量是用来解决进程间的同步与互斥问题的一种进程间通信机制,包括一个称为信号量的变量和在该信号量下等待资源的进程等待队列,以及对信号量进行的两个原子操作(P/V操作)。其中,信号量对应于某一种资源,取一个非负的整形值。信号量值(常用sem_id表示)指的是当前可用的该资源的数量,若等于0则意味着目前没有可用的资源。

PV原子操作的具体定义如下:

P操作:如果有可用的资源(信号量值>0),则此操作所在的进程占用一个资源(此时信号量值减1,进入临界区代码); 如果没有可用的资源(信号量值=0),则此操作所在的进程被阻塞直到系统将资源分配给该进程(进入等待队列,一直等 到资源轮到该进程)。

V操作:如果在该信号量的等待队列中有进程在等待资源,则唤醒一个阻塞进程;如果没有进程等待它,则释放一个资源 (即信号量值加1)。

六、共享内存

共享内存,顾名思义,就是两个或多个进程都可以访问的同一块内存空间,一个进程对这块空间内容的修改可为其他参与通信的进程所看到的。显然,为了达到这个目的就需要做两件事:一件是在内存划出一块区域来作为共享区;另一件是把这个区域映射到参与通信的各个进程空间。共享内存区是最快的IPC形式。一旦这样的内存映射到共享它的进程的地址空间,这些进程间数据传递不再涉及到内核,换句话说是进程不再通过执行进入内核的系统调用来传递彼此的数据。下图是共享内存示意图:

共享内存会使用到下面几个函数:

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>

key_t ftok(const char *pathname, int proj_id);
该函数在信号量里已经讲过

int shmget(key_t key, size_t size, int shmflg);
说明:该函数用来创建共享内存,成功返回一个非负整数,即该共享内存段的标识码;失败返回-1。
第一个参数key是ftok()返回的key_t类型键值;
第二个参数size以字节为单位指定需要共享的内存容量。;
第三个参数shmflg是权限标志,它的作用与open函数的mode参数一样,如果要想在key标识的共享内存不存在就创建它的话,可以与
IPC_CREAT做或操作。共享内存的权限标志与文件的读写权限一样,举例来说,0644,它表示允许一个进程创建的共享内存被内存创建
者所拥有的进程向共享内存读取和写入数据,同时其他用户创建的进程只能读取共享内存。

void *shmat(int shmid, const void *shmaddr, int shmflg);
 说明:第一次创建完共享内存时,它还不能被任何进程访问,shmat函数的作用就是用来启动对该共享内存的访问,并把共享内存连
接到当前进程的地址空间。调用成功时返回一个指向共享内存第一个字节的指针,如果调用失败返回-1.
 第一个参数shmid是由shmget函数返回的共享内存标识;
 第二个参数shmaddr指定共享内存连接到当前进程中的地址位置,通常为空,表示让系统来选择共享内存的地址。
 第三个参数shmflg是一组标志位,通常为0。

int shmdt(const void *shmaddr);
 说明:该函数用于将共享内存从当前进程中分离。注意,将共享内存分离并不是删除它,只是使该共享内存对当前进程不再可用。
 参数shmaddr是shmat函数返回的地址指针,调用成功时返回0,失败时返回-1。

int shmctl(int shmid, int cmd, struct shmid_ds *buf);
 说明:该函数用于控制共享内存
 第一个参数shmid是shmget函数返回的共享内存标识符;
 第二个参数cmd是要采取的操作,它可以取下面的三个值:
 IPC_STAT:把shmid_ds结构中的数据设置为共享内存的当前关联值,即用共享内存的当前关联值覆盖shmid_ds的值;
 IPC_SET:如果进程有足够的权限,就把共享内存的当前关联值设置为shmid_ds结构中给出的值;
 IPC_RMID:删除共享内存段
 第三个参数,buf是一个结构指针,它指向共享内存模式和访问权限的结构。其结构体类型定义如下:

struct shmid_ds
{
 uid_t shm_perm.uid;
 uid_t shm_perm.gid;
 mode_t shm_perm.mode;
}

下面两个两个进程(不同程序)使用共享内存方式共享一个结构体变量的示例,其中shared_mem_write程序用来创建一个student结构体共享内存并改更新里面的成员内容,而shared_mem_read 则在另外一个毫无关系的进程中同步访问该结构体里的内容。

vim shared_mem_write.c

#include <stdio.h>
#include <string.h>
#include <errno.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#define FTOK_PATH "/dev/zero"
#define FTOK_PROJID 0x22
typedef struct st_student
{
 char name[64];
 int age;
} t_student;
int main(int argc, char **argv)
{
 key_t key;
 int shmid;
 int i;
 t_student *student;
 if( (key=ftok(FTOK_PATH, FTOK_PROJID)) < 0 )
 {
 printf("ftok() get IPC token failure: %s\n", strerror(errno));
 return -1;
 }
 shmid = shmget(key, sizeof(t_student), IPC_CREAT|0666);
 if( shmid < 0)
 {
 printf("shmget() create shared memroy failure: %s\n", strerror(errno));
 return -2;
 } 
 student = shmat(shmid, NULL, 0);
 if( (void *)-1 == student )
 {
 printf("shmat() alloc shared memroy failure: %s\n", strerror(errno));
 return -2;
 }
 strncpy(student->name, "zhangsan", sizeof(student->name));
 student->age = 18;
 for(i=0; i<4; i++)
 {
 student->age ++;
 printf("Student '%s' age [%d]\n", student->name, student->age);
 sleep(1);
 }
 shmdt(student);
 shmctl(shmid, IPC_RMID, NULL);
 return 0;
}

 vim shared_mem_read.c

#include <stdio.h>
#include <string.h>
#include <errno.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#define FTOK_PATH "/dev/zero"
#define FTOK_PROJID 0x22
typedef struct st_student
{
 char name[64];
 int age;
} t_student;
int main(int argc, char **argv)
{
 key_t key;
 int shmid;
 int i;
 t_student *student;
 if( (key=ftok(FTOK_PATH, FTOK_PROJID)) < 0 )
 {
 printf("ftok() get IPC token failure: %s\n", strerror(errno));
 return -1;
 }
 shmid = shmget(key, sizeof(t_student), IPC_CREAT|0666);
 if( shmid < 0)
 {
 printf("shmget() create shared memroy failure: %s\n", strerror(errno));
 return -2;
 }
 student = shmat(shmid, NULL, 0);
 if( (void *)-1 == student )
 {
 printf("shmat() alloc shared memroy failure: %s\n", strerror(errno));
 return -2;
 }
 for(i=0; i<4; i++)
 {
 printf("Student '%s' age [%d]\n", student->name, student->age);
 sleep(1);
 }
 shmctl(shmid, IPC_RMID, NULL);
 return 0;
}

gcc shared_mem_write.c -o shared_mem_write

gcc shared_mem_read.c -o shared_mem_read

./shared_mem_write

Student 'zhangsan' age [19]
Student 'zhangsan' age [20]
Student 'zhangsan' age [21]
Student 'zhangsan' age [22]

./shared_mem_read

Student 'zhangsan' age [19]
Student 'zhangsan' age [20]
Student 'zhangsan' age [21]
Student 'zhangsan' age [22]

七、消息队列

消息队列(Message Queue,简称MQ)提供了一个从一个进程向另外一个进程发送一块数据的方法,每个数据块都被认为 是有一个类型,接收者进程接收的数据块可以有不同的类型值。消息队列也有管道一样的不足,就是每个消息的最大长度是有上 限的(MSGMAX),每个消息队列的总的字节数是有上限的(MSGMNB),系统上消息队列的总数也有一个上限 (MSGMNI)。

MQ 传递的是消息,消息即是我们需要在进程间传递的数据。MQ 采用链表来实现消息队列,该链表是由系统内核维护,系统 中可能有很多的 MQ,每个 MQ 用消息队列描述符(消息队列 ID:qid)来区分,qid 是唯一的,用来区分不同的 MQ。在进行 进程间通信时,一个进程将消息加到 MQ 尾端,另一个进程从消息队列中取消息(不一定以先进先出来取消息,也可以按照消息 类型字段取消息),这样就实现了进程间的通信。如下 MQ 的模型, 进程 A 向内核维护的消息队列中发消息,进程 B 从消息队 列中取消息,从而实现了 A 和 B 的进程间通信。:

了解了原理,来看看如何使用 MQ。MQ 的 API 操作与共享内存几乎是相同的,分为下面 4 个步骤:

1. 创建和访问 MQ

2. 发送消息

3. 接受消息

4. 删除 MQ 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值