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 出一个子进程,通过无名管道向子进程发送字符,子进程收到数据后将字符串中的小写字符转换成大写并输出。
// 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,代表管道
- 文件大小是0
- fifo 文件需要有读写两端,否则在打开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;
}
// 打印获取到的内核对象 id
printf("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函数创建内核对象后,内核开辟一块内存。只要你不删除,就永远在内核中。
4.3 获取内核对象的id号
为了能够得到内核对象的 id 号,用户程序需要提供键值——key,它的类型是 key_t (int 整型)。系统调用函数(shmget, msgget 和 semget)根据 key,就可以查找到你需要的内核 id 号。在内核创建完成后,就已经有一个唯一的 key 值和它绑定起来了,也就是说 key 和内核对象是一一对应的关系。(key = 0 为特殊的键,它不能用来查找内核对象)
相同的key值,使用不用的get函数就能获取是内存内核对象的 id,还是消息队列的或者信号量的内核对象 id。
int id = shmget(0x8888, 0, 0); // 返回 0
int id = msgget(0x8888, 0); // 返回 1
int 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从进程空间中选择一个合适的或者指定的线性地址,挂接到共享内存物理页上。
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_t shm_segsz; /* 段大小 */
time_t shm_atime; /* 最后挂接时间 */
time_t shm_dtime; /* 最后卸载时间 */
time_t shm_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_t uid; /* 所有者有效用户 id */
gid_t gid; /* 所有者有效用户组 id */
uid_t cuid; /* 创建者有效用户 id */
gid_t cgid; /* 创建者有效用户组 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_perm
void 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 秒后,执行 shmdt
ASSERT(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 -c
I'm 36732
create 3801089
$ ./shmctl -v
I'm 36734
euid of owner = 1000
egid of owner = 1000
euid of creator = 1000
egid of creator = 1000
mode = 0664
segment size = 123
last attach time = Thu Jan 1 07:00:00 1970
last detach time = Thu Jan 1 07:00:00 1970
last change time = Tue Sep 28 07:36:10 2021
pid of creator = 36732
pid of last shmat/shmdt = 0
No. of current attaches = 0
设置内核对象,将内核对象权限设置为 0600
$ ./shmctl -s
I'm 36749
set 3801089
$ ./shmctl -v
I'm 36750
euid of owner = 1000
egid of owner = 1000
euid of creator = 1000
egid of creator = 1000
mode = 0600
segment size = 123
last attach time = Thu Jan 1 07:00:00 1970
last detach time = Thu Jan 1 07:00:00 1970
last change time = Tue Sep 28 07:49:32 2021
pid of creator = 36732
pid of last shmat/shmdt = 0
No. 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_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 内核对象 id
int 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; /* 要操作的信号量索引 */
short sem_op; /* > 0 归还资源数,< 0 请求资源数 */
short sem_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,9
semctl(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 */
short sem_op; /* semaphore operation */
short sem_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 2
void 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 个 R2
puts("4. 请求 3 个 R1 和 5 个 R2");
struct sembuf ops1[2] = {
{1, -3, 0},
{2, -5, 0}
};
semop(id, ops1, 2);
printSem(id);
// 5. 释放 2 个 R1
puts("5. 释放 2 个 R1");
struct sembuf op2 = { 1, 2, 0 };
semop(id, &op2, 1);
printSem(id);
// 6. 释放 1 个 R0, 1 个 R1,3 个 R2
puts("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 = 0
1. 设置第 2 个信号量(R2)值为 20
R0 = 0, R1= 0, R2 = 20
2. 同时设置 3 个信号量的值为 12, 5, 9
R0 = 12, R1= 5, R2 = 9
3. 请求 2 个 R0 资源
R0 = 10, R1= 5, R2 = 9
4. 请求 3 个 R1 和 5 个 R2
R0 = 10, R1= 2, R2 = 4
5. 释放 2 个 R1
R0 = 10, R1= 4, R2 = 4
6. 释放 1 个 R0, 1 个 R1,3 个 R2
R0 = 11, R1= 5, R2 = 7
7. 删除 ipc 内核对象
7 生产者消费者模型
PV 原语:
-
P(S) 将资源S减 1,即 S = S - 1. 如果 S <= 0,该进程进入等待。
-
V(S):将资源S加 1,即 S = S + 1。
信号量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);
else
id = 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 2
static 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 : 启动消费者进程