Linux系统学习——进程间的通信方式
进程间的通信又称为IPC,指不同的进程间传送消息和交换消息
IPC的方式通常有管道(包括无名管道和命名管道)、消息队列、信号量、共享存储、Socket、Streams等。其中Socket和Streams支持不同主机上的两个进程IPC。
一、管道
管道通常指的是无名管道
1.特点
- 无名管道类似于一条水管,水只能从一端流入,另一端流出,而水管内部不储存水,具有固定的读端和写端
- 它只能用于具有亲缘关系的进程之间的通信(也是父子进程或者兄弟进程之间)
- 它可以看成是一种特殊的文件,对于它的读写也可以使用普通的read、write 等函数。但是它不是普通的文件,并不属于其他任何文件系统,并且只存在于内存中
2.函数原型
#include <unistd.h>
int pipe(int pipefd[2]);
返回值为:-1即为创建管道失败,0为创建管道成功
当一个管道建立时,会生成两个文件描述符,即fd[0]为读取而打开管道,fd[1]为写入而打开管道,要关闭管道,只要把两个文件描述符用close关闭即可
3.例子
数据想要从父进程流向子进程,只需要关闭在父进程上读端( fd[0] ) 和子进程上的写端 ( fd[1] ),反之从子进程流向父进程,操作相反即可
代码演示:
1 #include<stdio.h>
2 #include <unistd.h>
3 #include<string.h>
4 #include<stdlib.h>
5
6 int main()
7 {
8 int fd[2]; //创建两个文件描述符
9 int pid;
10 char buf[128];
11
12 if(pipe(fd) == -1) // 检测管道是否创建失败
13 {
14 printf("open guan dao failed\n");
15 }
16
17 pid = fork(); // 创建子进程
18
19 if(pid < 0)
20 {
21 printf("open jin cheng error\n");
22 }
23 else if(pid >0) //数据在父进程上流向子进程
24 {
25 sleep(3);
26 printf("This is father jincheng\n");
27 close(fd[0]); //关闭读入端
28 write(fd[1],"liuzheng hen shuai",strlen("liuzheng hen shuai")); //打开写入端
29 wait();
30 }
31 else{
32 printf("This is child jincheng\n");
33 close(fd[1]); //关闭写端
34 read(fd[0],buf,128); //打开读端
35
36 printf("read father :%s\n",buf);
37 exit(0);
38 }
39 return 0;
40 }
二、FIFO(命名管道)
FIFO,称为命名管道,是一种文件类型
1、特点
1. FIFO可以在无关的进程之间交换数据,与无名管道不同。
2. FIFO有路径名与之关联,它以一种特殊设备文件形式存在于文件系统中。
3. 在数据读出时,FIFO管道中同时清除数据,并且"先进先出"。
2.函数原型
#include <sys/types.h>
#include <sys/stat.h>
int mkfifo(const char *pathname, mode_t mode);
返回值:创建成功返回 0 ,失败返回 -1,可通过 perror() 打印出 失败原因
其中
1、pathname :文件命名路径
2、 mode参数 和 open函数中的 mode参数相同, 指可以用一般文件I/O函数操作它(0600:可读可写、0777: 可读可写可执行)
特别注意
当 open 一个FIFO时,是否设置非阻塞标志(O_NONBLOCK)的区别:
-
若没有指定O_NONBLOCK(默认设置),只读 open 要阻塞到某个其他进程为写而打开此 FIFO(就是你在这边进程读open,此时是阻塞情况不会有数据出现,需要使用另一个进程去写write才能打开这个阻塞)。类似的,只写 open 要阻塞到某个其他进程为读而打开它
-
若是指定了O_NONBLOCK下,若是只读open,会立刻输出数据,但该数据跟我们设定的可能会不一样,需要在另一端打开写write进程,才能得到我们预先设定的数据结果
3.例子演示
read.c
1 #include<stdio.h>
2 #include <sys/types.h>
3 #include <sys/stat.h>
4 #include <fcntl.h>
5 #include<errno.h>
6
7 int main()
8 {
9 char buf[30] = {0};
10
11 int nread = 0;
12 int cnt = 0;
13
14 if( (mkfifo("./file1",0600)==-1) && errno != EEXIST) //创建fifo ,EEXIST 检查 文件是否已经存在
15 {
16 printf("create file1 failed\n");
17 perror("why:"); //创建fifo失败,则打出错误原因
18 }
19 // O_RDONLY 只读打开文件
20 int fd = open("./file1",O_RDONLY|O_NONBLOCK); // 设置O_NONBLOCK,只要open读打开,会立马获取数据
21
22 printf("open success!\n");
23
24 while(1)
25 {
26 nread = read(fd,buf,30);
27 sleep(2);
28
29 printf("read %d from fifo , context fifo is %s\n",nread,buf);
30
31 cnt++;
32 if(cnt == 10)
33 {
34 break;
35 }
36 }
37 close(fd); //关闭文件
38 return 0;
write.c
1 #include<stdio.h>
2 #include <sys/types.h>
3 #include <sys/stat.h>
4 #include <fcntl.h>
5 #include<string.h>
6 int main()
7 {
8 int cnt = 0;
9 char *str = "Liuzheng from fifo";
10
11 int fd = open("./file1",O_WRONLY); //O_WRNOLY 文件只写打开
12
13 printf("write open success!\n");
14 while(1)
15 {
16 write(fd,str,strlen(str)); //写入数据,流向open端,在open读那边打开
17 sleep(2);
18 cnt++;
19 if(cnt == 10)
20 {
21 break;
22 }
23
24 }
25 close(fd);
26 return 0;
27 }
运行结果:
(1)设置了O_NONBLOCK情况下:
没有打开write端之前:
open端由于设置了O_NONBLOCK 没有阻塞
打开write端后
打开write
三、消息队列
消息队列,是消息的链接表,存放在内核中。一个消息队列由一个标识符 (即队列ID) 来标识
1.特点:
1. 消息队列是面向记录的,其中的消息具有特定的格式以及特定的优先级。
2. 消息队列独立于发送与接收进程。进程终止时,消息队列及其内容并不会被删除
3. 消息队列可以实现消息的随机查询,消息不一定要以先进先出的次序读取,也可以按消息的类型读取
2.函数原型:
//头文件
#include <sys/msg.h>
#include <sys/types.h>
#include <sys/ipc.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);
msqid : 队列id
flag :文件权限(0777:可读可写可执行)
msgrcv、msgsnd 中 flag :默认为0,没有对应类型的数据就会阻塞在此
cmd :一般防止消息队列一直在内核堆积,使用 IPC_RMID 去移除队列
3.ftok函数:
- 系统建立IPC通讯 (消息队列、信号量和共享内存) 时必须指定一个ID值。通常情况下,该ID值通过 ftok 函数得到
(1)头文件
#include <sys/types.h>
#include <sys/ipc.h>
(2)函数原型
key_t ftok( const char * fname, int id )
// fname就是你指定的文件名(已经存在的文件名),一般使用当前目录
//如:
key_t key;
key = ftok(".", 1); // 这样就是将fname设为当前目录
服务端程序一直在等待特定类型的消息,当收到该类型的消息以后,发送另一种特定类型的消息作为反馈,客户端读取该反馈并打印出来。
4.代码演示:
msgGet.c
1 #include<stdio.h>
2 #include <sys/types.h>
3 #include <sys/ipc.h>
4 #include <sys/msg.h>
5 #include<string.h>
6
7 struct massage{ //消息队列的结构
8
9 long mtype;
10 char mtext[127];
11
12 };
13
14 int main()
15 {
16 struct massage readbuf;
17
18 key_t key;
19 key = ftok(".",'z'); //创建队列ID
20
21 printf("key = %x\n",key);
22
23 int msgID = msgget(key,IPC_CREAT|0777); //创建或者打开消息队列
24 if(msgID == -1)
25 {
26 printf("The msg failed\n");
27 }
28
29 //读取另一端消息
30 msgrcv(msgID,&readbuf,sizeof(readbuf.mtext),666,0); //读取消息
31 printf("read from Sent %s\n",readbuf.mtext);
32
33 //添写消息给另一端输出
34 struct massage sentbuf = {998,"The massage from Sent"};
35
36 msgsnd(msgID,&sentbuf,strlen(sentbuf.mtext),0); //添加消息
37
38
39 msgctl(msgID,IPC_RMID,NULL); //移除已经存在内核中的消息队列,防止堆积
40 return 0;
41 }
msgSent.c
1 #include<stdio.h>
2 #include <sys/types.h>
3 #include <sys/ipc.h>
4 #include <sys/msg.h>
5 #include<string.h>
6
7 struct massage{ //消息队列的结构
8
9 long mtype;
10 char mtext[127];
11
12 };
13
14 int main()
15 {
16
17 struct massage sentbuf = {666,"this is massage from get"};
18
19 key_t key;
20 key = ftok(".",'z'); //获取队列ID号
21
22 printf("key = %x\n",key);
23
24 int msgID = msgget(key,IPC_CREAT|0777); // 创建 / 打开消息队列
25 if(msgID == -1)
26 {
27 printf("The msg failed\n");
28 }
29 // 添加消息到 另一端 666 内容
30 msgsnd(msgID,&sentbuf,strlen(sentbuf.mtext),0); //添加消息
31 printf("Sent success\n");
32
33 // 读取另一端添加进 998的内容
34 struct massage readbuf;
35 msgrcv(msgID,&readbuf,sizeof(readbuf.mtext),998,0); //读取消息
36 printf("return from get %s\n",readbuf.mtext);
37
38 msgctl(msgID,IPC_RMID,NULL); //移除已经存在内核中的消息队列
39
40 return 0;
41 }
运行展示:
四、共享内存
可以进行双向传输。顾名思义,就是申请一块特殊的内存空间,挂载在两个进程上,使两个进程都能对这块空间进行操作。(这块空间是独立与两个进程的物理内存上的)。
共享内存方式是指: 两个或者多个进程,在同一块内存区域共享信息操作
思路:
1、创建共享内存
2、将共享内存映射到创建的进程
3、读写数据到共享内存
4、释放共享内存的空间
5、干掉共享内存
1.原型:
shmget 函数
flag :
- 有IPC_CREAT 、IPC_EXCL 、SHM_HUGETLB、SHM_NORESERVE这几个宏定义,最常用的是IPC_CREAT即创建共享内存。其他的宏定义有其他的应用场景作为初学者学会IPC_CREAT就可以了
- size:申请空间的大小,注意必须以M为单位
#include <sys/shm.h>
#include <sys/ipc.h>
// 创建或获取一个共享内存:成功返回共享内存ID,失败返回-1
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
int shmctl(int shm_id, int cmd, struct shmid_ds *buf);
shmat函数参数:
shmid 即为申请的共享内存id。
shmaddr 如果设置为0将会由内核自动分配地址(推荐使用)
flg 如果配置为SHM_RDONLY将会设置为只读,否则会设置为读写
shmdt函数:
参数为共享内存的地址。作用是解除挂解进程,释放共享内存空间。
shmctl函数:
参数解释:
shmid 为共享内存的id号
cmd 为命令,在解除挂载的时候使用IPC_RMID就好了。
buf 用于存放各种信息,不关心这些信息的话直接写0或者NULL就好
2.代码例子:
shmw.c
1 #include <sys/ipc.h>
2 #include <sys/shm.h>
3 #include<stdio.h>
4 #include <sys/types.h>
5 #include <unistd.h>
6 #include<string.h>
7 #include<stdlib.h>
8 // int shmget(key_t key, size_t size, int shmflg);
9 // int shmdt(const void *shmaddr);
10 // int shmctl(int shmid, int cmd, struct shmid_ds *buf);
11 // void *shmat(int shmid, const void *shmaddr, int shmflg);
12 int main()
13 {
14
15 char *shmdr;
16 int shmid;
17 key_t key;
18 key = ftok(".",1);
19
20 shmid = shmget(key,1024*4,IPC_CREAT|0666); //创建一个共享内存ID号,申请共享内存,如果不存在就创建并权限为可读可写
21 if(shmid == -1)
22 {
23 printf("create error\n");
24 exit(-1);
25 }
26
27
28
29 shmdr = shmat(shmid,0,0); // 映射共享内存数据到 各自内存上
30 printf("shmat success\n");
31
32 strcpy(shmdr,"longchuangchenyixun"); //通过共享内存空间,数据交换
33 sleep(5); // 等待 5s 另一个进程读取
34 shmdt(shmdr); // 释放共享内存空间
35 shmctl(shmid,IPC_RMID ,0); // 干掉共享内存
36 printf("stop\n");
37 return 0;
38 }
shmr.c
1 #include <sys/ipc.h>
2 #include <sys/shm.h>
3 #include<stdio.h>
4 #include <sys/types.h>
5 #include <unistd.h>
6 #include<string.h>
7 #include<stdlib.h>
8 // int shmget(key_t key, size_t size, int shmflg);
9 // int shmdt(const void *shmaddr);
10 // int shmctl(int shmid, int cmd, struct shmid_ds *buf);
11 // void *shmat(int shmid, const void *shmaddr, int shmflg);
12
13
14
15 int main()
16 {
17
18 char *shmdr;
19 int shmid;
20 key_t key;
21 key = ftok(".",1);
22
23 shmid = shmget(key,1024*4,0); //创建一个共享内存ID号
24 if(shmid == -1)
25 {
26 printf("create error\n");
27 exit(-1);
28 }
29
30 shmdr = shmat(shmid,0,0); // 映射共享内存数据到 各自内存上
31 printf("shmat ok\n");
32 printf("data : %s\n",shmdr); //读取共享内存上的数据并输出
33
34 shmdt(shmdr); //释放共享内存空间
35
36 printf("stop\n");
37 return 0;
38 }
五、信号量
信号量(semaphore)与已经介绍过的 IPC 结构不同,它是一个计数器。信号量用于实现进程间的互斥与同步,而不是用于存储进程间通信数据。
1.特点:
- 信号量用于进程间同步,若要在进程间传递数据需要结合共享内存。
- 信号量基于操作系统的 PV 操作,程序对信号量的操作都是原子操作。
- 每次对信号量的 PV 操作不仅限于对信号量值加 1 或减 1,而且可以加减任意正整数。
- 支持信号量组。
P :可以简单描述为 拿钥匙开锁
V: 可以简单描述为 把钥匙放回
二值信号量:
最简单的信号量是只能取 0 和 1 的变量,这也是信号量最常见的一种形式,叫做二值信号量(Binary Semaphore)。
而可以取多个正整数的信号量被称为通用信号量。
2.函数原型:
Linux 下的信号量函数都是在通用的信号量数组上进行操作,而不是在一个单一的二值信号量上进行操作。
头文件:
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
// 创建或获取一个信号量组: 若成功返回信号量集ID,失败返回-1
int semget(key_t key, int num_sems, int sem_flags);
// 对信号量组进行操作,改变信号量的值: 成功返回0,失败返回-1
int semop(int semid, struct sembuf semoparray[], size_t numops);
// 控制信号量的相关信息
int semctl(int semid, int sem_num, int cmd, ...);
semget :当 semget 创建新的信号量集合时,必须指定集合中信号量的个数(即num_sems),通常为1;如果是引用一个现有的集合,则将num_sems指定为0.
演示:
(1)semget
原型:
// 创建或获取一个信号量组: 若成功返回信号量集ID,失败返回-1
int semget(key_t key, int num_sems, int sem_flags);
key_t key;
int semid;
key = ftok(".",2);
semid = semget(key,1,IPC_CREAT|0666); // 创建 / 获取 信号量
// num_sems = 1 指当前信号量集合中有一个信号量
(2)semctl
原型:
// 控制信号量的相关信息
int semctl(int semid, int sem_num, int cmd, ...);
semid : 信号量ID
sem_num : 指当前操作第几个信号量 (信号量也和数组一样,从0开始)
cmd : 设置信号量的指令,一般常用 SETVAL 来设置信号量的值(使用 SETVAL 的话 SETVAL 需要有 4 个参数)
union semun {
int val; /* Value for SETVAL */
struct semid_ds *buf; /* Buffer for IPC_STAT, IPC_SET */
unsigned short *array; /* Array for GETALL, SETALL */
struct seminfo *__buf; /* Buffer for IPC_INFO
(Linux-specific) */
};
union semun initsem;
initsem.val = 1; // 当前设置一把钥匙
semctl(semid,0,SETVAL,initsem) // 初始化信号量
// 0 指 操作当前第0个信号量
//SETVAL 使用这个IPC指令需要 四 个参数,作用为 设置信号量的值 为 initsem
在semctl函数中的命令有多种,这里就说两个常用的:
- SETVAL:
用于初始化信号量为一个已知的值。所需要的值作为联合体semun的val成员来传递。在信号量第一次使用之前需要设置信号量。 - IPC_RMID:
删除一个信号量集合。如果不删除信号量,它将继续在系统中存在,即使程序已经退出,他可能在你下次运行此程序时引发问题,而且信号量是一种有限的资源。
(3)semop
原型:
// 对信号量组进行操作,改变信号量的值: 成功返回0,失败返回-1
int semop(int semid, struct sembuf semoparray[], size_t numops);
semop函数 第二个参数使用结构体,这个sembuf结构体系统已经建立,直接可以调用,可通过man 2 semop手册查询
系统以两个信号量为样例:
struct sembuf
{
short sem_num; // 信号量组中对应的序号, 0~sem_nums-1
short sem_op; // 信号量值在一次操作中的改变量
short sem_flg; //IPC_NOWAIT, SEM_UNDO
}
struct sembuf sops[2];
int semid;
/* Code to set semid omitted */
sops[0].sem_num = 0; /* Operate on semaphore 0 */
sops[0].sem_op = 0; /* Wait for value to equal 0 */
sops[0].sem_flg = 0;
sops[1].sem_num = 0; /* Operate on semaphore 0 */
sops[1].sem_op = 1; /* Increment value by one */
sops[1].sem_flg = 0;
if (semop(semid, sops, 2) == -1) {
perror("semop");
exit(EXIT_FAILURE);
}
这里我以xhl2.c代码演示:
1 #include<stdio.h>
2 #include <sys/types.h>
3 #include <sys/ipc.h>
4 #include <sys/sem.h>
5
6 // int semget(key_t key, int nsems, int semflg);
7 // int semop(int semid, struct sembuf *sops, unsigned nsops);
8 // int semctl(int semid, int semnum, int cmd, ...);
9 // int semop(int semid, struct sembuf *sops, unsigned nsops);
10
11
12 union semun {
13 int val; /* Value for SETVAL */
14 struct semid_ds *buf; /* Buffer for IPC_STAT, IPC_SET */
15 unsigned short *array; /* Array for GETALL, SETALL */
16 struct seminfo *__buf; /* Buffer for IPC_INFO
17 (Linux-specific) */
18 };
19
20 void pGetKey(int id) //拿取钥匙
21 {
22 struct sembuf new;
23
24 new.sem_num = 0; //要操作的信号量编号ID,当前是0
25 new.sem_op = -1; //initsem.val设置了一把钥匙,所以要拿走一把钥匙,需要-1
26 new.sem_flg = SEM_UNDO; //如果钥匙被拿走了为0情况下,会进行等待钥匙的归还
27
28 semop(id ,&new, 1); //第二个参数是指针,所以需要取地址
29 printf("Get key\n");
30 }
31
32 void vPutBackKey(int id) //放回钥匙
33 {
34 struct sembuf new;
35
36 new.sem_num = 0; //要操作的信号量编号ID,当前是0
37 new.sem_op = 1; //由于钥匙被拿走了,所以还钥匙需要+1
38 new.sem_flg = SEM_UNDO; //如果钥匙被拿走了为0情况下,会进行等待钥匙的归还
39
40 semop(id ,&new, 1); //第二个参数是指针,所以需要取地址
41 printf("Put key\n");
42 }
43
44 int main()
45 {
46
47 key_t key;
48 int semid;
49 key = ftok(".",2);
50
51 semid = semget(key,1,IPC_CREAT|0666);
52 union semun initsem;
53 initsem.val = 0; //设置开始的时候没有钥匙
54
55 semctl(semid,0, SETVAL ,initsem); //初始化信号量
56
57 int pid;
58 pid = fork();
59
60 if(pid > 0)
61 {
62 pGetKey(semid);
63 printf("This is father\n");
64 vPutBackKey(semid);
65
66 }else if(pid == 0){
67
68 // pGetKey(semid);
69 printf("This is child\n");
70 vPutBackKey(semid);
71
72 }else{
73
74 printf("fork error\n");
75 }
76
77 return 0;
78 }
思想:
刚开始创建进程,因为一开始钥匙设置为0,所以父进程的pGetKey 函数会因没有钥匙而进行阻塞,这时子进程会执行vPutBackKey函数把钥匙放回去,并打印子进程printf,这样放回了钥匙父进程就不会阻塞,继续往下走打印父进程的printf
结果:
不足的内容后续会补充,用于记录学习,欢迎交流
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/68484.html