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 }
写管道示例代码如下:
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 }
在创建完成管道文件a.pipe后,先后执行两代码,读管道程序会打印x=1到x=100。
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 }
写共享内存示例代码如下:
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 }
执行两示例需要注意的是,写示例需要先执行。如果读示例先执行,会导致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 }
进程并行示例代码如下:
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 }
首先执行信号量控制程序,再执行进程并发程序。可以发现首先会有5个进程执行,另外5个进程等待前5个进程结束后执行。
原文:https://www.cnblogs.com/Lioker/p/10950419.html