进程间通信
进程间通信概念
进程间通信(IPC,Interprocess communication)是一组编程接口,让程序员能够协调不同的进程,使之能在一个操作系统里同时运行,并相互传递、交换信息。这使得一个程序能够在同一时间里处理许多用户的要求。因为即使只有一个用户发出要求,也可能导致一个操作系统中多个进程的运行,进程之间必须互相通话。IPC接口就提供了这种可能性。每个IPC方法均有它自己的优点和局限性,一般,对于单个程序而言使用所有的IPC方法是不常见的。
进程间通信目的
- 数据传输:一个进程需要将它的数据发送给另一个进程,发送的数据量在一个字节到几兆字节之间。
- 共享数据:多个进程想要操作共享数据,一个进程对共享数据的修改,别的进程应该立刻看到。
- 通知事件:一个进程需要向另一个或一组进程发送消息,通知它(它们)发生了某种事件(如进程终止时要通知父进程)。
- 资源共享:多个进程之间共享同样的资源。为了作到这一点,需要内核提供锁和同步机制。
- 进程控制:有些进程希望完全控制另一个进程的执行(如Debug进程),此时控制进程希望能够拦截另一个进程的所有陷入和异常,并能够及时知道它的状态改变。
进程间通信方式
管道pipe
:管道是一种半双工的通信方式,数据只能单向流动,而且只能在具有亲缘关系的进程间使用。进程的亲缘关系通常是指父子进程关系;命名管道FIFO
:命名管道也是半双工的通信方式,但是它允许无亲缘关系进程间的通信;消息队列MessageQueue
:消息队列是由消息的链表,存放在内核中并由消息队列标识符标识。消息队列克服了信号传递信息少、管道只能承载无格式字节流以及缓冲区大小受限等缺点;共享内存SharedMemory
:共享内存就是映射一段能被其他进程所访问的内存,这段共享内存由一个进程创建,但多个进程都可以访问。共享内存是最快的 IPC 方式,它是针对其他进程间通信方式运行效率低而专门设计的。它往往与其他通信机制,如信号两,配合使用,来实现进程间的同步和通信;信号量Semaphore
:信号量是一个计数器,可以用来控制多个进程对共享资源的访问。它常作为一种锁机制,防止某进程正在访问共享资源时,其他进程也访问该资源。因此,主要作为进程间以及同一进程内不同线程之间的同步手段;套接字Socket
:套解口也是一种进程间通信机制,与其他通信机制不同的是,它可用于不同及其间的进程通信;- 信号 ( sinal ) : 信号是一种比较复杂的通信方式,用于通知接收进程某个事件已经发生。
管道
什么是管道?
- 管道是Unix中最古老的进程间通信的形式
- 我们把从一个进程连接到另一个进程的一个数据流称为一个“管道”
- 管道具有以下几个特点:
(1)管道是半双工的,数据只能想一个方向流动;需要双方通信时,需要建立起两个管道;
(2)匿名管道只能用于父子进程活兄弟进程之间(具有亲缘关系的进程);
(3)单独构成一个独立的文件系统:管道杜宇管道两端的进程而言,就是一个文件,但它不是普通文件,部不属于某种文件系统,而是自立门户,单独构成一个文件系统,并且只存在与内存中;
(4)管道分为pipe(匿名管道)和FIFO(命名管道)两种,除了建立,打开,删除的方式不同外,这两种管道几乎时一样的,他们都是通过内核缓冲区实现数据传输。 - 匿名管道:
用于相关进程间的通信,例如父进程和子进程,它通过pipe()系统调用来创建打开,当最后一个使用它的进程关闭对它的引用时,pipe将自动撤销。
原型:
#include <unistd.h>
int pipe(int fd[2]); // 返回值:若成功返回0,失败返回-1
pipe的例子:父进程创建管道,并在管道中写入数据,而子进程从管道中读出数据;
#include<stdio.h>
#include<unistd.h>
int main()
{
int fd[2]; // 两个文件描述符
pid_t pid;
char buff[20];
if(pipe(fd) < 0) // 创建管道
printf("Create Pipe Error!\n");
if((pid = fork()) < 0) // 创建子进程
printf("Fork Error!\n");
else if(pid > 0) // 父进程
{
close(fd[0]); // 关闭读端
write(fd[1], "hello world\n", 12);
}
else
{
close(fd[1]); // 关闭写端
read(fd[0], buff, 20);
printf("%s", buff);
}
return 0;
}
- 命名管道:
和匿名管道的主要区别
在于,命名管道有一个名字,命名管道的名字对应于一个磁盘引结点,任何进程有相应的权限都可以对它进行访问;而匿名管道却不同,进程只能访问自己或祖先创建的管道,而不能任意访问已经存在的管道,因为没有名字。
Linux中通过系统调用mknod或makefifo()来创建一个命名管道,最简单的方式就是直接用shell。- mkfifo myfifo 等价于 mknod myfifo p`;这是一个在当前目录下创建了一个名我myfifo的命名管道,用ls -p命名查看文件的类型时,可以看到命名管道对应的文件名后又一条竖线 ‘|’,表示该文件不是普通文件而是命名管道。
- 使用open()函数可以打开已经创建的命名管道,而匿名管道则不能用open()函数打开;当一个命名管道不在被任何进程打开时,它没有消失,还可以再次被打开,就像打开一个磁盘文件一样。
- 可以用删除普通文件的方法将其删除,实际删除的是磁盘上对应的结点信息。
原型:
#include <sys/stat.h>
int mkfifo(const char *pathname, mode_t mode); // 返回值:成功返回0,出错返回-1
例子:用命名管道实现server&client通信
serverPipe.c:
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdlib.h>
#define ERR_EXIT(m) \
do{\
perror(m);\
exit(EXIT_FAILURE);\
}while(0)
int main()
{
umask(0);
if (mkfifo("mypipe", 0644) < 0){
ERR_EXIT("mkfifo");
}
int rfd = open("mypipe", O_RDONLY);
if (rfd < 0){
ERR_EXIT("open");
}
char buf[1024];
while (1){
buf[0] = 0;
printf("Please wait...\n");
ssize_t s = read(rfd, buf, sizeof(buf)-1);
if (s > 0){
buf[s - 1] = 0;
printf("client say# %s\n", buf);
}
else if (s == 0){
printf("client quit, exit now!\n");
exit(EXIT_SUCCESS);
}
else{
ERR_EXIT("read");
}
}
close(rfd);
return 0;
}
clientPipe.c:
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#define ERR_EXIT(m) \
do{\
perror(m);\
exit(EXIT_FAILURE);\
}while(0)
int main()
{
int wfd = open("mypipe", O_WRONLY);
if (wfd < 0){
ERR_EXIT("open");
}
char buf[1024];
while (1){
buf[0] = 0;
printf("Please Enter# ");
fflush(stdout);
ssize_t s = read(0, buf, sizeof(buf)-1);
if (s > 0){
buf[s] = 0;
write(wfd, buf, strlen(buf));
}
else if (s <= 0){
ERR_EXIT("read");
}
}
close(wfd);
return 0;
}
Makefile:
消息队列
消息队列是消息的链表,存放在内核中;一个消息队列由一个标识符(即队列ID)来标识;
- 特点:
(1)消息队列是面向记录的,其中的消息具有特定的格式以及特定的优先级;
(2)消息队列独立于发送与接收数据;进程终止时,消息队列及其内容并不会被删除;
(3)消息队列可以实现消息的随机查询,消息不一定要已先进先出的次序读取,也可以按消息的类型读取; - 原型:
#include <sys/msg.h>
// 创建或打开消息队列:成功返回队列ID,失败返回-1
int msgget(key_t key, int flag);
// 添加消息:成功返回0,失败返回-1
int msgsnd(int msqid, const void *ptr, size_t size, int flag);
// 读取消息:成功返回消息数据的长度,失败返回-1
int msgrcv(int msqid, void *ptr, size_t size, long type,int flag);
// 控制消息队列:成功返回0,失败返回-1
int msgctl(int msqid, int cmd, struct msqid_ds *buf);
在以下两种情况下,msgget将创建一个新的消息队列:
- 如果没有与键值key相对应的消息队列,并且flag中包含了IPC_CREAT标志位。
- key参数为IPC_PRIVATE。
函数msgrcv在读取消息队列时,type参数有下面几种情况:
- type == 0,返回队列中的第一个消息;
- type > 0,返回队列中消息类型为 type 的第一个消息;
- type < 0,返回队列中消息类型值小于或等于 type 绝对值的消息,如果有多个,则取类型值最小的消息。
可以看出,type值非 0 时用于以非先进先出次序读消息。也可以把 type 看做优先级的权值。
例子:下面是一个简单的使用消息队列,服务端一直在等待特定类型的消息,当收到该类型的消息以后,发送另一个特定类型的消息作为反馈,客户端读取该反馈并打印出来。
msg_serve.c
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <sys/msg.h>
4
5 // 用于创建一个唯一的key
6 #define MSG_FILE "/etc/passwd"
7
8 // 消息结构
9 struct msg_form {
10 long mtype;
11 char mtext[256];
12 };
13
14 int main()
15 {
16 int msqid;
17 key_t key;
18 struct msg_form msg;
19
20 // 获取key值
21 if((key = ftok(MSG_FILE,'z')) < 0)
22 {
23 perror("ftok error");
24 exit(1);
25 }
26
27 // 打印key值
28 printf("Message Queue - Server key is: %d.\n", key);
29
30 // 创建消息队列
31 if ((msqid = msgget(key, IPC_CREAT|0777)) == -1)
32 {
33 perror("msgget error");
34 exit(1);
35 }
36
37 // 打印消息队列ID及进程ID
38 printf("My msqid is: %d.\n", msqid);
39 printf("My pid is: %d.\n", getpid());
40
41 // 循环读取消息
42 for(;;)
43 {
44 msgrcv(msqid, &msg, 256, 888, 0);// 返回类型为888的第一个消息
45 printf("Server: receive msg.mtext is: %s.\n", msg.mtext);
46 printf("Server: receive msg.mtype is: %d.\n", msg.mtype);
47
48 msg.mtype = 999; // 客户端接收的消息类型
49 sprintf(msg.mtext, "hello, I'm server %d", getpid());
50 msgsnd(msqid, &msg, sizeof(msg.mtext), 0);
51 }
52 return 0;
53 }
msg_client.c
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <sys/msg.h>
4
5 // 用于创建一个唯一的key
6 #define MSG_FILE "/etc/passwd"
7
8 // 消息结构
9 struct msg_form {
10 long mtype;
11 char mtext[256];
12 };
13
14 int main()
15 {
16 int msqid;
17 key_t key;
18 struct msg_form msg;
19
20 // 获取key值
21 if ((key = ftok(MSG_FILE, 'z')) < 0)
22 {
23 perror("ftok error");
24 exit(1);
25 }
26
27 // 打印key值
28 printf("Message Queue - Client key is: %d.\n", key);
29
30 // 打开消息队列
31 if ((msqid = msgget(key, IPC_CREAT|0777)) == -1)
32 {
33 perror("msgget error");
34 exit(1);
35 }
36
37 // 打印消息队列ID及进程ID
38 printf("My msqid is: %d.\n", msqid);
39 printf("My pid is: %d.\n", getpid());
40
41 // 添加消息,类型为888
42 msg.mtype = 888;
43 sprintf(msg.mtext, "hello, I'm client %d", getpid());
44 msgsnd(msqid, &msg, sizeof(msg.mtext), 0);
45
46 // 读取类型为777的消息
47 msgrcv(msqid, &msg, 256, 999, 0);
48 printf("Client: receive msg.mtext is: %s.\n", msg.mtext);
49 printf("Client: receive msg.mtype is: %d.\n", msg.mtype);
50 return 0;
51 }
共享内存
共享内存是进程间通信中最高效的一种方式,因为它不涉及进程之间的任何数据传输,但同样,这种高效仍然带来了问题,我们必须用其他手段来同步进程对共享内存的访问(因为共享内存是不带任何同步机制的),否则就会产生竞态条件。因此,共享内存通常是和其他进程间通信方式一起使用的。
-
共享内存的特点
1、使用灵活,可以是无关联的进程;
2、效率高:程序直接访问内存,而不需要任何的书库拷贝。对于像管道和消息队列
等通信方式,则需要在内核和用户空间进行四次的数据拷贝,而共享内存则只拷贝两次数据:一次从输入文件到共享内存区,另一次从共享内存区到输出文件。实际上,进程之间在共享内存时,并不总是读写少量数据后就解除映射,有新的通信时,再重新建立共享内存区域。而是保持共享区域,直到通信完毕为止,这样,数据内容一直保存在共享内存中,并没有写回文件。共享内存中的内容往往是在解除映射时才写回文件的。因此,采用共享内存的通信方式效率是非常高的。 -
创建共享内存
int shmget(key_t key, size_t size, int shmflg);
功能:创建一段新的共享内存,或者获取一段已经存在的共享内存。
返回值:成功时翻译一个正整数,即共享内存的标识符。失败时返回-1并设置errno。
参数:
- key:是一个键值,用来标识一段全局唯一的共享内存。
- size:指定共享内存的大小,单位是字节。如果是新创建的共享内存,则size值必须被指定。如果是获取已经存在的共享内存,则可以把size设置为0.
- shmflg:该参数与semget系统调用的sem_flags参数相同。
- 挂接 & 去挂接
共享内存获取之后我们不能立即访问,而是先将它关联到进程的地址空间;使用完成后也需要将其从进程的地址空间上分离。
void *shmat(int shmid, const void *shmaddr, int shmflg); //挂接/关联
int shmdt(const void *shmaddr); //去挂接/去关联
-
shmid参数是由shmget调用返回的共享内存标识符。
-
shmaddr参数指定将共享内存关联到进程地址空间的哪块,最终效果还受到shmflg参数中可选标志的SHM_RND的影响。
shmaddr为NULL:被关联的地址由操作系统指定。(推荐这种做法,以确保代码的可移植性);
shmaddr为非NULL,且SHM_RND标志未被设置:共享内存被关联到指定的地址处。 -
shmat的返回值:成功时返回共享内存被关联的地址,失败返回-1并设置errno。
shmdt函数是将共享内存从进程地址空间中分离。成功时返回0,失败时返回-1并设置errno。 -
控制共享内存的属性
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
- cmd参数指定要执行的命令。
shmctl所支持的cmd选项:
IPC_STAT 取该共享内存的shmid_ds结构,并存在第三个参数中
IPC_SET 使用buf指定的结构设置相关属性
IPC_RMID 删除指定共享内存,只有当buf中的shm_nattch值为0时才真正删除
IPC_LOCK 在内存中对共享内存加锁(超级用户权限)
IPC_UNLOCK 解锁共享内存(超级用户权限)
返回值:成功时的返回值取决与cmd参数。失败范湖-1并设置errno。
信号量
- 什么是信号量
信号量的主要作用是为了保护临街资源,保证了在任意一个时刻内只有一个进程能进入临界区对资源进行操作。也就是说,信号量是用来协调进程对共享资源的访问的。 - 工作原理
信号量的本质是一种数据操作锁。其只能进行两种操作:等待和发送信号,即P、V操作。假设有信号量sv,
P:如果信号量的值大于0,就将其减1;如果sv为0,就将进程挂起;—–释放资源
V:如果有其他进程因为等待该信号量而挂起,则该操作是将其唤醒;如果没有,就将sv加1;—–申请资源
举个例子:假如有进程A、B共享信号量sv,当A进程执行了P操作后,就可以获得信号量sv并进入临界区,并将sv的值减1;当B进程来访问临界区时,其试图进行P操作,但是此时sv为0,则B进程就会被挂起等待直到A进程离开临界区并执行了V操作后,B进程被唤醒,然后就可以回复执行了。
-
特点
1、保护临界资源、协同进程;
2、有公共、私有接口;
3、生命周期随系统;
4、创建、初始化不是原子操作,创建与初始化分开。 -
系统调用
主要有三个:semget,semop,semctl,都被设计为操作一组信号量,即信号量集。
【创建一个新的信号量集】
int semget(key_t key, int nsems, int semflg);
返回值:成功返回一个正整数,是信号量集的标识符;失败返回-1,并设置errno。
参数:
- key:标识一个全局唯一的信号量集。要通过信号量进行通信的进程需要使用相同的键值来创建/获取信号量集。
- nsems:指定要创建/获取的信号量集中信号量的数目。如果是创建,则改值必须被指定;若是获取,则可将其设置为0.
- semflg:指定一组标志。低9位是信号量的权限。可以与IPC_CREATE按位“或”以创建新的信号量集。即使这个信号量集已经存在也不会产生错误。创建一组新的、唯一的信号量集—IPC_CREATE 和 IPC_EXCL,如果信号量集已经存在,则semget返回错误并设置errno为EEXIST。
【对信号量集进行操作】
int semop(int semid, struct sembuf *sops, unsigned nsops);
返回值:成功返回0,失败返回-1,且sem_ops数组中指定的所有操作都不被执行。
参数:
- semid:semget返回的信号量集标识符,用以指定被操作的目标信号量集。
- sops:指向一个sembuf结构体。
- sembuf结构体:unsigned short sem_num; 信号量集中信号量的编号
- short sem_op; 指定操作类型(正数、0、负数)
- short sem_flg; IPC_NOWAIT/IPC_UNDO
- nsops:指定要执行的操作个数,即sops中元素的个数。semop对数组中的每个成员依次执行操作,且该过程是原子的。
【对信号量集进行控制】
int semctl(int semid, int semnum, int cmd, ...);
返回值:成功时的参数取决于cmd参数;失败返回-1且设置errno
- semid:由semget调用返回的信号量集标识符。
- semnum:指定被操作的信号量在信号量集中的编号。(从0开始访问)
- cmd:指定要执行的指令。(IPC_RMID:立即移除信号量集,唤醒所有等待该信号量集的所有进程)