第十五章:进程间通信

Unix系统早期一般使用多进程解决问题。有时多个进程之间需要交互数据,而进程和进程之间不能直接交互数据,因此引入了进程间通信(IPC)。

IPC主要包括以下方式:

1. 文件I/O

2. 信号

3. 管道

4. 共享内存

5. 消息队列

6. 信号量集

7. 网络socket

其中,共享内存、消息队列和信号量集都遵循XSI IPC,因此三者编程中有很多共性。管道目前很少使用。

一、管道

管道(FIFO或PIPE)之所以目前很少使用,是由于他有两点局限性:

1. 一般为半双工(即同一时刻,数据只能在一个方向流动,也就是不能同时传输)。

2. 管道只能在两个有共同祖先进程的两个进程之间使用。

管道分为有名管道(有文件名)和无名管道(没有名字)。有名管道就是自己创建管道文件,然后进行交互。无名管道就是系统帮我们创建管道文件,利用系统的管道文件进行交互。

也就是有名管道适用于所有的进程的通信,无名管道只适用于fork()创建的父子进程之间的通信。

管道也是一个文件,后缀名为.pipe。我们可以使用mkfifo命令或mkfifio()函数创建一个有名管道文件:

$ mkfifo a.pipe

mkfifo函数定义如下:

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

int mkfifo(const char * pathname,mode_t mode);

/* 示例 */
mkfifo("a.pipe", 0777);    /* 成功返回0,失败返回-1,错误代码存在errno中 */

读管道示例代码如下: 

 1 #include <stdio.h>
 2 #include <stdlib.h>
 3 #include <unistd.h>
 4 #include <fcntl.h>
 5 
 6 int main()
 7 {
 8     int fd = open("a.pipe", O_RDONLY);
 9     if (fd == -1)
10         perror("open"), exit(-1);
11 
12     int x = 0;
13     while (1) {
14         int res = read(fd, &x, sizeof(x));
15         if (res == -1) {
16             perror("read");
17             close(fd);
18             exit(-1);
19         }
20         if (!res) break;
21         printf("x=%d\n", x);
22     }
23     close(fd);
24 
25     return 0;
26 }
View Code

写管道示例代码如下:

 1 #include <stdio.h>
 2 #include <stdlib.h>
 3 #include <unistd.h>
 4 #include <fcntl.h>
 5 
 6 int main()
 7 {
 8 //    int fd = open("a.pipe", O_RDWR);    // 读写管道
 9     int fd = open("a.pipe", O_WRONLY);
10     if (fd == -1)
11         perror("open"), exit(-1);
12     
13     int x;
14     for (x = 0; x < 100; x++) {
15         int res = write(fd, &x, 4);
16         if (res == -1) {
17             perror("write");
18             close(fd);
19             exit(-1);
20         }
21     }
22     close(fd);
23 
24     return 0;
25 }
View Code

在创建完成管道文件a.pipe后,先后执行两代码,读管道程序会打印x=1到x=100。

二、XSI IPC

XSI IPC的固定步骤:

1. 创建或获取IPC结构之前,必须提供一个外部提供的key

2. 每个IPC结构都有一个唯一的ID与之对应,使用key可以获取ID

3. 外部key的类型是key_t,获得key的方式有以下三种:

a. 使用宏IPC_PRIVATE做key。这种方式一般不使用,因为这个key只能创建,不能获取

b. 使用ftok()函数创建key

c. 在公共的头文件中定义每个IPC结构使用的key,key本身是一个整数

4. xxxget()函数可以使用key创建或获得ID,比如:

shmget()/msgget()

5. 使用xxxget()函数新建IPC结构,参数flag一般为IPC_CREAT | 权限

6. 每种IPC结构都提供了一个xxxctl()函数,此函数可以修改、删除和查询IPC结构

7. xxxctl()函数中,cmd支持以下宏:

IPC_STAT:用于查询

IPC_SET:用于修改权限

IPC_RMID:用于删除

由上分析,ftok()是XSI IPC通用代码,其函数定义如下:

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

key_t ftok(const char *pathname, int proj_id);

函数参数以及返回值:

pathname:路径名,真实存在即可

proj_id:项目ID,就是0到255的任一整数

返回值:成功返回key;出错返回(key_t)-1。


三、共享内存

共享内存允许两个或多个进程共享一个给定的存储区。因为数据不需要再客户进程和服务器进程之间复制,所以这是最快的一种IPC。

其类函数定义如下:

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

/* 1. 创建共享内存ID */
int shmget(key_t key, size_t size, int flag);  /* size表示新建的共享内存大小,只获取共享内存时指定为0 */

/* 2. 映射共享内存 */
void *shmat(int shmid, const void *shmaddr, int shmflg);  
             /* shmaddr,一般设为NULL */
              /* shmflg定义为SHM_RDONLY为只读模式,其它为读写模式
*/ /* 3. 数据交互 */ /* 4. 解除映射 */ int shmdt(const void *shmaddr); /* 5. 删除共享内存,buf用于存储共享内存信息 */ int shmctl(int shmid, int cmd, struct shmid_ds *buf);

读共享内存示例代码如下: 

 1 #include <stdio.h>
 2 #include <stdlib.h>
 3 #include <sys/ipc.h>
 4 #include <sys/shm.h>
 5 
 6 int main()
 7 {
 8     key_t key = ftok(".", 100);
 9     if (key == -1)
10         perror("ftok"), exit(-1);
11     
12     /* 获取共享内存,size和flag指定为0 */
13     int shmid = shmget(key, 0, 0);
14     if (shmid == -1)
15         perror("shmget"), exit(-1);
16     
17     /* 读取共享内存数据 */
18     int* p = shmat(shmid, 0, 0);
19     printf("*p=%d\n", *p);
20     
21     shmdt(p);
22 
23     return 0;
24 }
View Code

写共享内存示例代码如下:

 1 #include <stdio.h>
 2 #include <stdlib.h>
 3 #include <unistd.h>
 4 #include <sys/ipc.h>
 5 #include <sys/shm.h>
 6 
 7 int main()
 8 {
 9     key_t key = ftok(".", 100);
10     if (key == -1)
11         perror("ftok"), exit(-1);
12     
13     int shmid = shmget(key, 4, IPC_CREAT | 0666);
14     if (shmid == -1)
15         perror("shmget"), exit(-1);
16 
17     void* p = shmat(shmid, 0, 0);            // 读写模式
18     int* pi = p;
19     *pi = 10;
20     shmdt(p);
21     
22     sleep(10);
23     
24     struct shmid_ds ds;
25     shmctl(shmid, IPC_STAT, &ds);            // 查询
26     printf("shmid=%d\n", shmid);            // id
27     printf("size=%d\n", ds.shm_segsz);        // 大小 
28     printf("nattch=%d\n", ds.shm_nattch);    // 权限
29     printf("mode=%d\n", ds.shm_perm.mode);    // 挂接数
30 
31     ds.shm_segsz = 400;                        // 不能改
32     ds.shm_perm.mode = 0644;                // 能改
33     shmctl(shmid, IPC_SET, &ds);            // 修改,只能修改权限
34     shmctl(shmid, IPC_RMID, 0);                // 删除
35 
36     return 0;
37 }
View Code

执行两示例需要注意的是,写示例需要先执行。如果读示例先执行,会导致shmget()函数出错。

共享内存的缺点是若多个进程同时写,会导致读出的数据完全混乱。

四、消息队列

相比共享内存,消息队列设计更加的合理。消息队列也是采用内存做交互媒介,它先把数据传入消息中,再把消息存入队列。也就是从队列中取出或放入数据。

为了克服共享内存的缺点,消息队列引入了无类型消息和有类型消息。有类型消息需要定义为结构体,示例如下:

struct mymsg               // 结构名称可以自定义
{
    long mtype;             // 消息类型,结构体的第一个成员必须是它
    char mtext[512];    // 数据区,支持任意类型的数据
};

消息队列类函数定义如下: 

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

/* 1. 创建消息队列ID */
int msgget(key_t key, int flag);

/* 2. 数据交互 */
int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);

ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp, int msgflg);

/* 3. 删除消息队列 */
int msgctl(int msqid, int cmd, struct msqid_ds *buf);

msgsnd()和msgrcv()函数参数以及返回值:

msqid:消息队列ID

msgp:消息结构体指针

msgsz:消息结构体的数据部分大小

msgtyp:指定消息类型

> 0,接受特定类型的消息

= 0,接受任意类型的消息

< 0,接受类型小于等于msgtyp绝对值的消息,接收顺序按消息结构体中mtype的数值从小到大

msgflg:0表示阻塞,IPC_NOWAIT非阻塞

返回值:msgsnd()成功返回0;出错返回-1。msgrcv()成功返回消息结构体的数据部分大小;出错返回-1。


读消息队列示例代码如下:

 1 #include <stdio.h>
 2 #include <stdlib.h>
 3 #include <sys/ipc.h>
 4 #include <sys/msg.h>
 5 #include <string.h>
 6 
 7 struct Msg
 8 {
 9     long mtype;
10     char buf[256];
11 };
12 
13 int main()
14 {
15     key_t key = ftok(".", 100);
16     
17     int msgid = msgget(key, 0);
18     if (msgid == -1)
19         perror("msgget"), exit(-1);
20 
21     struct Msg msg;
22 
23     int res = msgrcv(msgid, &msg, sizeof(msg.buf), -2, 0);
24     
25     if (res == -1)
26         perror("msgrcv"), exit(-1);
27     
28     else
29         printf("%s\n", msg.buf);
30 
31     return 0;
32 }

写消息队列示例代码如下:

 1 #include <stdio.h>
 2 #include <stdlib.h>
 3 #include <sys/ipc.h>
 4 #include <sys/msg.h>
 5 #include <string.h>
 6 
 7 struct Msg
 8 {
 9     long mtype;
10     char buf[256];
11 };
12 
13 int main()
14 {
15     key_t key = ftok(".", 100);
16     
17     int msgid = msgget(key, IPC_CREAT | 0666);
18     if (msgid == -1)
19         perror("msgget"), exit(-1);
20     
21     struct Msg msg1, msg2;
22     msg1.mtype = 1;
23     strcpy(msg1.buf, "Hello ");
24     
25     msg2.mtype = 2;
26     strcpy(msg2.buf, "World");
27 
28     msgsnd(msgid, &msg1, sizeof(msg1.buf), 0);
29     msgsnd(msgid, &msg2, sizeof(msg2.buf), 0);
30 
31     return 0;
32 }

五、信号量

信号量是一个计数器,负责控制访问共享资源的最大并行进程总数。

信号量初始时设为最大值,每进入一个进程计数器-1,每离开一个进程计数器+1,当计数器到0时进程阻塞,直到计数器重新大于0则解除阻塞。

如果有多个共享资源需要控制最大并行进程数,则需要多个信号量。信号量集用于存储多个信号量。IPC拿到的是信号量集,而不是单一的信号量。

信号量集类函数定义如下:

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

/* 1. 创建信号量集ID */
int semget(key_t key, int nsems, int flag);

/* 2. 为每个信号量设置初始的最大值 */
int semctl(int semid, int semnum, int cmd, ...);

/* 3. 计数 +1 或 -1 */
int semop(int semid, struct sembuf *sops, size_t nsops);

/* 4. 删除信号量集 */
int semctl(int semid, int semnum, int cmd, ...);

semop()函数的sops定义如下:

struct sembuf
{
    unsigned short sem_num;    // 操作信号量的下标
    short sem_op;    // -1代表计数减1,1代表加1
    short sem_flg;    // 0代表阻塞,IPC_NOWAIT非阻塞
};

信号量集一般用于控制某类事务的数量。在此以控制子进程并发个数为例。

信号量控制示例代码如下:

 1 #include <stdio.h>
 2 #include <stdlib.h>
 3 #include <sys/ipc.h>
 4 #include <sys/sem.h>
 5 #include <signal.h>
 6 
 7 int semid;
 8 
 9 void sig_exit(int signo)
10 {
11     printf("准备删除信号量集\n");
12     semctl(semid, 0, IPC_RMID, NULL);
13     exit(0);
14 }
15 
16 int main()
17 {
18     printf("用Ctrl + C退出\n");
19     key_t key = ftok(".", 100);
20     
21     semid = semget(key, 1, IPC_CREAT | 0666);    // 1个元素
22     if (semid == -1)
23         perror("semget"),exit(-1);
24 
25     int res = semctl(semid, 0, SETVAL, 5);        // 最多支持5个并行进程
26     if (!res)
27         printf("信号量集成功创建\n");
28 
29     signal(SIGINT, sig_exit);
30 
31     while(1);
32 
33     return 0;
34 }
View Code

进程并行示例代码如下:

 1 #include <stdio.h>
 2 #include <stdlib.h>
 3 #include <sys/ipc.h>
 4 #include <sys/sem.h>
 5 #include <unistd.h>
 6 
 7 int main()
 8 {
 9     key_t key = ftok(".", 100);
10     
11     int semid = semget(key, 0, 0);    // 获取不新建
12     if (semid == -1)
13         perror("semget"),exit(-1);
14 
15     int i;
16     for (i = 0; i < 10; i++) {
17         pid_t pid = fork();
18         if (!pid) {
19             printf("申请一个计数\n");
20             
21             struct sembuf buf;
22             buf.sem_num = 0;
23             buf.sem_op = -1;        // 计数 -1
24             buf.sem_flg = 0;        // 阻塞
25             
26             semop(semid, &buf, 1);    // 数组就是首元素的地址
27             printf("申请成功\n");
28             
29             sleep(5);
30             
31             buf.sem_op = 1;            // 计数 +1
32             printf("释放一个计数\n");
33             
34             semop(semid, &buf, 1);    // 数组就是首元素的地址
35             exit(0);
36         }
37     }
38 }
View Code

首先执行信号量控制程序,再执行进程并发程序。可以发现首先会有5个进程执行,另外5个进程等待前5个进程结束后执行。

猜你喜欢

转载自www.cnblogs.com/Lioker/p/10950419.html