4 进程间通信(二)

4.4 进程通信之IPC通信

​ IPC通信分为共享内存、消息队列以及信号灯。这些IPC对象(共享内存、消息队列、信号灯)都存在于内核空间中。

应用程序使用IPC通信的一般步骤如下:

  • 首先生成一个key值。有两种生成key的方式,一种是使用宏IPC_PRIVATE表示一个key,它表示一个私有对象,只能用于当前进程或者具有亲缘关系的进程访问。另一种是使用ftok函数来生成一个key值,这种方式创建的IPC对象可以被不同的进程访问。

  • 使用生成的key值,创建一个IPC对象(如果是已经创建好的IPC对象,则打开该IPC对象),这个时候每个IPC对象都有一个唯一的ID号(IPC_id,可以是shm_id,msg_id,sem_id,每个id代表一个IPC对象)。

  • 进程通过IPC_id,调用访问IPC通道的读写函数来操作IPC对象。调用shmctrl,shmat,shmdt来访问共享内存;调用msgctrl,msgsnd,msgrecv访问消息队列;调用semctrl,semop访问信号灯。

​ 如何理解key和IPC_id(shm_id/msg_id/sem_id)

​ 回答这个问题,请先思考一个问题,应用程序如何访问一个IPC对象(共享内存,消息队列、信号量灯)?

​ 显然,我们需要一个唯一表示该IPC对象的身份ID(IPC_id,该IPC_id是由操作系统来管理的),但是由于这个ID只在当前创建该IPC对象的进程中可以获取到,在别的IPC进程中都没法获取,那么如何得到IPC对象的ID呢?这个时候就需要key值了,它相当于IPC_id的一个别名,或者叫做外部名,因此key值必须也是唯一的,这样才能得到唯一的IPC对象id。不同进程通过同一个key值得到同一个IPC对象id,来访问同一个IPC对象。如下图所示

ftok函数

函数原型 : char ftok(const char *path, char key)

参数 : path,存在并且可以访问的文件路径

       key,一个字符

返回值 : 正确返回一个key值,出错返回-1

​ 为何需要ftok函数先生成key,然后再创建IPC对象?

​ 这就类似于无名管道和有名管道的区别,使用IPC_PRIVATE宏创建的共享内存就类似于无名管道,只能实现有亲缘关系的进程间通信。

​ 那么为什么又需要使用ftok生成一个key值呢?是否可以直接指定一个非零值呢?直接指定一个非零的key值做法是不建议的,因为读者自己指定的key值很有可能于系统中已经存在的key值一样。

​ ftok函数创建了一个key值之后,就类似于有名管道,既可以实现具有亲缘关系的进程间通信,又能够实现非亲缘关系的进程间通信。

4.4.1 共享内存

4.4.1.1 特点

​ 所谓共享内存是指多个进程都可以访问的同一块地址空间,但是我们知道Linux操作系统为了保证系统执行的安全,为每个进程划分了各自独立的地址空间,每个进程不能访问别的进程的地址空间,那么共享内存实现的原理是什么呢?

​ 内核开辟一块物理内存区域,进程本身将这片内存空间映射到自己的地址空间进行读写。

​ 从图中可以看到,进程可以直接访问这片内存,数据不需要在两进程间复制,所以速度较快。共享内存没有任何的同步与互斥机制,所以要使用信号量来实现对共享内存的存取的同步。

​ 当需要使用共享内存进行通信时,一般步骤如下:

  • 先创建一片共享内存,该内存存在于内核空间中。

  • 进程通过key值找到这片共享内存的唯一ID,然后将这片共享内存映射到自己的地址空间。

  • 每个进程通过读写映射后的地址,来访问内核中的共享内存。

4.4.1.2创建共享内存

函数原型 : int shmget(key_t key, int size, int shmflg)

头文件: #include <sys/shm.h>

函数参数 : key: IPC_PRIVATE 或 ftok的返回值

		 IPC_PRIVATE返回的key值都是一样的,都是0

		 size : 共享内存区大小

		 shmflg : 同open函数的权限位,也可以用八进制表示法

返回值  : 成功,共享内存段标识符ID; -1 出错

程序示例1(参考jz2440\process_ipc\1st_shm\1st_shm.c)

01 /**********************************************************************
02  * 功能描述: 1.使用IPC_PRIVATE创建共享内存
03  * 输入参数: 无
04  * 输出参数: 无
05  * 返 回 值: 无
06  * 修改日期        版本号     修改人          修改内容
07  * -----------------------------------------------
08  * 2020/05/16       V1.0      zh(ryan)        创建
09  ***********************************************************************/
10 
11 #include <stdio.h>
12 #include <stdlib.h>
13 #include <unistd.h>
14 #include <sys/types.h>
15 #include <sys/shm.h>
16 #include <signal.h>
17 
18 int main(int argc, char *argv[])
19 {
20     int shmid;
21     
22     shmid = shmget(IPC_PRIVATE, 128, 0777);
23     if (shmid < 0) {
24         printf("create shared memory fail\n");
25         return -1;
26     }
27     printf("create shared memory sucess, shmid = %d\n", shmid);
28     system("ipcs -m");
29     return 0;
30 }

JZ2440实验

  • 编译
arm-linux-gcc 1st_shm.c -o 1st_shm
  • 拷贝到NFS文件系统
cp 1st_shm /work/nfs_root/first_fs
  • 运行

执行第18行程序后,会在串口打印如下信息,这行语句的作用和我们直接在串口console下面输入“ipcs -m”是一样的。我们发现此时共享内存的key值为0。

./1st_shm

程序示例2(参考jz2440\process_ipc\1st_shm\2nd_shm.c)

程序源码,使用fotk函数生成一个key值

01 /**********************************************************************
02  * 功能描述: 1.使用ftok函数生成的key创建共享内存
03  * 输入参数: 无
04  * 输出参数: 无
05  * 返 回 值: 无
06  * 修改日期        版本号     修改人          修改内容
07  * -----------------------------------------------
08  * 2020/05/16       V1.0      zh(ryan)        创建
09  ***********************************************************************/
10 
11 #include <stdio.h>
12 #include <stdlib.h>
13 #include <unistd.h>
14 #include <sys/types.h>
15 #include <sys/shm.h>
16 #include <signal.h>
17 
18 int main(int argc, char *argv[])
19 {
20     int shmid;
21     int key;
22 
23     key = ftok("./a.c", 'a');  //先创建一个key值
24     if (key < 0) {
25         printf("create key fail\n");
26         return -1;
27     }
28     printf("create key sucess key = 0x%X\n",key);
29     
30     shmid = shmget(key, 128, IPC_CREAT | 0777);
31     if (shmid < 0) {
32         printf("create shared memory fail\n");
33         return -1;
34     }
35     printf("create shared memory sucess, shmid = %d\n", shmid);
36     system("ipcs -m");
37     return 0;
38 }

JZ2440实验

  • 编译
arm-linux-gcc 2nd_shm.c -o 2nd_shm
  • 拷贝到NFS文件系统
cp 2nd_shm /work/nfs_root/first_fs
  • 运行

我们需要在2nd_shm所在的同级目录下创建一个文件a.c(在jz2440开发板上)

touch a.c

我们发现此时共享内存的key值为非零值0x610d0169.

./2nd_shm

4.4.1.3 应用程序如何访问共享内存

​ 我们知道创建的共享内存还是处于内核空间中,用户程序不能直接访问内核地址空间,那么用户程序如何访问这个共享内存呢?

shmat函数

将共享内存映射到用户空间,这样应用程序就可以直接访问共享内存了

函数原型 : void *shmat(int shmid, const void *shmaddr, int shmflg)

参数 : shmid ID号

	   shmaddr 映射地址, NULL为系统自动完成的映射

       shmflg SHM_RDONLY共享内存只读

       默认是0,可读可写

返回值:成功,映射后的地址;失败,返回NULL

程序示例(参考jz2440\process_ipc\1st_shm\3nd_shm.c)

01 /**********************************************************************
02  * 功能描述: 1.创建共享内存,将该共享内存地址通过shmat映射到用户地址空间
03             2.用户通过标准输入向这个共享内存中输入一行字符串
04             3.然后从该共享内存中读取内容,验证是否能够读取到
05  * 输入参数: 无
06  * 输出参数: 无
07  * 返 回 值: 无
08  * 修改日期        版本号     修改人          修改内容
09  * -----------------------------------------------
10  * 2020/05/16       V1.0      zh(ryan)        创建
11  ***********************************************************************/
12 #include <stdio.h>
13 #include <stdlib.h>
14 #include <unistd.h>
15 #include <sys/types.h>
16 #include <sys/shm.h>
17 #include <signal.h>
18 
19 int main(int argc, char *argv[])
20 {
21     int shmid;
22     int key;
23     char *p;
24     
25     key = ftok("./a.c", 'b');
26     if (key < 0) {
27         printf("create key fail\n");
28         return -1;
29     }
30     printf("create key sucess key = 0x%X\n",key);
31     
32     shmid = shmget(key, 128, IPC_CREAT | 0777);
33     if (shmid < 0) {
34         printf("create shared memory fail\n");
35         return -1;
36     }
37     printf("create shared memory sucess, shmid = %d\n", shmid);
38     system("ipcs -m");
39     
40     p = (char *)shmat(shmid, NULL, 0);
41     if (p == NULL) {
42         printf("shmat fail\n");
43         return -1;
44     }
45     printf("shmat sucess\n");
46     
47     //等待console输入,然后向共享内存写入数据
48     fgets(p, 128, stdin);
49     
50     //读共享内存
51     printf("share memory data:%s\n", p);
52     
53     //再次读共享内存
54     printf("share memory data:%s\n", p);
55     return 0;
56 }

JZ2440实验

  • 编译
arm-linux-gcc 3nd_shm.c -o 3nd_shm
  • 拷贝到NFS文件系统
cp 3nd_shm /work/nfs_root/first_fs
  • 运行

我们需要在3nd_shm所在的同级目录下创建一个文件a.c(在jz2440开发板上)

touch a.c

此时会提示用户输入信息

./3nd_shm

我们在console下输入任意字符,比如“hello linux”,然后按下回车,发现打印如下

​ 问题:代码中第51行读了一遍共享内存,然后第54行又读了一遍共享内存,发现两次都能读到共享内存的内容,说明共享内存被读了之后,内容仍然存在。而在管道中,读了一遍管道内容之后,如果紧接着读取第二遍(在没有新写入的前提下),我们是不能读到管道中的内容的,说明管道只要读取一次之后,内容就消失了,读者可以通过实验自行验证一下。

shmdt函数

函数原型:int shmdt(const void *shmaddr)

参数 ; shmat的返回值

返回值 : 成功0,出错-1

程序示例(参考jz2440\process_ipc\1st_shm\4th_shm.c)

01 /**********************************************************************
02  * 功能描述: 1.创建共享内存,将该共享内存地址通过shmat映射到用户地址空间
03             2.用户通过标准输入向这个共享内存中输入一行字符串
04             3.然后从该共享内存中读取内容
05             4.调用shmdt解除地址映射,此时应用程序继续访问会出错
06  * 输入参数: 无
07  * 输出参数: 无
08  * 返 回 值: 无
09  * 修改日期        版本号     修改人          修改内容
10  * -----------------------------------------------
11  * 2020/05/16       V1.0      zh(ryan)        创建
12  ***********************************************************************/
13 #include <stdio.h>
14 #include <stdlib.h>
15 #include <unistd.h>
16 #include <sys/types.h>
17 #include <sys/shm.h>
18 #include <signal.h>
19 #include <string.h>
20 
21 int main(int argc, char *argv[])
22 {
23     int shmid;
24     int key;
25     char *p;
26     
27     key = ftok("./a.c", 'b');
28     if (key < 0) {
29         printf("create key fail\n");
30         return -1;
31     }
32     printf("create key sucess key = 0x%X\n",key);
33     
34     shmid = shmget(key, 128, IPC_CREAT | 0777);
35     if (shmid < 0) {
36         printf("create shared memory fail\n");
37         return -1;
38     }
39     printf("create shared memory sucess, shmid = %d\n", shmid);
40     system("ipcs -m");
41     
42     p = (char *)shmat(shmid, NULL, 0);
43     if (p == NULL) {
44         printf("shmat fail\n");
45         return -1;
46     }
47     printf("shmat sucess\n");
48     
49     //write share memory
50     fgets(p, 128, stdin);
51     
52     //start read share memory
53     printf("share memory data:%s\n", p);
54     
55     //start read share memory again
56     printf("share memory data:%s\n", p);
57 
58     //在用户空间删除共享内存的地址
59     shmdt(p);
60     
61     memcpy(p, "abcd", 4);  //执行这个语句会出现segment fault,因为解除了共享内存地址映射
62     return 0;
63 }

JZ2440实验

  • 编译
arm-linux-gcc 4th_shm.c -o 4th_shm
  • 拷贝到NFS文件系统
cp 4th_shm /work/nfs_root/first_fs
  • 运行

我们需要在4th_shm.c所在的同级目录下创建一个文件a.c(在jz2440开发板上)

touch a.c

运行,此时会提示用户输入信息,输入完之后,执行第61行语句会出现Segmentation fault,这是程序期待的现象。

./4th_shm

shmctl函数

函数原型:int shmctl(int shmid, int cmd, struct shmid_ds *buf)

参数: shmid : 共享内存标识符

	 cmd : 
		IPC_START (获取对象属性)   --- 实现了命令 ipcs -m

    	IPC_SET(设置对象属性)

	 	IPC_RMID (删除对象属性)    --- 实现了命令 ipcrm -m

	 buf : 指定IPC_START/IPC_SET 时用以保存/设置属性

返回值 : 成功0,出错-1

程序示例(参考jz2440\process_ipc\1st_shm\5th_shm.c)

01 /**********************************************************************
02  * 功能描述: 1.创建共享内存,将该共享内存地址通过shmat映射到用户地址空间
03               2.用户通过标准输入向这个共享内存中输入一行字符串
04               3.然后从该共享内存中读取内容
05               4.调用shmdt解除地址映射,此时应用程序继续访问会出错
06               5.最后调用shmctl函数删除内核中的共享内存
07  * 输入参数: 无
08  * 输出参数: 无
09  * 返 回 值: 无
10  * 修改日期        版本号     修改人          修改内容
11  * -----------------------------------------------
12  * 2020/05/16       V1.0      zh(ryan)        创建
13  ***********************************************************************/
14 
15 #include <stdio.h>
16 #include <stdlib.h>
17 #include <unistd.h>
18 #include <sys/types.h>
19 #include <sys/shm.h>
20 #include <signal.h>
21 #include <string.h>
22 
23 int main(int argc, char *argv[])
24 {
25     int shmid;
26     int key;
27     char *p;
28     
29     key = ftok("./a.c", 'b');
30     if (key < 0) {
31         printf("create key fail\n");
32         return -1;
33     }
34     printf("create key sucess key = 0x%X\n",key);
35     
36     shmid = shmget(key, 128, IPC_CREAT | 0777);
37     if (shmid < 0) {
38         printf("create shared memory fail\n");
39         return -1;
40     }
41     printf("create shared memory sucess, shmid = %d\n", shmid);
42     system("ipcs -m");
43     
44     p = (char *)shmat(shmid, NULL, 0);
45     if (p == NULL) {
46         printf("shmat fail\n");
47         return -1;
48     }
49     printf("shmat sucess\n");
50     
51     //write share memory
52     fgets(p, 128, stdin);
53     
54     //start read share memory
55     printf("share memory data:%s\n", p);
56     
57     //start read share memory again
58     printf("share memory data:%s\n", p);
59 
60     //在用户空间删除共享内存的地址
61     shmdt(p);
62     
63     //memcpy(p, "abcd", 4);  //执行这个语句会出现segment fault
64     
65     shmctl(shmid, IPC_RMID, NULL);
66     system("ipcs -m");
67     return 0;
68 }

JZ2440实验

  • 编译
arm-linux-gcc 5th_shm.c -o 5th_shm
  • 拷贝到NFS文件系统
cp 5th_shm /work/nfs_root/first_fs
  • 运行
touch a.c

​ 运行。此时会提示用户输入信息,第一次执行第42行语句时,读者可以看到共享内存,第二次执行第66行语句时,读者就看不到共享内存了,因为此时共享内存已经被删除了。

./4th_shm

4.4.1.4 共享内存实现进程间通信

步骤:

1. 创建/打开共享内存

2. 映射共享内存,即把指定的共享内存映射到进程的地址空间用于访问

3. 读写共享内存

4. 撤销共享内存映射

5. 删除共享内存对象

使用共享内存时的一些注意点或是限制条件

1. 共享内存的数量是有限制的,通过ipcs -l命令查看,当然如果我们具有管理员权限,可以通过 cat /proc/sys/kernel/shmmax来查看

2. 共享内存删除的时间点,shmctl添加删除标记,只有当所有进程都取消共享内存映射时(即所有进程调用shmdt之后),才会删除共享内存。

示例源码(参考jz2440\process_ipc\1st_shm\6th_shm.c)

01 /**********************************************************************
02  * 功能描述: 1.在父进程中创建使用key值为IPC_PRIVATE创建一个共享内存
03             2.然后在父进程中创建一个子进程
04             3.通过标准输入,父进程向共享内存中写入字符串
05             4.父进程调用发送信号函数通知子进程可以读取共享内存了
06             5.子进程收到父进程发送过来的信号,开始读取共享内存
07             6.子进程读完共享内存后,发送信号通知父进程读取完成
08  * 输入参数: 无
09  * 输出参数: 无
10  * 返 回 值: 无
11  * 修改日期        版本号     修改人          修改内容
12  * -----------------------------------------------
13  * 2020/05/16       V1.0      zh(ryan)        创建
14  ***********************************************************************/
15 
16 #include <stdio.h>
17 #include <stdlib.h>
18 #include <unistd.h>
19 #include <sys/types.h>
20 #include <sys/shm.h>
21 #include <signal.h>
22 #include <string.h>
23 
24 void myfun(int signum)
25 {
26     return;
27 }
28 
29 int main(int argc, char *argv[])
30 {
31     int shmid;
32     int key;
33     char *p;
34     int pid;
35 
36 
37     shmid = shmget(IPC_PRIVATE, 128, IPC_CREAT | 0777);
38     if (shmid < 0) {
39         printf("create shared memory fail\n");
40         return -1;
41     }
42     printf("create shared memory sucess, shmid = %d\n", shmid);
43 
44     pid = fork();
45     if (pid > 0) {    // 父进程
46         signal(SIGUSR2, myfun);
47         p = (char *)shmat(shmid, NULL, 0);
48         if (p == NULL) {
49             printf("shmat fail\n");
50             return -1;
51         }
52         printf("parent process shmat sucess\n");
53         while (1) {
54             //从标准输入获取字符串,将其写入到共享内存
55             printf("parent process begin to write memory data:");
56             fgets(p, 128, stdin);
57             kill(pid, SIGUSR1);   // 发信号通知子进程读共享内存
58             pause();              // 等待子进程读完共享内存的信号
59         }
60     }
61     if (pid == 0) { // 子进程
62         signal(SIGUSR1, myfun);
63         p = (char *)shmat(shmid, NULL, 0);
64         if (p == NULL) {
65             printf("shmat fail\n");
66             return -1;
67         }
68         printf("child process shmat sucess\n");
69         while (1) {
70             pause(); // 等待父进程发信号,准备读取共享内存
71             //子进程开始读共享内存,并发信号给父进程告知读完成
72             printf("child process read share memory data:%s\n", p);
73             kill(getppid(), SIGUSR2);
74         }
75     }
76 
77     //在用户空间删除共享内存的地址
78     shmdt(p);
79     
80     //memcpy(p, "abcd", 4);  //执行这个语句会出现segment fault
81     
82     shmctl(shmid, IPC_RMID, NULL);
83     system("ipcs -m");
84     return 0;
85 }

JZ2440实验

  • 编译
arm-linux-gcc 6th_shm.c -o 6th_shm
  • 拷贝到NFS文件系统
cp 6th_shm /work/nfs_root/first_fs
  • 运行

父进程从标准输入获取用户输入的字符串,然后子进程会打印出该字符串。

./6th_shm

server进程源码(参考jz2440\process_ipc\1st_shm\7th_shm_1.c)

01 /**********************************************************************
02  * 功能描述: 1.server进程使用ftok生成一个key值,利用这个key值创建一个共享内存
03             2.通过标准输入,向共享内存中写入字符串
04             3.server进程调用发送信号函数通知client进程
05  * 输入参数: 无
06  * 输出参数: 无
07  * 返 回 值: 无
08  * 修改日期        版本号     修改人          修改内容
09  * -----------------------------------------------
10  * 2020/05/16       V1.0      zh(ryan)        创建
11  ***********************************************************************/
12 
13 #include <stdio.h>
14 #include <stdlib.h>
15 #include <unistd.h>
16 #include <sys/types.h>
17 #include <sys/shm.h>
18 #include <signal.h>
19 #include <string.h>
20 
21 struct mybuf
22 {
23     int pid;
24     char buf[124];
25 };
26 
27 void myfun(int signum)
28 {
29     return;
30 }
31 
32 int main(int argc, char *argv[])
33 {
34     int shmid;
35     int key;
36     struct mybuf *p;
37     int pid;
38 
39     key = ftok("./a.c", 'a');
40     if (key < 0) {
41         printf("create key fail\n");
42         return -1;
43     }
44     printf("create key sucess\n");
45 
46     shmid = shmget(key, 128, IPC_CREAT | 0777);
47     if (shmid < 0) {
48         printf("create shared memory fail\n");
49         return -1;
50     }
51     printf("create shared memory sucess, shmid = %d\n", shmid);
52 
53     signal(SIGUSR2, myfun);
54     p = (struct mybuf *)shmat(shmid, NULL, 0);
55     if (p == NULL) {
56         printf("shmat fail\n");
57         return -1;
58     }
59     printf("parent process shmat sucess\n");
60 
61     p->pid = getpid(); // 将server进程的pid号写入到共享内存
62     pause();           // 等待client读取到server pid号
63     pid=p->pid;        // 获取client的进程号
64 
65     while (1) {
66         //write share memory
67         printf("parent process begin to write memory data\n");
68         fgets(p->buf, 124, stdin);
69         kill(pid, SIGUSR1);   // 向client发送信号通知client读取共享内存数据
70         pause();              // 等待client读取完共享内存数据
71     }
72 
73     //在用户空间删除共享内存的地址
74     shmdt(p);
75 
76     shmctl(shmid, IPC_RMID, NULL);
77     system("ipcs -m");
78     return 0;
79 }

client进程源码(参考jz2440\process_ipc\1st_shm\7th_shm_2.c)

01 /**********************************************************************
02  * 功能描述: 1.client进程使用ftok生成一个key值,利用这个key值打开一个共享内存
03             2.client进程收到server进程发送过来的信号之后,开始读取共享内存
04             3.子进程读完共享内存后,发送信号通知父进程读取完成
05  * 输入参数: 无
06  * 输出参数: 无
07  * 返 回 值: 无
08  * 修改日期        版本号     修改人          修改内容
09  * -----------------------------------------------
10  * 2020/05/16       V1.0      zh(ryan)        创建
11  ***********************************************************************/
12 
13 #include <stdio.h>
14 #include <stdlib.h>
15 #include <unistd.h>
16 #include <sys/types.h>
17 #include <sys/shm.h>
18 #include <signal.h>
19 #include <string.h>
20 
21 struct mybuf
22 {
23     int pid;
24     char buf[124];
25 };
26 
27 void myfun(int signum)
28 {
29     return;
30 }
31 
32 int main(int argc, char *argv[])
33 {
34     int shmid;
35     int key;
36     struct mybuf *p;
37     int pid;
38     
39     key = ftok("./a.c", 'a');
40     if (key < 0) {
41         printf("create key fail\n");
42         return -1;
43     }
44     printf("create key sucess\n");
45 
46     shmid = shmget(key, 128, IPC_CREAT | 0777);
47     if (shmid < 0) {
48         printf("create shared memory fail\n");
49         return -1;
50     }
51     printf("create shared memory sucess, shmid = %d\n", shmid);
52 
53     signal(SIGUSR1, myfun);
54     p = (struct mybuf *)shmat(shmid, NULL, 0);
55     if (p == NULL) {
56         printf("shmat fail\n");
57         return -1;
58     }
59     printf("client process shmat sucess\n");
60     
61     // get server pid
62     //read share memory
63     pid = p->pid;
64     // write client pid to share memory
65     p->pid = getpid();
66     kill(pid, SIGUSR2);   // tell server process to read data
67     
68     //client start to read share memory
69 
70     while (1) {
71         pause();              // wait server process write share memory
72         printf("client process read data:%s\n", p->buf); // read data
73         kill(pid, SIGUSR2);   // server can  write share memory
74     }
75 
76     //在用户空间删除共享内存的地址
77     shmdt(p);
78     
79     shmctl(shmid, IPC_RMID, NULL);
80     system("ipcs -m");
81     return 0;
82 }

​ 该源码留给读者自行实验,需要注意的是,因为这个时候需要运行两个console,分别运行server进程和client进程,可以采用一个串口console,一个telnet console。我们也可以在ubuntu下开启两个terminal验证。

4.4.2 消息队列

4.4.2.1 什么是消息队列

​ 消息队列是消息的链表,它是一个链式队列,和管道类似,每个消息多有最大长度限制,可用cat/proc/sys/kernel/msgmax查看。

​ 内核为每个消息队列对象维护了一个数据结构msgqid_ds,用于标识消息队列,以便让进程知道当前操作的是哪一个消息队列,每一个msqid_ds表示一个消息队列,并通过msqid_ds.msg_first、msg_last维护一个先进先出的msg链表队列,当发送一个消息到该消息队列时,把发送的消息构造成一个msg的结构对象,并添加到msqid_ds.msg_first、msg_last维护的链表队列。在内核中的表示如下:

4.4.2.2 特点

  1. 生命周期跟随内核,消息队列一直存在,需要用户显示调用接口删除或者使用命令删除。

  2. 消息队列可以实现双向通信

  3. 克服了管道只能承载无格式字节流的缺点

4.4.2.3 消息队列函数

msgget函数

创建或者打开消息队列的函数
头文件:#include <sys/types.h>
        #include <sys/ipc.h>
       #include <sys/msg.h>
原型: int msgget(key_t key, int flag)
参数: key 和消息队列关联的key值
       flag 消息队列的访问权限
返回值: 成功,消息队列ID,出错 -1

msgctl函数

消息队列控制函数
原型: int msgctl(int msgqid, int cmd, struct msqid_ds *buf)
参数: msgqid 消息队列ID
           cmd IPC_STAT 读取消息队列的属性,并将其保存在buf指向的缓冲区中
               IPC_SET  设置消息队列的属性,这个值取自buf参数
               IPC_RMID 从系统中删除消息队列
           buf 消息缓冲区
返回值: 成功 0,出错 -1

msgsnd函数

把一条消息添加到消息队列中
头文件#include <sys/types.h>
      #include <sys/ipc.h>
      #include <sys/msg.h>
原型: int msgsnd(int msgqid, const void *msgp, size_t size, int flag)
参数: msgqid 消息队列ID
           msgp 指向消息的指针,常用消息结构msgbuf如下
           struct msgbuf {
               long mtype;        //消息类型
               char mtext[N];     //消息正文
           };
           size 消息正文的字节数
           flag IPC_NOWAIT 消息没有发送完成也会立即返回
                0: 直到发送完成函数才会返回
    返回值: 成功 0,出错 -1

msgrcv函数

从一个消息队列接受消息
原型: int msgrcv(int msgqid, void *msgp, size_t size, long msgtype, int flag)
参数: msgqid 消息队列ID
       msgp 接收消息的缓冲区
       size 要接收消息的字节数
       msgtype  0 接收消息队列中第一个消息
                大于0 接收消息队列中第一个类型为msgtype的消息
                小于0 接收消息队列中类型值不大于msgtype的绝对值且类型值又最小的消息
           flag IPC_NOWAIT 没有消息,会立即返回
                0: 若无消息则会一直阻塞
                
返回值: 成功 接收消息的长度,出错 -1

4.4.2.4 消息队列实现进程间通信

server源码(参考jz2440\process_ipc\2nd_shm\write_msg.c)

01 /**********************************************************************
02  * 功能描述: 1.server进程向消息队列中写数据
03  * 输入参数: 无
04  * 输出参数: 无
05  * 返 回 值: 无
06  * 修改日期        版本号     修改人          修改内容
07  * -----------------------------------------------
08  * 2020/05/16       V1.0      zh(ryan)        创建
09  ***********************************************************************/
10 
11 #include <stdio.h>
12 #include <stdlib.h>
13 #include <unistd.h>
14 #include <sys/types.h>
15 #include <sys/msg.h>
16 #include <signal.h>
17 #include <string.h>
18 
19 struct msgbuf {
20     long type;        //消息类型
21     char voltage[124];     //消息正文
22     char ID[4];
23 };
24 
25 int main(int argc, char *argv[])
26 {
27     int msgid, readret, key;
28     struct msgbuf sendbuf;
29 
30     key = ftok("./a.c", 'a');
31     if (key < 0){
32         printf("create key fail\n");
33         return -1;
34     }
35     msgid = msgget(key, IPC_CREAT|0777);
36     if (msgid < 0) {
37         printf("create msg queue fail\n");
38         return -1;
39     }
40     printf("create msg queue sucess, msgid = %d\n", msgid);
41     system("ipcs -q");
42 
43     // write message queue
44     sendbuf.type = 100;
45     while(1) {
46         memset(sendbuf.voltage, 0, 124);  //clear send buffer
47         printf("please input message:");
48         fgets(sendbuf.voltage, 124, stdin);
49         //start write msg to msg queue
50         msgsnd(msgid, (void *)&sendbuf, strlen(sendbuf.voltage), 0);
51     }
52 
53     return 0;
54 }

client源码(参考jz2440\process_ipc\2nd_shm\read_msg.c)

01 /**********************************************************************
02  * 功能描述: 1.client进程从消息队列中读数据
03  * 输入参数: 无
04  * 输出参数: 无
05  * 返 回 值: 无
06  * 修改日期        版本号     修改人          修改内容
07  * -----------------------------------------------
08  * 2020/05/16       V1.0      zh(ryan)        创建
09  ***********************************************************************/
10 
11 #include <stdio.h>
12 #include <stdlib.h>
13 #include <unistd.h>
14 #include <sys/types.h>
15 #include <sys/msg.h>
16 #include <signal.h>
17 #include <string.h>
18 
19 struct msgbuf {
20     long type;        //消息类型
21     char voltage[124];     //消息正文
22     char ID[4];
23 };
24 
25 int main(int argc, char *argv[])
26 {
27     int msgid, key;
28     struct msgbuf readbuf;
29 
30     key = ftok("./a.c", 'a');
31     if (key < 0){
32         printf("create key fail\n");
33         return -1;
34     }
35     msgid = msgget(key, IPC_CREAT|0777);
36     if (msgid < 0) {
37         printf("create msg queue fail\n");
38         return -1;
39     }
40     printf("create msg queue sucess, msgid = %d\n", msgid);
41     system("ipcs -q");
42 
43     // read message queue
44     while(1) {
45         memset(readbuf.voltage, 0, 124);  //clear recv buffer
46         //start read msg to msg queue
47         msgrcv(msgid, (void *)&readbuf, 124, 100, 0);
48         printf("recv data from message queue:%s", readbuf.voltage);
49     }
50 
51     return 0;
52 }

JZ2440实验

  • 编译
arm-linux-gcc write_msg.c -o write_msg
arm-linux-gcc read_msg.c -o read_msg
  • 拷贝到NFS文件系统
cp write_msg /work/nfs_root/first_fs
cp read_msg /work/nfs_root/first_fs
  • 运行

先在后台执行read_msg,然后在前台运行write_msg,此时在console下输入字符串,可以看到client进程能读到消息队列中的字符串 。

./read_msg &
./ write_msg

4.4.3 信号量灯

4.4.3.1 什么是P、V操作

​ 当不同进程需要访问同一个资源时,由于不同进程的执行次序是未知的,有可能某个进程正在写该资源,而另一个进程正在读该资源,这样会造成进程执行的不确定性。这样的同一个资源,我们称为共享资源,共享资源一次只允许一个进程访问。因此进程在访问共享资源时,需要加上同步、互斥操作。

​ 一般地,P操作表示申请该共享资源,V操作表示释放该共享资源。

4.4.3.2 什么是信号量灯

​ 它是信号量的集合,包含多个信号量,可对多个信号灯同时进行P/V操作,主要用来实现进程、线程间同步/互斥。内核为每个信号量灯维护了一个数据结构semid_ds,用于标识信号量灯,以便进程知道当前操作的是哪个信号量灯,在内核中的表示如下所示。
image

​ 它和POSIX规范中的信号量有什么区别呢?POSIX规范中的信号量只作用于一个信号量,而IPC对象中的信号量灯会作用于一组信号量。

功能 信号量(POSIX) 信号量灯(IPC对象)
定义信号变量 sem_t sem1 semget
初始化信号量 sem_init semctl
P操作 sem_wait semop
V操作 sem_post semop

​ 为什么需要IPC对象中的信号量灯呢?有POSIX规范中的信号量不够吗?

考虑如下场景:

  1. 线程A和线程B都需要访问共享资源1和共享资源2,在线程A中会需要先申请共享资源1,然后再申请共享资源2。

  2. 但是在线程B中,会先申请贡献资源2,然后再申请共享资源1。

  3. 当线程A中开始申请共享资源1时,紧接着会申请共享资源2;而此时线程B中开始申请共享资源2时,紧接着会申请共享资源1。

  4. 线程B正在占用着共享资源2,线程A正在占着共享资源1,导致线程B申请不到共享资源1,它就不会释放共享资源2;线程A申请不到共享资源2,它就不会释放共享资源1;这样就造成了死锁。

4.4.3.3 信号量灯函数

semget函数

创建或者打开函数
头文件#includde <sys/types.h>
#includde <sys/ipc.h>
#includde <sys/sem.h>
原型: int semget(key_t key, int nsems, int semflag)
参数: key 和信号灯集关联的key值
      nsems 信号灯集包含的信号灯数目
      semflag 信号灯集的访问权限
返回值: 成功,信号灯ID,出错 -1

semctl函数

信号量灯控制函数
头文件#includde <sys/types.h>
#includde <sys/ipc.h>
#includde <sys/sem.h>
原型: int semctl(int semid, int semnum, int cmd, ...union semun arg)
          注意最后一个参数不是地址,可以有,可以没有
参数: semid 信号灯集id
      semnum 要修改的信号灯集编号,删除操作时,这个值可以设置为任意值
      cmd GETVAL 获取信号灯的值
           SETVAL 设置信号灯的值
           IPC_RMID 删除信号灯
      union semun arg: 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) */
                 };
返回值: 成功,消息队列ID,出错 -1

semop函数

p/v操作函数
头文件#includde <sys/types.h>
#includde <sys/ipc.h>
#includde <sys/sem.h>
原型: int semop(int semid, struct sembuf *opsptr, size_t nops)
参数: semid 信号灯集id
      opsptr struct sembuf{
                  short sem_num;   //要操作信号灯的编号
                  short sem_op;    //0: 等待,直到信号灯的值变为0,1:资源释放,V操作,-1:分配资源,P操作
                  short sem_flg;   //0: IPC_NOWAIT, SEM_UNDO
              }
       nops 要操作信号灯个数
返回值: 成功,消息队列ID,出错 -1

4.4.3.4 信号量灯实现进程间同步/互斥

程序源码(参考jz2440\process_ipc\3rd_shm\share_sysv.c)

01 /**********************************************************************
02  * 功能描述: 1.父进程从键盘输入字符串到共享内存.
03             2.子进程删除字符串中的空格并打印.
04             3.父进程输入quit后删除共享内存和信号灯集,程序结束.
05  * 输入参数: 无
06  * 输出参数: 无
07  * 返 回 值: 无
08  * 修改日期        版本号     修改人          修改内容
09  * -----------------------------------------------
10  * 2020/05/16       V1.0      zh(ryan)        创建
11  ***********************************************************************/
12 
13 #include <stdio.h>
14 #include <stdlib.h>
15 #include <string.h>
16 #include <sys/ipc.h>
17 #include <sys/sem.h>
18 #include <sys/types.h>
19 #include <sys/shm.h>
20 #include <signal.h>
21 #include <unistd.h>
22 
23 #define N 64
24 #define READ 0
25 #define WRITE 1
26 
27 union semun {
28 	int val;
29         struct semid_ds *buf;
30         unsigned short *array;
31         struct seminfo *__buf;
32 };
33 
34 void init_sem(int semid, int s[], int n)
35 {
36 	int i;
37 	union semun myun;
38 
39 	for (i = 0; i < n; i++){
40 		myun.val = s[i];
41 		semctl(semid, i, SETVAL, myun);
42 	}
43 }
44 
45 void pv(int semid, int num, int op)
46 {
47 	struct sembuf buf;
48 
49 	buf.sem_num = num;
50 	buf.sem_op = op;
51 	buf.sem_flg = 0;
52 	semop(semid, &buf, 1);
53 }
54 
55 int main(int argc, char *argv[])
56 {
57 	int shmid, semid, s[] = {0, 1};
58 	pid_t pid;
59 	key_t key;
60 	char *shmaddr;
61 
62 	key = ftok(".", 's');
63 	if (key == -1){
64 		perror("ftok");
65 		exit(-1);
66 	}
67 
68 	shmid = shmget(key, N, IPC_CREAT|0666);
69 	if (shmid < 0) {
70 		perror("shmid");
71 		exit(-1);
72 	}
73 
74 	semid = semget(key, 2, IPC_CREAT|0666);
75 	if (semid < 0) {
76 		perror("semget");
77 		goto __ERROR1;
78 	}
79 	init_sem(semid, s, 2);
80 
81 	shmaddr = shmat(shmid, NULL, 0);
82 	if (shmaddr == NULL) {
83 		perror("shmaddr");
84 		goto __ERROR2;
85 	}
86 
87 	pid = fork();
88 	if(pid < 0) {
89 		perror("fork");
90 		goto __ERROR2;
91 	} else if (pid == 0) {
92 		char *p, *q;
93 		while(1) {
94 			pv(semid, READ, -1);
95 			p = q = shmaddr;
96 			while (*q) {
97 				if (*q != ' ') {
98 					*p++ = *q;
99 				}
100 				q++;
101 			}
102 			*p = '\0';
103 			printf("%s", shmaddr);
104 			pv(semid, WRITE, 1);
105 		}
106 	} else {
107 		while (1) {
108 			pv(semid, WRITE, -1);
109 			printf("input > ");
110 			fgets(shmaddr, N, stdin);
111 			if (strcmp(shmaddr, "quit\n") == 0) break;
112 				pv(semid, READ, 1);
113 			}
114 			kill(pid, SIGUSR1);
115 		}
116 
117 	__ERROR2:
118 		semctl(semid, 0, IPC_RMID);
119 	__ERROR1:
120 		shmctl(shmid, IPC_RMID, NULL);
121 		return 0;
122 }

JZ2440实验

  • 编译
arm-linux-gcc share_sysv.c -o share_sysv
  • 拷贝到NFS文件系统
cp share_sysv /work/nfs_root/first_fs
  • 运行

在父进程的console下输入字符串,此时子进程会读取到这个字符串。

./share_sysv

image