进程间通信(IPC,InterProcess Communication)

1.管道(通常指无名管道)通过内核缓冲区实现数据传输

管道是半双工的,数据单相流动;若需双方通信,需建立两个管道

管道对管道两端进程而言,是一种单独的文件系统的文件,并且只存在在内存中

管道内部保证同步机制,保证访问数据的一致性;面向字节流;进程在管道在,进程消失对应的端口也关闭

无名管道/匿名管道(pipe),父子进程或者兄弟进程(具有亲缘关系的进程),因为其利用fork(),然后分别删除父进程的fd[0]和子进程的fd[1]

#include<unistd.h>

int pipe(int fd[2])//成功返回0,出错返回-1,fd参数返回两个文件描述符,fd[0]指向管道的读端,fd[1]指向管道的写端。fd[1]的输出是fd[0]的输入。

如果一个管道的写端一直在写,而读端引用计数(创建一个新对象,引用计数为1,新指针指向它,引用计数加1,不再指向,引用计数减一,当其为0,说明其不在被任何指针指向,可以销毁回收)大于0,只写不读再次调用write会导致管道堵塞; 
如果一个管道的读端一直在读,而写端引用计数大于0,只读不写再次调用read会导致管道堵塞; 
而当他们的引用计数等于0时,只写不读(fd[0]被关闭,fd[1]在写)会导致写端的进程收到一个SIGPIPE信号,导致进程终止,只读不写(fd[1]关闭,fd[0]在读)会导致read返回0,就像读到文件末尾一样。

管道的实质是一个内核缓冲区,进程以先进先出的方式从缓冲区存取数据:管道一端的进程顺序地将进程数据写入缓冲区,另一端的进程则顺序地读取数据,该缓冲区可以看做一个循环队列,读和写的位置都是自动增加的,一个数据只能被读一次,读出以后再缓冲区都不复存在了。当缓冲区读空或者写满时,有一定的规则控制相应的读进程或写进程是否进入等待队列,当空的缓冲区有新数据写入或满的缓冲区有数据读出时,就唤醒等待队列中的进程继续读写。


有名管道/命名管道(FIFO)(一种文件类型)有路径名与之关联,以一种特殊设备文件形式存在于文件系统,在磁盘上有对应节点,但没有数据块,只拥有一个名字和相应的访问权限,运行于同一台机器上的任何对FIFO有适当访问权的进程都可以通过文件名将其打开和进行读写,当不再被进程使用时,FIFO在内存中释放,但磁盘节点仍然存在。

#include <sys/stat.h>
 // 返回值:成功返回0,出错返回-1

 int mkfifo(const char *pathname, mode_t mode);//mode 参数与open函数中的 mode 相同,mknod是比较老的函数,建议用mkfifo

mkfifo函数只是创建一个FIFO文件,要使用命名管道还是将其打开(使用open函数)

注意:1、就是程序不能以O_RDWR模式打开FIFO文件进行读写操作,而其行为也未明确定义,因为如一个管道以读/写方式打开,进程就会读回自己的输出,同时我们通常使用FIFO只是为了单向的数据传递。
2、就是传递给open调用的是FIFO的路径名,而不是正常的文件。

2.消息队列

消息队列,是消息的链接表,存放在内核中,提供了从一个进程向另一个进程发送一个数据块的方法。一个消息队列由一个标识符(即队列ID)来标识。避免了命名管道的同步和阻塞问题,消息队列和命名管道一样每个数据块有一个最大长度的限制。

我们可以将每个数据块当作是一中消息类型(频道),发送和接收的内容就是这个类型(频道)对应的消息(节目),每个类型(频道)相当于一个独立的管道,相互之间互不影响。

1、特点
消息队列是面向记录的,其中的消息具有特定的格式以及特定的优先级。
消息队列独立于发送与接收进程。进程终止时,消息队列及其内容并不会被删除。

消息队列可以实现消息的随机查询,消息不一定要以先进先出的次序读取,也可以按消息的类型读取。

#include <sys/msg.h>

 int msgget(key_t key, int flag);// 创建或打开消息队列:成功返回队列ID,失败返回-1

 若没有与键值key相对应的消息队列,并且flag中包含了IPC_CREAT标志位或key参数为IPC_PRIVATE时,将创建消息队列;flag是标志位,设定权限

ftok()函数可以将文件名转换成键值

int msgsnd(int msqid, const void *ptr, size_t size, int flag);// 添加消息:成功返回0,失败返回-1

msqid 消息队列id;ptr 指向消息缓冲区的指针,此位置用来暂时存储发送和接收的消息,是一个用户可定义的通用结构,结构体包含type(long型)表示消息类型,必须大于0,mtext[1](char型),消息文本;size 消息(注意,是消息不是结构体)大小

int msgrcv(int msqid, void *ptr, size_t size, long type,int flag);// 读取消息:成功返回消息数据的长度,失败返回-1,将消息存储在ptr,flag控制函数行为,
type == 0,返回队列中的第一个消息;
type > 0,返回队列中消息类型为 type 的第一个消息;

type < 0,返回队列中消息类型值小于或等于 type 绝对值的消息,如果有多个,则取类型值最小的消息。type值非 0 时用于以非先进先出次序读消息。也可以把 type 看做优先级的权值 

int msgctl(int msqid, int cmd, struct msqid_ds *buf);// 控制消息队列:成功返回0,失败返回-1,对msqid消息队列进行cmd操作列,buf用来描述当前队列状态

3.信号量

信号量(semaphore)与管道、消息队列结构不同,它是一个计数器。信号量用于实现进程间的互斥与同步,而不是用于存储进程间通信数据。

p操作(wait):申请一个单位资源,进程进入,信号量--

v操作(signal):释放一个单位资源,进程进入,信号量++

1、特点
信号量用于进程间同步,若要在进程间传递数据需要结合共享内存
信号量基于操作系统的 PV 操作,程序对信号量的操作都是原子操作
每次对信号量的 PV 操作不仅限于对信号量值加 1 或减 1,而且可以加减任意正整数

支持信号量组。

最简单的信号量是只能取 0 和 1 的变量,这也是信号量最常见的一种形式,叫做二值信号量(Binary Semaphore)。而可以取多个正整数的信号量被称为通用信号量。

 #include <sys/sem.h>
int semget(key_t key, int num_sems(信号量个数,通常为1,如果引用现有集合,指定其为0), int sem_flags);// 创建或获取一个信号量组:若成功返回信号量集ID,失败返回-1

int semop(int semid(信号量集标识符), struct sembuf semoparray[], size_t numops(操作信号量结构体数组中元素个数));  // 对信号量组进行加减操作,改变信号量的值:成功返回0,失败返回-1
struct sembuf 
{
    short sem_num; // 信号量组中对应的序号,0~sem_nums-1

    short sem_op;  // 信号量值在一次操作中的改变量

                           //>0 信号量的值在原来的基础上加上此值;<0 如果信号量的值小于 semop 的绝对值,则挂起操作进程。如果信号量的值大于等于 semop 的绝对值,则信号量的值在原来的基础上减去 semop 的绝对值。=0 sem_op = 0:调用进程sleep(),直到信号量的值为0.

    short sem_flg; // 信号量的操作标识 IPC_NOWAIT(对信号量不能执行就立即返回), SEM_UNDO进程退出,撤销该进程对信号量的操作
 }
int semctl(int semid(信号量集标识符), int sem_num(信号量序号), int cmd, ...);// 控制信号量的相关信息

4.共享内存(两个或多个进程共享一个给定存储区)

1、特点
共享内存是最快的一种 IPC,因为进程是直接对内存进行存取。
因为多个进程可以同时操作,所以需要进行同步。

信号量+共享内存通常结合在一起使用,信号量用来同步对共享内存的访问。

#include <sys/shm.h>

// 创建或获取一个共享内存:成功返回共享内存ID,失败返回-1;创建必指定size,若为0则是引用一个已存在的共享内存,创建以后必须用shmat才能被进程访问

int shmget(key_t key, size_t size, int flag);
// 连接共享内存到当前进程的地址空间:成功返回指向共享内存的指针,失败返回-1
void *shmat(int shm_id, const void *addr, int flag);
// 断开进程与共享内存的连接:成功返回0,失败返回-1
int shmdt(void *addr); 
// 控制共享内存的相关信息:成功返回0,失败返回-1;cmd为IPC_RMId从系统中删除该共享内存

int shmctl(int shm_id, int cmd, struct shmid_ds *buf);

POSIX共享内存使用方法有以下两个步骤:
    a)通过shm_open创建或打开一个POSIX共享内存对象;
    b)然后调用mmap将它映射到当前进程的地址空间;
和通过
内存映射文件进行通信的使用上差别在于mmap描述符参数获取方式不一样:通过open(共享映射文件使用open)或shm_open。

5.套接字(socket) 用于不同机器间的进程通信

6.信号

信号是异步的(进程间通信机制中唯一的异步通信机制),是Linux中向进程发送的消息,接收到该信号的进程会相应地采取一些行动(类似于处理器收到一个中断),即通过软中断(软件层次上对中断机制的一种模拟)的方式来响应这个信号,触发一些事先指定或特定的事件。进程之间可以互相通过系统调用kill来发送信号,内核也可以因为内部事件而给进程发送信号,通知进程发生了某件事件

信号的产生:

 1,当用户按下某些按键时,产生信号.

2,硬件异常产生信号:除数为 0 ,无效的存储访问等等.这些情况通常由硬件检测到,将其通知内核, 然后内核产生适当的信号通知进程,例如,内核对正访问一个无效存储区的进程产生一个 SIGSEGV 信号.

3,进程用 kill 函数 将信号发送给另一个进程.

 4,用户可用 kill 命令将信号发送给其他进程.

6.1响应信号

进程可以通过三种方式来响应一个信号:(1)忽略信号,即对信号不做任何处理,其中,有两个信号不能忽略:SIGKILL及SIGSTOP;(2)捕捉信号。定义信号处理函数,当信号发生时,执行相应的处理函数;(3)执行缺省操作,Linux对每种信号都规定了默认操作。注意,进程对实时信号的缺省反应是进程终止

#include <signal.h>
void (*signal(int sig, void (*func)(int)))(int);//signal函数返回值和func一样,也是一个带有1个int类型参数且无返回值的函数指针。signal的函数作用是捕获到sig信号(都是非实时信号)以后,调用func指向的函数(信号处理函数);个人理解,sig和func函数的int是一个数值

func可用如下特殊值来代替信号处理函数(signal.h中定义,已SIG开头)

SIG_IGN 忽略信号,但是SIGKILL和SIGSTOP不能被忽略;SIGINT,从键盘上发出的结束信号,终端中断,相当于ctrl-c;SIG_DFL,恢复默认行为、重绑定;SIGKILL(不能被捕获,个人理解为不能作为signal函数的第一个变量,因为不能被捕获),终止进程;SIGTERM(可以被捕获),进程终止信号;SIGSEGV,非法内存段访问;SIGCHLD,标识子进程结束或停止的信号;SIGALRM


其中带*的信号,系统对该信号的响应视具体实现而定。如果进程接收到上表信号中的任何一个,但事先没有安排捕获它,进程将会立刻终止。

6.2发送信号

#include <sys/types.h>
#include <signal.h>

int kill(pid_t pid, int sig);//pid 发信号目标进程id,sig发送的信号值,成功返回0,失败返回-1,并设置errno变量

#include <unistd.h>
unsigned int alarm(unsigned int seconds);//seconds以后发送SIGALRM信号,置0取消已设置的闹钟请求
#include <unistd.h>

int pause(void);//程序挂起直到一个信号出现为止

【拓宽】linux命令

想要给一个进程发送信号,而该进程并不是当前的前台进程,就需要使用kill命令,它需要一个可选的信号代码,和一个进程ID,例如给PID为520的进程发送挂断信号(SIGHUP),使用如下命令(kill -HUP 520

kill [参数] [进程号]

如果没有指定信号代码或值,则默认情况下,采用编号为15的TERM信号。TERM信号将终止所有不能捕获该信号(TERM信号)的进程。对于那些可以捕获该信号的进程就要用编号为9的kill信号,强行“杀掉”该进程。


kill命令有一个变体,即killall,它可以给运行着某一命令的所有进程发送信号,不知道某进程的pid,或给执行相同命令的许多不同的进程发送信号,killall -HUP inetd (给运行inetd命令的进程发SIGHUP)

7.共享文件映射/共享存储映射mmap

利用fork后,父子进程共享文件描述符。即共享打开的文件

mmap建立进程空间到文件的映射,在建立的时候并不直接将文件拷贝到物理内存,同样采用缺页终端。mmap映射一个具体的文件可以实现任意进程间共享内存,映射一个匿名文件,可以实现父子进程间共享内存。

存储映射I/O (Memory-mapped I/O) 使一个磁盘文件与存储空间中的一个缓冲区相映射。于是当从缓冲区中取数据,就相当于读文件中的相应字节。于此类似,将数据存入缓冲区,则相应的字节就自动写入文件。这样,就可在不适用read和write函数的情况下,使用地址(指针)完成I/O操作。

使用这种方法,首先应通知内核,将一个指定文件映射到存储区域中。这个映射工作可以通过mmap函数来实现。

void *mmap(void *adrr, size_t length, int prot, int flags, int fd, off_t offset);

返回:成功:返回创建的映射区首地址;失败:MAP_FAILED宏

参数:
addr: 建立映射区的首地址,由Linux内核指定。使用时,直接传递NULL
length: 欲创建映射区的大小
prot: 映射区权限PROT_READ、PROT_WRITE、PROT_READ|PROT_WRITE
flags: 标志位参数(常用于设定更新物理区域、设置共享、创建匿名映射区)
   MAP_SHARED:  会将映射区所做的操作反映到物理设备(磁盘)上。
   MAP_PRIVATE: 映射区所做的修改不会反映到物理设备。
fd: 用来建立映射区的文件描述符,读取文件时的 fd=open("temp")
offset: 映射文件的偏移(4k的整数倍),从何处开始映射
munmap函数
同malloc函数申请内存空间类似的,mmap建立的映射区在使用结束后也应调用类似free的函数来释放。所以munmap释放申请的内存空间.在调用以后,才把内存中的相应内容写回磁盘文件

int munmap(void *addr, size_t length); 成功:0; 失败:-1

匿名映射

通过使用我们发现,使用映射区来完成文件读写操作十分方便,父子进程间通信也较容易。但缺陷是,每次创建映射区一定要依赖一个文件才能实现。通常为了建立映射区要open一个temp文件,创建好了再unlink、close掉,比较麻烦。 可以直接使用匿名映射来代替。其实Linux系统给我们提供了创建匿名映射区的方法,无需依赖一个文件即可创建映射区。同样需要借助标志位参数flags来指定,使用MAP_ANONYMOUS (或MAP_ANON),fd参数置位-1

实质上mmap是内核借助文件帮我们创建了一个映射区,多个进程之间利用该映射区完成数据传递。由于内核空间多进程共享,因此无血缘关系的进程间也可以使用mmap来完成通信。只要设置相应的标志位参数flags即可。若想实现共享,当然应该使用MAP_SHARED了。

总结

1.管道:速度慢,容量有限,只有父子进程能通讯    
2.FIFO:任何进程间都能通讯,但速度慢    
3.消息队列:容量受到系统限制,且要注意第一次读的时候,要考虑上一次没有读完数据的问题    
4.信号量:不能传递复杂消息,只能用来同步    
5.共享内存区:能够很容易控制容量,速度快,但要保持同步,比如一个进程在写的时候,另一个进程要注意读写的问题,相当于线程中的线程安全,当然,共享内存区同样可以用作线程间通讯,不过没这个必要,线程间本来就已经共享了同一进程内的一块内存

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值