700字范文,内容丰富有趣,生活中的好帮手!
700字范文 > Linux系统编程(三) --进程间通信

Linux系统编程(三) --进程间通信

时间:2020-12-06 06:07:50

相关推荐

Linux系统编程(三) --进程间通信

文章目录

1 进程间通信总览1.1 进程间如何通信1.2 Linux IPC 分类1.3 Linux IPC常用手段2 无名管道2.1 pipe 函数2.2 用pipe进行进程间通信3 有名管道3.1 创建 FIFO 类型文件3.2 FIFO文件特性4 System V共享内存4.1 共享内存4.2 IPC 内核对象4.3 获取内核对象的id号4.4 创建IPC内核对象4.5 shmget函数4.6 键值与ftok4.7 shmget函数4.8 shmctl5 System V消息队列5.1 消息队列相关的函数5.2 消息数据格式5.3 msgsnd函数5.4 msgrcv函数6 System V信号量6.1 System V信号量简介6.2 创建和获取信号量6.3 设置和获取信号量值6.4 请求和释放信号量7 生产者消费者模型

1 进程间通信总览

进程间通信,IPC(Inter Process Communication),指不同进程间的交流,就是进程之间发送和接收数据。

1.1 进程间如何通信

进程的 0-3GB 空间是互不相干的,3GB-4GB 是内核空间,所有进程间共带。下图说明了进程空间的独立性,内核空间的共享性。内核的共享特性,给进程的通信带来了可能。

进程的用户空间与内核空间

实验:进程 A 从标准输入读取字符,然后“发送给”进程 B,进程 B 接收到数据后,将字符中的小写转换成大写后打印到屏幕。

方案:

进程 A 创建一个文件 tmp,并向 tmp 写入数据。进程 A 写完数据后关闭 tmp,并向进程 B 发送信号 SIGUSR1。进程 B 接收到信号后,知道进程 A 已经写完数据,于是打开文件 tmp 读取数据。进程 B 读取完数据后关闭 tmp 文件,并把 tmp 文件删除。进程 B 把读取到的数据中的字符全部转换成大写打印到屏幕。

// sender.c#include <unistd.h>#include <sys/types.h>#include <sys/stat.h>#include <fcntl.h>#include <signal.h>#include <stdio.h>#include <stdlib.h>int main(int argc, char* argv[]) {// 要想发送信号,必须知道另一个进程的进程 id 号,所以这里通过参数将进程 id 传进来if (argc < 2) {printf("usage: %s <pid>", argv[0]);return 1;}pid_t pid = atoi(argv[1]);char buf[64] = {0 };int n = 0;while (1) {// 从标准输入中读取数据,并写到文件中if ((n = read(STDIN_FILENO, buf, 64)) > 0) {int fd = open("tmp", O_WRONLY | O_CREAT | O_EXCL, 0664);if (fd < 0) {perror("open");continue;}write(fd, buf, n);// 写完数据后,向接收进程发送 SIGUSR1 信号close(fd);if (kill(pid, SIGUSR1) < 0) {perror("kill");}// 如果用户输入 q,就关闭程序if (*buf == 'q') return 0;}}return 0;}

// recver.c#include <unistd.h>#include <sys/types.h>#include <sys/stat.h>#include <fcntl.h>#include <signal.h>#include <stdio.h>#include <stdlib.h>#include <ctype.h>// 信号处理函数,从文件中读取数据,并转换成大写打印到屏幕void handler(int sig) {char buf[64];int i;int fd = open("tmp", O_RDONLY);if (fd < 0) {perror("open");return;}int n = 0;if ((n = read(fd, buf, 64)) < 0) {perror("read");close(fd);return;}close(fd);unlink("tmp"); // 读取完成后将文件删除for (i = 0; i < n; ++i)putchar(toupper(buf[i])); // 将数据转换成大写并打印到屏幕。toupper 是 C 库函数,声明于 ctype.h 文件中if (*buf == 'q') exit(0); // 如果收到的数据以 q 开头就退出}int main() {printf("I'm %d\n", getpid());// 注册 SIGUSR1 信号struct sigaction act;act.sa_handler = handler;sigemptyset(&act.sa_mask);act.sa_flags = 0;if (sigaction(SIGUSR1, &act, NULL) < 0) {perror("sigaction");return 1;}// main 函数进入休眠while (1) pause();}

编译,运行

$ gcc sender.c -o sender$ gcc recver.c -o recver$ ./sender 34803

再开启另一个终端,执行

$ ./recver

1.2 Linux IPC 分类

分为四类:

最初的 Unix IPC:包括无名管道,有名管道,信号。System V IPC:包括 System V 共享内存、System V 消息队列、System V 信号量。基于 socket IPC:主要使用套接字的方式进行通信。POSIX IPC:POSIX 共享内存、POSIX 消息队列、POSIX 信号量。

1.3 Linux IPC常用手段

无名管道(pipe)、有名管道(named pipe):无名管道只能用于有亲缘关系(父子进程)的进程,有名管道用于任意两进程间通信。信号(signal)。消息(message)队列:包括Posix消息队列system V消息队列。消息队列克服了信号承载信息量少,管道只能承载无格式字节流以及缓冲区大小受限等缺点。共享内存(share memory):多个进程可以访问同一块内存空间,是最快的可用IPC形式,效率高。往往与其它通信机制,如信号量结合使用,来达到进程间的同步及互斥。信号量(semaphore):主要作为进程间以及同一进程不同线程之间的同步手段。套接口(Socket):用于不同机器之间的进程间通信。

2 无名管道

本质上,pipe 函数会在进程内核空间申请一块内存(比如一个内存页,一般是 4KB),然后把这块内存当成一个先进先出(FIFO)的循环队列来存取数据。管道位于进程内核空间

无名管道和文件确实很像,也有描述符。但它不是普通的本地文件,而是一种抽象的存在。

2.1 pipe 函数

int pipe(int pipefd[2]);

成功返回0,失败返回-1。

pipefd[0] 用于读,而 pipefd[1] 用于写。

用于读写的描述符必须同时打开。

如果关闭读 (close(pipefd[0])) 端保留写端,继续向写端 (pipefd[1]) 端写数据的进程会收到 SIGPIPE 信号。如果关闭写 (close(pipefd[1])) 端保留读端,继续向读端 (pipefd[0]) 端读数据(read 函数),read 函数会返回0。

2.2 用pipe进行进程间通信

用 pipe 打开两个描述符后,fork一个子进程。这样,子进程也会继承这两个描述符,描述符引用计数变成 2。

父进程向子进程发送数据:

父进程关闭pipefd[0](读端),子进程关闭pipefd[1]写端。

步骤一:fork 子进程

fork 后的半双工管道

步骤二:父进程读端,子进程关闭写端

从父进程到子进程的管道

实验

进程 fork 出一个子进程,通过无名管道向子进程发送字符,子进程收到数据后将字符串中的小写字符转换成大写并输出。

// hellopipe.c#include <unistd.h>#include <stdio.h>#include <stdlib.h>#include <ctype.h>void child(int* fd) {close(fd[1]); // 子进程关闭写端char buf[64];int n = 0, i;while (1) {n = read(fd[0], buf, 64); // 如果没有数据可读,read 会阻塞;如果父进程退出,read 返回 0.for (i = 0; i < n; ++i) putchar(toupper(buf[i]));if (*buf == 'q') {close(fd[0]);exit(0);}if (n == 0) {puts("no data to read!");sleep(1);}}exit(0);}int main() {int fd[2];int n = 0;char buf[64] = {0 };if (pipe(fd) < 0) {perror("pipe");return -1;}pid_t pid = fork();if (pid == 0) {child(fd);}close(fd[0]);// 父进程关闭读端while (1) {n = read(STDIN_FILENO, buf, 64);write(fd[1], buf, n);if (*buf == 'q') {close(fd[1]);exit(0);}}return 0;}

3 有名管道

有名管道有一个实实在在的FIFO类型的文件。只要不同的进程打开 FIFO 文件,就可以彼此通信。

3.1 创建 FIFO 类型文件

通过命令 mkfifo 创建,如 mkfifo hello

通过函数 mkfifo(3) 创建

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

该函数返回 0 表示成功,-1 失败。

比如:mkfifo(“hello”, 0664);

hello文件信息如下:

prw-r--r-- 1 skx skx 0 9月 27 07:53 hello|

3.2 FIFO文件特性

文件类型是p,代表管道文件大小是0fifo 文件需要有读写两端,否则在打开fifo文件时会阻塞。

实验:使用 cat 命令打印 hello 文件内容

$ cat hello

接下来你的 cat 命令被阻塞住。

开启另一个终端,执行:

$ echo "hello world" > hello

然后你会看到被阻塞的 cat 又继续执行完毕,在屏幕打印 “hello world”。如果你反过来执行上面两个命令,会发现先执行的那个总是被阻塞。

实验

下面有两个程序,分别是发送端 send 和接收端面 recv。程序 send 从标准输入接收字符,并发送到程序 recv,同时 recv 将接收到的字符打印到屏幕。

发送端

// send.c#include <unistd.h>#include <sys/types.h>#include <sys/stat.h>#include <fcntl.h>#include <stdio.h>int main() {char buf[64];int n = 0;// 有名管道文件不存在则创建if (access("/home/skx/pra/learn_linux/52/hello", 0)) {if ((n = mkfifo("hello", 0664)) < 0)perror("mkfifo");}int fd = open("hello", O_WRONLY);if (fd < 0) {perror("open fifo"); return -1; }puts("has opend fifo");while ((n = read(STDIN_FILENO, buf, 64)) > 0) {write(fd, buf, n);if (buf[0] == 'q') break;}close(fd);return 0;}

接收端

// recv.c#include <unistd.h>#include <sys/types.h>#include <sys/stat.h>#include <fcntl.h>#include <stdio.h>int main() {char buf[64];int n = 0;// 有名管道文件不存在则创建if (access("/home/skx/pra/learn_linux/52/hello", 0)) {if ((n = mkfifo("hello", 0664)) < 0)perror("mkfifo");}int fd = open("hello", O_RDONLY);if (fd < 0) {perror("open fifo"); return -1; }puts("has opened fifo");while ((n = read(fd, buf, 64)) > 0) {write(STDOUT_FILENO, buf, n);}if (n == 0) {puts("remote closed");}else {perror("read fifo");return -1;}close(fd);return 0;}

编译运行

$ gcc send.c -o send$ gcc recv.c -o recv

运行

$ ./send

因为 recv 端还没打开 hello 文件,这时候 send 是阻塞状态的。

再开启另一个终端:

$ ./recv

这时候 send 端和 recv 端都在终端显示has opend fifo

此时在 send 端输入数据,recv 打印。

4 System V共享内存

4.1 共享内存

方法如下:

根据已知的键(key) 使用 get 函数获取或者创建内核对象,并且返回内核对象的 id 号。根据 id 号获取内存地址。向内存读写数据。

内核对象,理解为位于内核空间中的结构体。

键值是事先约定好的。get函数是以get为后缀的函数名,共享内存里对应的函数为shmget,shm意思为share memory。

根据key获取的id 号,唯一的标识了内核中的一个对象。对于共享内存,可以用此id将内核对象中的内存挂接到用户空间的线性地址。

实验:该实例有两个程序,程序 a 创建一个共享内存的内核对象,获取内存后,向其写入数据。程序 b 获取此内核对象后挂接内存,读取数据然后打印。

程序 a

// a.c#include <unistd.h>#include <sys/ipc.h>#include <sys/shm.h>#include <stdio.h>#include <string.h>int main() {// shmget 函数通过事先约定的键值 0x8888 创建(IPC_CREAT)一个内核对象并返回其 id。如果 0x8888 对应的内核对象存在,就失败(IPC_EXCL)。0664 是该内核对象的权限。第二个参数表示创建的共享内存大小。int id = shmget(0x8888, 4096, IPC_CREAT | IPC_EXCL | 0664);// 如果失败就退出if (id < 0) {perror("shmget");return -1;}// 打印获取到的内核对象 idprintf("id = %d\n", id);// 使用函数 shmat (share memory attach) 将内核对象维护的内存挂接到指定线性地址(第二个参数)// 如果第二个参数为 0,则系统帮你选择一个合适的线性地址。char* buf = shmat(id, NULL, 0);// 如果挂接失败就退出if (buf == (char*)-1) {perror("shmat");return -1;}// 将数据拷贝到共享内存strcpy(buf, "hello, share memory!\n");// 使用 shmdt(share memory detach) 将挂接的内存卸载if (shmdt(buf) < 0) {perror("shmdt");return -1;}return 0;}

程序b

// b.c#include <unistd.h>#include <sys/ipc.h>#include <sys/shm.h>#include <stdio.h>#include <string.h>int main() {// 根据事先约定后的键值获取内核对象 id,这时候后面两个参数都可以为 0.int id = shmget(0x8888, 0, 0);if (id < 0) {perror("shmget");return -1;}printf("id = %d\n", id);// 挂接内存char* buf = shmat(id, NULL, 0);if (buf == (char*)-1) {perror("shmat");return -1;}// 打印数据printf("%s", buf);// 卸载if (shmdt(buf) < 0) {perror("shmdt");return -1;}// 删除内核对象if (shmctl(id, IPC_RMID, NULL) < 0) {perror("shmctl");return -1;}return 0;}

编译

$ gcc a.c -o a$ gcc b.c -o b

运行

$ ./a

如果你的程序运行成功,会在屏幕打印内核对象的 id 号。另外通过命令ipcs -m可以看到刚刚创建的共享内存内核对象。

运行程序 b

$ ./b

屏幕打印如下结果:

4.2 IPC 内核对象

IPC 内核对象都是位于内核空间中的一个结构体。使用get函数创建内核对象后,内核开辟一块内存。只要你不删除,就永远在内核中。

IPC 内核对象示意图

4.3 获取内核对象的id号

为了能够得到内核对象的 id 号,用户程序需要提供键值——key,它的类型是 key_t (int 整型)。系统调用函数(shmget, msgget 和 semget)根据 key,就可以查找到你需要的内核 id 号。在内核创建完成后,就已经有一个唯一的 key 值和它绑定起来了,也就是说 key 和内核对象是一一对应的关系。(key = 0 为特殊的键,它不能用来查找内核对象)

根据 key 获取内核对象 id 号

相同的key值,使用不用的get函数就能获取是内存内核对象的 id,还是消息队列的或者信号量的内核对象 id。

int id = shmget(0x8888, 0, 0); // 返回 0int id = msgget(0x8888, 0); // 返回 1int id = semget(0x8888, 0, 0); // 返回 4

用 key = 0 的键调用 get 后缀函数,将导致创建一个匿名内核对象而不是获取内核对象,这样的内核对象不绑定任何键值,这意味着你将无法通过 get 后缀函数来获取其 id。

4.4 创建IPC内核对象

在创建 IPC 内核对象时,要提供 key 值。

// 在 0x8888 这个键上创建内核对象,权限为 0644,如果已经存在就返回错误。int id = shmget(0x8888, 4096, IPC_CREAT | IPC_EXCL | 0644);int id = msgget(0x8888, IPC_CREAT | IPC_EXCL | 0644);int id = semget(0x8888, 1, IPC_CREAT | IPC_EXCL | 0644); //第二个参数表示创建几个信号量

4.5 shmget函数

int shmget(key_t key, size_t size, int flags);

参数 key:约定好的键值。

为 IPC_PRIVATE(这个宏被定义为 0),则表示创建一个新的内核对象并返回其 id 号。如果该值不等于 0,表示创建或者获取 IPC 内核对象的 id 号(具体是创建还是获取需要依据 shmflg 参数)。

参数 size

创建内核对象时才有效,表示创建共享内存的大小(一般设置为一页内存大小的整数倍,页面内存大小通过 getpagesize() 函数获取)。

参数 flags:可选项

IPC_CREAT:创建内核对象。如果内核对象已存在,且未指定 IPC_EXCL,就返回该内核对象的 id 号。

IPC_EXCL:总是搭配 IPC_CREAT 一起使用。如果设定该选项,当内核对象已存在就返回错误,同时 errno 设定为 EEXIST。

权限位:如果是创建新的内核对象,flags 还需要位或内核对象的权限位,比如 0664。

三个 System V IPC(shmget, msgget, semget) 都有参数 key 和 flags,用法都是一样的。

返回值:

成功返回内核对象id,失败返回-1。

如果要获取已存在的内核对象id,除了key,其它参数都为0。

实验:创建 ipc 内核对象

程序 ipccreate 用于在指定的键值上创建 ipc 内核对象。使用格式为./ipccreate <ipc type> <key>,比如./ipccreate 0 0x8888表示在键值 0x8888 上创建共享内存。具体运行方式看后面的示例。

// ipccreate.c#include <unistd.h>#include <sys/ipc.h>#include <sys/shm.h>#include <sys/msg.h>#include <sys/sem.h>#include <stdio.h>#include <stdlib.h>#include <string.h>int main(int argc, char* argv[]) {if (argc < 3) {printf("%s <ipc type> <key>\n", argv[0]);return -1;}key_t key = strtoll(argv[2], NULL, 16);char type = argv[1][0];char buf[64];int id;if (type == '0') {id = shmget(key, getpagesize(), IPC_CREAT | IPC_EXCL | 0644);strcpy(buf, "share memory");}else if (type == '1') {id = msgget(key, IPC_CREAT | IPC_EXCL | 0644);strcpy(buf, "message queue");}else if (type == '2') {id = semget(key, 5, IPC_CREAT | IPC_EXCL | 0644);strcpy(buf, "semaphore");}else {printf("type must be 0, 1, or 2\n");return -1;}if (id < 0) {perror("get error");return -1;}printf("create %s at 0x%x, id = %d\n", buf, key, id);return 0;}

编译和运行

$ gcc ipccreate.c -o ipccreate1$ ./ipccreate 0 0x1234// 创建共享内存$ ./ipccreate 1 0x1234 // 创建消息队列$ ./ipccreate 2 0x1234 // 创建信号量

运行结果

实验:获取 ipc 内核对象

程序 ipcget 用于在指定的键值上获取 ipc 内核对象的 id 号。使用格式为./ipcget <ipc type> <key>,比如./ipcget 0 0x8888表示获取键值 0x8888 上的共享内存 id 号。

// ipcget.c#include <unistd.h>#include <sys/ipc.h>#include <sys/shm.h>#include <sys/msg.h>#include <sys/sem.h>#include <stdio.h>#include <stdlib.h>#include <string.h>int main(int argc, char* argv[]) {if (argc < 3) {printf("%s <ipc type> <key>\n", argv[0]);return -1;}key_t key = strtoll(argv[2], NULL, 16);char type = argv[1][0];char buf[64];int id;if (type == '0') {id = shmget(key, 0, 0);strcpy(buf, "share memory");}else if (type == '1') {id = msgget(key, 0);strcpy(buf, "message queue");}else if (type == '2') {id = semget(key, 0, 0);strcpy(buf, "semaphore");}else {printf("type must be 0, 1, or 2\n");return -1;}if (id < 0) {perror("get error");return -1;}printf("get %s at 0x%x, id = %d\n", buf, key, id);return 0;}

编译和运行

$ gcc ipcget.c -o ipcget1$ ./ipcget 0 0x1234// 创建共享内存$ ./ipcget 1 0x1234 // 创建消息队列$ ./ipcget 2 0x1234 // 创建信号量

运行结果

创建的共享内存,消息队列,信号量可以通过命令ipcrm命令删除

4.6 键值与ftok

函数 ftok 可以根据路径和一个整数生成 key 值。如此你就可以约定好一个路径以及一个整数来取得相同的 key 了。

#include <sys/types.h>#include <sys/ipc.h>key_t ftok(const char *pathname, int proj_id);

参数 pathname 可以是目录路径,也可以是文件路径(随便什么类型的文件都可以)。

参数 proj_id 可以取任意整数。

实验:使用 ftok 生成 key 并将其打印到屏幕。

// ftok.c#include <unistd.h>#include <sys/types.h>#include <sys/ipc.h>#include <stdio.h>#include <stdlib.h>int main(int argc, char* argv[]) {if (argc < 3) {printf("usage: %s <path> <id>\n", argv[0]);return -1;}int id = atoi(argv[2]);key_t key = ftok(argv[1], id);if (key == -1) {perror("ftok");return -1;}printf("key = 0x%08x\n", key);return 0;}

编译和运行

$ gcc ftok.c -o ftok1$ touch tmp // 生成一个文件$ ./ftok tmp 10

ftok的算法

通过 stat 函数读取 pathname 的设备号和 inode 号,取设备号的低8位,inode 号的 低 16 位,以及 proj_id 的低 8 位组合成 key。

<proj_id 8 bit>-<dev 8 bit>-<inode 16 bit>

tmp 信息如下

可以看到 tmp 的设备号为 2049(十六进制为 0x801),inode 号为 930564(十六进制为 0xe3304),再根据 proj_id,图1 中使用的是 10,(十进制 0xa),分别取:

proj_id 的低 8 位—— 0x0a

设备号低 8 位 —— 0x01

inode 号低 16 位—— 0x3304

最后组合成成 key——0x0a013304。

4.7 shmget函数

黑色部分表示的是未分配的线性地址。

进程空间与物理页

shmget创建出共享内存,系统分配一个(或多个)物理页,具体分配多少看shmget 第二个参数。

shmat与 shmdt函数

void *shmat(int shmid, const void *shmaddr, int shmflg);int shmdt(const void *shmaddr);

shmadd为0,系统自动选合适的线性地址。shmflg:读写权限,为0可读写,为 SHM_RDONLY,只读。

shmat返回挂接的线性地址,

shmat 全称是 share memory attach,中译为共享内存挂接。

shmat函数原理

shmat从进程空间中选择一个合适的或者指定的线性地址,挂接到共享内存物理页上。

shmat 函数将线性地址挂接到物理页

4.8 shmctl

int shmctl(int shmid, int cmd, struct shmid_ds *buf);

shmid:ipc:内核对象id。

cmd:命令

为IPC_STAT时, 第三个buf接收返回值,为IPC_SET时,buf传递值并设置内核对象。为cmd = IPC_RMID时,buf为NULL,删除内核对象。

shmid_ds结构体

通将此结构体访问内核空间的ipc内核对象。

struct shmid_ds {struct ipc_perm shm_perm; /* 所有权和权限位 */size_tshm_segsz; /* 段大小 */time_tshm_atime; /* 最后挂接时间 */time_tshm_dtime; /* 最后卸载时间 */time_tshm_ctime; /* 最后改变当前结构体的时间(由IPC_SET命令改变) */pid_t shm_cpid; /* 创建 ipc 内核对象的进程 pid */pid_t shm_lpid; /* 最后执行 shmat/shmdt 的进程 pid */shmatt_t shm_nattch; /* 挂接进程个数 */...};

其中成员 shm_perm 是所有 System V IPC 内核对象都包含的,它的结构如下:

struct ipc_perm {uid_tuid;/* 所有者有效用户 id */gid_tgid;/* 所有者有效用户组 id */uid_tcuid;/* 创建者有效用户 id */gid_tcgid;/* 创建者有效用户组 id */unsigned short mode;/* 权限位*/};

实验

下面的程序 shmctl 可以用来创建、删除内核对象,也可以挂接、卸载共享内存,还可以打印、设置内核对象信息。具体使用方法具体见下面的说明:

./shmctl -c : 创建内核对象。./shmctl -d : 删除内核对象。./shmctl -v : 显示内核对象信息。./shmctl -s : 设置内核对象(将权限设置为 0600)。./shmctl -a : 挂接和卸载共享内存(挂接 5 秒后,再执行 shmdt,然后退出)。

// shmctl.c#include <unistd.h>#include <sys/ipc.h>#include <sys/shm.h>#include <stdio.h>#include <time.h>#include <stdlib.h>#include <string.h>#define ASSERT(res) if((res)<0){perror(__FUNCTION__);exit(-1);}// 打印 ipc_permvoid printPerm(struct ipc_perm* perm) {printf("euid of owner = %d\n", perm->uid);printf("egid of owner = %d\n", perm->gid);printf("euid of creator = %d\n", perm->cuid);printf("egid of creator = %d\n", perm->cgid);printf("mode = 0%o\n", perm->mode);}// 打印 ipc 内核对象信息void printShmid(struct shmid_ds* shmid) {printPerm(&shmid->shm_perm);printf("segment size = %d\n", shmid->shm_segsz);printf("last attach time = %s", ctime(&shmid->shm_atime));printf("last detach time = %s", ctime(&shmid->shm_dtime));printf("last change time = %s", ctime(&shmid->shm_ctime));printf("pid of creator = %d\n", shmid->shm_cpid);printf("pid of last shmat/shmdt = %d\n", shmid->shm_lpid);printf("No. of current attaches = %ld\n", shmid->shm_nattch);}// 创建 ipc 内核对象void create() {int id = shmget(0x8888, 123, IPC_CREAT | IPC_EXCL | 0664);printf("create %d\n", id);ASSERT(id);}// IPC_STAT 命令使用,用来获取 ipc 内核对象信息void show() {int id = shmget(0x8888, 0, 0);ASSERT(id);struct shmid_ds shmid;ASSERT(shmctl(id, IPC_STAT, &shmid));printShmid(&shmid);}// IPC_SET 命令使用,用来设置 ipc 内核对象信息void set() {int id = shmget(0x8888, 123, IPC_CREAT | 0664);ASSERT(id);struct shmid_ds shmid;ASSERT(shmctl(id, IPC_STAT, &shmid));shmid.shm_perm.mode = 0600;ASSERT(shmctl(id, IPC_SET, &shmid));printf("set %d\n", id);}// IPC_RMID 命令使用,用来删除 ipc 内核对象void rm() {int id = shmget(0x8888, 123, IPC_CREAT | 0664);ASSERT(id);ASSERT(shmctl(id, IPC_RMID, NULL));printf("remove %d\n", id);}// 挂接和卸载void at_dt() {int id = shmget(0x8888, 123, IPC_CREAT | 0664);ASSERT(id);char* buf = shmat(id, NULL, 0);if (buf == (char*)-1) ASSERT(-1);printf("shmat %p\n", buf);sleep(5); // 等待 5 秒后,执行 shmdtASSERT(shmdt(buf));printf("shmdt %p\n", buf);}int main(int argc, char* argv[]) {if (argc < 2) {printf("usage: %s <option -c -v -s -d -a>\n", argv[0]);return -1;}printf("I'm %d\n", getpid());if (!strcmp(argv[1], "-c")) {create();}else if (!strcmp(argv[1], "-v")) {show();}else if (!strcmp(argv[1], "-s")) {set();}else if (!strcmp(argv[1], "-d")) {rm();}else if (!strcmp(argv[1], "-a")) {at_dt();}return 0;}

创建内核对象

$ ./shmctl -cI'm 36732create 3801089$ ./shmctl -vI'm 36734euid of owner = 1000egid of owner = 1000euid of creator = 1000egid of creator = 1000mode = 0664segment size = 123last attach time = Thu Jan 1 07:00:00 1970last detach time = Thu Jan 1 07:00:00 1970last change time = Tue Sep 28 07:36:10 pid of creator = 36732pid of last shmat/shmdt = 0No. of current attaches = 0

设置内核对象,将内核对象权限设置为 0600

$ ./shmctl -sI'm 36749set 3801089$ ./shmctl -vI'm 36750euid of owner = 1000egid of owner = 1000euid of creator = 1000egid of creator = 1000mode = 0600segment size = 123last attach time = Thu Jan 1 07:00:00 1970last detach time = Thu Jan 1 07:00:00 1970last change time = Tue Sep 28 07:49:32 pid of creator = 36732pid of last shmat/shmdt = 0No. of current attaches = 0

先在另一个终端执行./shmctl -a,然后在当前终端执行./shmctl -v(注意手速,5秒内要搞定)。

上面的./shmctl -a结束后,再执行一次./shmctl -v.

5 System V消息队列

消息队列本质上是位于内核空间的链表,链表的每个节点都是一条消息。每一条消息都有自己的消息类型,消息类型用整数来表示,而且必须大于 0。

位于内核空间的消息队列

数字1表示类型为1的消息,数字2、3、4 类似。消息类型为0的链表记录了所有消息加入队列的顺序。

5.1 消息队列相关的函数

// 创建和获取 ipc 内核对象int msgget(key_t key, int flags);// 将消息发送到消息队列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);// 查看、设置、删除 ipc 内核对象(用法和 shmctl 一样)int msgctl(int msqid, int cmd, struct msqid_ds *buf);

5.2 消息数据格式

struct Msg{long type; // 消息类型。这个是必须的,而且值必须 > 0,这个值被系统使用// 消息正文,多少字节随你而定// ...}

只要保证首4字节是一个整数就行了,下面的都可以

struct Msg {long type;char name[20];int age;} msg;struct Msg {long type;int start;int end;} msg;

正文部分是什么数据类型都没关系,因为消息队列传递的是 2 进制数据,不一定非得是文本。

5.3 msgsnd函数

作用:用于将数据发送到消息队列。

int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);

msqid:ipc 内核对象 id

msgp:消息数据地址

msgsz:消息正文部分的大小(不包含消息类型)

msgflg:可选项

为0:如果消息队列空间不够,msgsnd 会阻塞。

为IPC_NOWAIT:直接返回,如果空间不够,设置errno为EAGIN。

返回值:0表示成功,-1失败

5.4 msgrcv函数

作用:从消息队列取出消息,并从消息队列里删除。

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

msqid:ipc 内核对象 id

msgp:用来接收消息数据地址

msgsz:消息正文部分的大小(不包含消息类型)

msgtyp:指定获取哪种类型的消息

msgtyp = 0:获取消息队列中的第一条消息

msgtyp > 0:获取类型为 msgtyp 的消息,若msgflg 为MSG_EXCEPT,获取除了 msgtyp 类型以外的消息。

msgtyp < 0:获取类型≤|msgtyp| 的消息。

msgflg:可选项。

为0:没有消息就阻塞。

IPC_NOWAIT:如果指定类型的消息不存在就立即返回,同时设置 errno 为 ENOMSG

MSG_EXCEPT:仅用于 msgtyp > 0 的情况。获取类型不为 msgtyp 的消息

MSG_NOERROR:如果消息数据正文内容大于 msgsz,就将消息数据截断为 msgsz

实验:程序 msg_send 和 msg_recv 分别用于向消息队列发送数据和接收数据。

msg_send.c

msg_send 程序定义了一个结构体 Msg,消息正文部分是结构体 Person。该程序向消息队列发送了 10 条消息。

// msg_send.c#include <unistd.h>#include <sys/ipc.h>#include <sys/msg.h>#include <stdio.h>#include <stdlib.h>#define ASSERT(prompt,res) if((res)<0){perror(#prompt);exit(-1);}typedef struct {char name[20];int age;}Person;typedef struct {long type;Person person;}Msg;int main(int argc, char* argv) {int id = msgget(0x8888, IPC_CREAT | 0664);ASSERT(msgget, id);Msg msg[10] = {{1, {"Luffy", 17}},{1, {"Zoro", 19}},{2, {"Nami", 18}},{2, {"Usopo", 17}},{1, {"Sanji", 19}},{3, {"Chopper", 15}},{4, {"Robin", 28}},{4, {"Franky", 34}},{5, {"Brook", 88}},{6, {"Sunny", 2}}};int i;for (i = 0; i < 10; ++i) {int res = msgsnd(id, &msg[i], sizeof(Person), 0);ASSERT(msgsnd, res);}return 0;}

第一次执行完 msg_send 后的消息队列msg_recv

msg_recv 程序接收一个参数,表示接收哪种类型的消息。比如./msg_recv 4表示接收类型为 4 的消息,并打印在屏幕。

// msg_recv.c#include <unistd.h>#include <sys/types.h>#include <sys/ipc.h>#include <sys/msg.h>#include <stdio.h>#include <stdlib.h>#include <errno.h>#define ASSERT(prompt,res) if((res)<0){perror(#prompt);exit(-1);}typedef struct {char name[20];int age;}Person;typedef struct {long type;Person person;}Msg;void printMsg(Msg* msg) {printf("{ type = %ld, name = %s, age = %d }\n",msg->type, msg->person.name, msg->person.age);}int main(int argc, char* argv[]) {if (argc < 2) {printf("usage: %s <type>\n", argv[0]);return -1;}// 要获取的消息类型long type = atol(argv[1]);// 获取 ipc 内核对象 idint id = msgget(0x8888, 0);// 如果错误就退出ASSERT(msgget, id);Msg msg;int res;while (1) {// 以非阻塞的方式接收类型为 type 的消息 res = msgrcv(id, &msg, sizeof(Person), type, IPC_NOWAIT);if (res < 0) {// 如果消息接收完毕就退出,否则报错并退出if (errno == ENOMSG) {printf("No message!\n");break;}else {ASSERT(msgrcv, res);}}// 打印消息内容printMsg(&msg);}return 0;}

编译

$ gcc msg_send.c -o msg_send$ gcc msg_recv.c -o msg_recv12

运行

先运行 msg_send,再运行 msg_recv。

接收所有消息

$ ./msg_send$ ./msg_recv 0{type = 1, name = Luffy, age = 17 }{type = 1, name = Zoro, age = 19 }{type = 2, name = Nami, age = 18 }{type = 2, name = Usopo, age = 17 }{type = 1, name = Sanji, age = 19 }{type = 3, name = Chopper, age = 15 }{type = 4, name = Robin, age = 28 }{type = 4, name = Franky, age = 34 }{type = 5, name = Brook, age = 88 }{type = 6, name = Sunny, age = 2 }No message!

接收类型为 4 的消息

$ ./msg_send$ ./msg_recv 4

接收类型小于等于 3 的所有消息

$ ./msg_recv -3

6 System V信号量

6.1 System V信号量简介

信号量是一种资源数量,你使用资源,信号量的值减少,归还资源,增多。

int semget(key_t key, int nsems, int semflg);// 创建信号量int semop(int semid, struct sembuf *sops, unsigned nsops);// 请求资源或归还资源int semctl(int semid, int semnum, int cmd);// 获取一个信号量int semctl(int semid, int semnum, int cmd, union semun buf);// 设置一个或多个信号量,获取所有信号量的值

semget 中的参数 nsems,表示你要创建几个信号量(即几个资源)。创建完成后,以后要操作哪个信号量,只要告诉信号量的索引号就行了。

semop 中的 sops 参数,该参数需要传递一个数组,nsops 表示数组的个数。数组元素是 sembuf 结构体,

struct sembuf {unsigned short sem_num; /* 要操作的信号量索引 */shortsem_op; /* > 0 归还资源数,< 0 请求资源数 */shortsem_flg; /* 可选项,操作的行为 */}

semctl 中的 semnum 参数,表示要操作哪个信号量。semctl 中的 union semun buf 参数依赖于 cmd 命令,具体如下:

union semun {int val; /* cmd = SETVAL */struct semid_ds *buf; /* cmd = IPC_STAT, IPC_SET */unsigned short *array; /* cmd = GETALL, SETALL */};

如何使用信号量

指定要创建的信号量的个数,创建信号量的 ipc 内核对象,获取 ipc 内核对象 id.使用 semctl 的 SETVAL 或者 SETALL 命令设置信号量的值(每种资源的个数)。使用 semop 对指定若干个信号量进行同时操作(请求资源或归还资源)。使用 semctl 的 IPC_RMID 命令删除信号量

6.2 创建和获取信号量

int semget(key_t key, int nsems, int semflg);

nsems:创建几个信号量。

例:创建一个 ipc 内核对象,包含 3 个信号量。

int id = semget(0x8888, 3, IPC_CREAT | IPC_EXCL | 0664);

6.3 设置和获取信号量值

这里主要使用函数 semctl,命令可以是 SETVAL,也可以是 SETALL。前者用来设置某个信号量的值,后者表示设置所有信号量的值。

int semctl(int semid, int semnum, int cmd, union semun);union semun {int val; /* Value for SETVAL */struct semid_ds *buf; /* Buffer for IPC_STAT, IPC_SET */unsigned short *array; /* Array for GETALL, SETALL */};

semnum:表示要操作哪个信号量,

cmd

SETVAL:设置某个信号量的值

SETALL:设置所有信号量的值。

例子:设置信号量

semctl(id, 2, SETVAL, 5); //设置第2个信号量的值为5:

设置所有信号量的值

unsigned short vals[3] = {3, 6, 9};//将3个信号量的值分别设置为3,6,9semctl(id, 0, SETALL, vals); //这时第二个参数被忽略

例子:获取信号量

获取第 1 个信号量的值

int val = semctl(id, 1, GETVAL); // 第4个参数不用写了,获取的值通过返回值返回

获取所有信号量的值

unsigned short vals[3];semctl(id, 0, GETALL, vals); // 这时第二个参数被忽略,获取的值保存到 vals 数组

6.4 请求和释放信号量

当请求的信号量值 > 0 时,semop 直接返回,否则阻塞,直到信号量值大于 0。

释放资源,semop 直接返回。

int semop(int semid, struct sembuf *sops, unsigned nsops);

该函数第二个参数是一个数组,第三个参数表示数组大小。第二个参数的结构体如下:

struct sembuf {unsigned short sem_num; /* semaphore number */shortsem_op; /* semaphore operation */shortsem_flg; /* operation flags */}

sops是数组,nsops为数组大小。

sem_num : 操作第几个信号量

sem_op:为一个短整型,操作完成后,这个数值加到信号量上

sem_flg:可选项,一般为 0

IPC_NOWAIT:无论请求的资源有没有,立即返回。

SEM_UNDO:如果设置该值,当进程结束后,该进程执行的所有的操作全部撤销。

例子:请求资源(P操作)

请求 3 个资源(将信号量值减 3)

struct sembuf op;op.sem_num = 2; // 请求第2个资源(信号量)op.sem_op = -3;op.sem_flg = 0;semop(id, &op, 1);

同时操作多个信号量

struct sembuf ops[3] = {{0, -1, 0},{1, -5, 0},{2, -3, 0}};semop(id, ops, 3);

例子:释放(归还)资源(V操作)

释放 2 个资源(将信号量值加 2)

struct sembuf op;op.sem_num = 1; // 请求第 1 个资源(信号量)op.sem_op = 2;op.sem_flg = 0;semop(id, &op, 1);

同时操作多个信号量

struct sembuf ops[3] = {{0, 4, 0}, // 释放 4 个 0 号资源{1, 2, 0}, // 释放 2 个 1 号资源{2, 5, 0} // 释放 5 个 2 号资源};semop(id, ops, 3);

实验

// semop.c#include <unistd.h>#include <sys/ipc.h>#include <sys/sem.h>#include <stdio.h>#include <stdlib.h>#define R0 0#define R1 1#define R2 2void printSem(int id) {unsigned short vals[3] = {0 };semctl(id, 3, GETALL, vals);printf("R0 = %d, R1= %d, R2 = %d\n\n", vals[0], vals[1], vals[2]);}int main() {int id = semget(0x8888, 3, IPC_CREAT | IPC_EXCL | 0664);// 打印信号量值puts("信号量初始值(默认值)");printSem(id);// 1. 设置第 2 个信号量值puts("1. 设置第 2 个信号量(R2)值为 20");semctl(id, 2, SETVAL, 20);printSem(id);// 2. 同时设置 3 个信号量的值puts("2. 同时设置 3 个信号量的值为 12, 5, 9");unsigned short vals[3] = {12, 5, 9 };semctl(id, 0, SETALL, vals);printSem(id);// 3. 请求 2 个 R0 资源puts("3. 请求 2 个 R0 资源");struct sembuf op1 = {0, -2, 0 };semop(id, &op1, 1);printSem(id);// 4. 请求 3 个 R1 和 5 个 R2puts("4. 请求 3 个 R1 和 5 个 R2");struct sembuf ops1[2] = {{1, -3, 0},{2, -5, 0}};semop(id, ops1, 2);printSem(id);// 5. 释放 2 个 R1puts("5. 释放 2 个 R1");struct sembuf op2 = {1, 2, 0 };semop(id, &op2, 1);printSem(id);// 6. 释放 1 个 R0, 1 个 R1,3 个 R2puts("6. 释放 1 个 R0, 1 个 R1,3 个 R2");struct sembuf ops2[3] = {{0, 1, 0},{1, 1, 0},{2, 3, 0}};semop(id, ops2, 3);printSem(id);// 7. 删除 ipc 内核对象puts("7. 删除 ipc 内核对象");semctl(id, 0, IPC_RMID);return 0;}

编译运行

$ gcc semop.c -o semop$ ./semop 信号量初始值(默认值)R0 = 0, R1= 0, R2 = 01. 设置第 2 个信号量(R2)值为 20R0 = 0, R1= 0, R2 = 202. 同时设置 3 个信号量的值为 12, 5, 9R0 = 12, R1= 5, R2 = 93. 请求 2 个 R0 资源R0 = 10, R1= 5, R2 = 94. 请求 3 个 R1 和 5 个 R2R0 = 10, R1= 2, R2 = 45. 释放 2 个 R1R0 = 10, R1= 4, R2 = 46. 释放 1 个 R0, 1 个 R1,3 个 R2R0 = 11, R1= 5, R2 = 77. 删除 ipc 内核对象

7 生产者消费者模型

PV原语:

P(S) 将资源S减 1,即 S = S - 1. 如果 S <= 0,该进程进入等待。

V(S):将资源S加 1,即 S = S + 1。

3个生产者和4个消费者 ,缓冲区为5

信号量MUTEX表示资源cake是否被占用,初始值为1。

信号量FULL表示蛋糕的个数,初始值为0。

信号量EMPTY表示空缓冲区的个数,初始值为5。

生产者进程

while(1) {P(EMPTY); // 减少一个空缓冲区个数P(MUTEX);if (cake < 5) {cake++;}V(MUTEX);V(FULL); // 增加一个蛋糕个数}

消费者进程

while(1) {P(FULL); // 减少一个蛋糕个数P(MUTEX);if (cake > 0) {cake--;}V(MUTEX);V(EMPTY); // 增加一个空缓冲区个数}

描述蛋糕个数的信号量FULL <= 0时,消费者执行到 P(FULL) 进入等待状态,不再被调度。当FULL > 0,才会被调度。避免CPU浪费。

实验:生产者消费者模型

头文件 semutil.h

// semutil.h#ifndef __SEMUTIL_H__#define __SEMUTIL_H__#include <unistd.h>#include <sys/ipc.h>#include <sys/sem.h>#include <stdio.h>#include <stdlib.h>#define ASSERT(prompt,res) if((res)<0){perror(#prompt);exit(-1);}/** Create* 创建和获取信号量 ipc 内核对象 id* count > 0 表示创建,count = 0 表示获取*/int C(int count);/** Set* 初始化第 semnum 个信号量的值为 val*/void S(int id, int semnum, int val);/** Get* 获取第 semnum 个信号量的值*/int G(int id, int semnum);/** Delete* 删除信号量内核对象*/void D(int id);/** 请求第 semnum 个信号量,将其值减 1*/void P(int id, int semnum);/** 归还第 semnum 个信号量,将其值加 1*/void V(int id, int semnum);#endif //__SEMUTIL_H__

实现文件 semutil.c

// semutil.c#include "semutil.h"int C(int count) {int id;if (count > 0)id = semget(0x8888, count, IPC_CREAT | IPC_EXCL | 0664);elseid = semget(0x8888, 0, 0);ASSERT(semget, id);return id;}void S(int id, int semnum, int val) {ASSERT(semctl, semctl(id, semnum, SETVAL, val));}void D(int id) {ASSERT(semctl, semctl(id, 0, IPC_RMID));}void P(int id, int semnum) {struct sembuf op;op.sem_num = semnum;op.sem_op = -1;op.sem_flg = 0;ASSERT(semop, semop(id, &op, 1));}void V(int id, int semnum) {struct sembuf op;op.sem_num = semnum;op.sem_op = 1;op.sem_flg = 0;ASSERT(semop, semop(id, &op, 1));}int G(int id, int semnum) {return semctl(id, semnum, GETVAL);}

程序代码

// pc.c#include "semutil.h"#include <string.h>#include <sys/shm.h>#define MUTEX 0#define FULL 1#define EMPTY 2static void init() {int id = C(3);S(id, MUTEX, 1); S(id, FULL, 0); S(id, EMPTY, 5); int shmid = shmget(0x8888, 4, IPC_CREAT | IPC_EXCL | 0664);ASSERT(shmget, shmid);int *cake= shmat(shmid, NULL, 0); if (cake == (int*)-1) ASSERT(shmat, -1); *cake = 0;ASSERT(shmdt, shmdt(cake));}static int getsemid() {return C(0);}static int getshmid() {int id = shmget(0x8888, 0, 0); ASSERT(shmget, id);return id; }static void release(int id) {D(id);ASSERT(shmctl, shmctl(getshmid(), IPC_RMID, NULL));}static void producer() {int id = getsemid();int shmid = getshmid();int *cake = shmat(shmid, NULL, 0); while(1) {P(id, EMPTY);P(id, MUTEX);printf("current cake = %d, ", *cake);(*cake)++;printf("produce a cake, ");printf("cake = %d\n", *cake);V(id, MUTEX);V(id, FULL);sleep(1);}shmdt(cake);}static void consumer() {int id = getsemid();int shmid = getshmid();int *cake = shmat(shmid, NULL, 0);int count = 10;while(count--) {P(id, FULL);P(id, MUTEX);printf("current cake = %d, ", *cake);(*cake)--;printf("consume a cake, ");printf("cake = %d\n", *cake);V(id, MUTEX);V(id, EMPTY);}shmdt(cake);}int main(int argc, char *argv[]) {if (argc < 2) {printf("usage: %s <option -b -d -p -c>\n", argv[0]);return -1;}if (!strcmp("-b", argv[1])) {init();}else if (!strcmp("-d", argv[1])) {release(getsemid());}else if (!strcmp("-p", argv[1])) {producer();}else if (!strcmp("-c", argv[1])) {consumer();}return 0;}

编译

$ gcc pc.c semuti.c -o pc

使用如下

./pc -b : 初始化 ipc 内核对象./pc -d : 删除 ipc 内核对象./pc -p : 启动生产者进程./pc -c : 启动消费者进程

本内容不代表本网观点和政治立场,如有侵犯你的权益请联系我们处理。
网友评论
网友评论仅供其表达个人看法,并不表明网站立场。