1. 基本概念
何为进程间通信:
进程间通信 (Interprocess Communication, IPC) 是指两个,
或多个进程之间进行数据交换的过程。
进程间通信分类:
1) 简单进程间通信:命令行参数(单向)、环境变量(单向)、信号(双向)、文件(双向)。
2) 传统进程间通信:管道 (fifo/pipe)。
fifo:有名管道;pipe:无名管道/匿名管道;
3) XSI 进程间通信:共享内存、消息队列、信号量。
4) 网络进程间通信:套接字。
2. 传统进程间通信——管道
1)管道是 Unix 系统最古老的进程间通信方式。
2)历史上的管道通常是指半双工管道,只允许数据单向流动。
现代系统大都提供全双工管道,数据可以沿着管道双向流动。
3)有名管道 (fifo):基于有名文件 (管道文件) 的管道通信。
a. 命令形式
# mkfifo fifo // 创建管道文件
# echo hello > fifo // hello 输入重定向到 fifo 里,如果不读取,会处于阻塞状态;
此时程序处于等待状态,可以在另一个窗口里输入以下命令:
# cat fifo
输出:hello
同时,上面窗口自动退出;
b. 编程模型
--------+-------------+-----------------+-------------+------
步骤| 进程 A | 函数 | 进程 B | 步骤
--------+-------------+-----------------+-------------+------
1 | 创建管道 | mkfifo | --- |
2 | 打开管道 | open | 打开管道 | 1
3 | 读写管道 | read/write | 读写管道 | 2
4 | 关闭管道 | close | 关闭管道 | 3
5 | 删除管道 | unlink | --- |
--------+-------------+-----------------+-------------+------
范例:wfifo.c(写管道)
#include <stdio.h>
#include <fcntl.h>
#include <string.h>
#include <sys/stat.h>
// 定义一个管道文件宏,用于创建管道;
#define FIFO_FILE "/tmp/fifo"
int main (void) {
printf ("创建管道...\n");
// 创建管道,不返回文件描述符
if (mkfifo (FIFO_FILE, 0666) == -1) {
perror ("mkfifo");
return -1;
}
printf ("打开管道...\n");
// open 打开文件,返回文件描述符
int fd = open (FIFO_FILE, O_WRONLY);
if (fd == -1) {
perror ("open");
return -1;
}
// 如果此时下面 rfifo.c 文件没打开,程序会阻塞在此;
printf ("发送数据...\n");
for (;;) {
printf ("> ");
// 等待键盘输入字符
char buf[1024];
gets (buf); // 读取字符串;
// 如果输入感叹号退出
if (! strcmp (buf, "!"))
break;
// 逐个字符写入管道
if (write (fd, buf, (strlen (buf) + 1) *
sizeof (buf[0])) == -1) {
perror ("write");
return -1;
}
}
printf ("关闭管道...\n");
if (close (fd) == -1) {
perror ("close");
return -1;
}
// 关闭后还需要删除管道
printf ("删除管道...\n");
if (unlink (FIFO_FILE) == -1) {
perror ("unlink");
return -1;
}
printf ("大功告成!\n");
return 0;
}
范例:rfifo.c(读管道)
#include <stdio.h>
#include <fcntl.h>
#define FIFO_FILE "/tmp/fifo"
int main (void) {
printf ("打开管道...\n");
// wfifo.c 已经创建了管道,这里只需要打开即可;
int fd = open (FIFO_FILE, O_RDONLY);
if (fd == -1) {
perror ("open");
return -1;
}
printf ("接收数据...\n");
for (;;) {
char buf[1024];
ssize_t rb = read (fd, buf, sizeof (buf));
if (rb == -1) {
perror ("read");
return -1;
}
if (! rb) // read 返回 0 表示那边管道被关闭
break;
printf ("< %s\n", buf);
}
printf ("关闭管道...\n");
// 关闭这边管道
if (close (fd) == -1) {
perror ("close");
return -1;
}
// 不需要删除,誰建谁删;
printf ("大功告成!\n");
return 0;
}
分析:fifo 必须两头都打开的时候,才会进行读写;
进一个数据,就出一个数据,不具备保存机制;
如果打开两个 rfifo.c 的时候,那么就无法确定是哪个 rififo.c 在读取;
两个随机读取 wfifo.c 写入的数据;
4)无名管道 (pipe):适用于父子进程之间的通信。
#include <unistd.h>
int pipe (int pipefd[2]);
成功返回 0,失败返回 -1。
通过输出参数 pipefd 返回两个文件描述符,
其中 pipefd[0] 用于读,pipefd[1] 用于写。
一般用法:
A. 调用该函数在内核中创建管道文件,并通过其输出参数,
获得分别用于读和写的两个文件描述符;
B. 调用 fork 函数,创建子进程;
C. 写数据的进程关闭读端 (pipefd[0]),
读数据的进程关闭写端 (pipefd[1]);
D. 传输数据;
E. 父子进程分别关闭自己的文件描述符。
范例:pipe.c
#include <stdio.h>
#include <string.h>
#include <unistd.h>
int main (void) {
printf ("父进程:创建管道...\n");
// 首先创建包含两个文件描述符的数组;
int pipefd[2];
if (pipe (pipefd) == -1) {
perror ("pipe");
return -1;
}
printf ("父进程:创建进程...\n");
pid_t pid = fork ();
if (pid == -1) {
perror ("fork");
return -1;
}
// 父进程负责写,子进程负责读
if (pid == 0) {
printf ("子进程:关闭写端...\n");
close (pipefd[1]);
printf ("子进程:接收数据...\n");
// 通过 for 循环不停的读取数据
for (;;) {
char buf[1024];
ssize_t rb = read (pipefd[0], buf, sizeof (buf)); // 子进程焦点
if (rb == -1) {
perror ("read");
return -1;
}
// rb 等于零,说明写端被关闭
if (! rb)
break; // 关闭子进程
// 否则打印
puts (buf);
}
printf ("子进程:关闭读端...\n");
close (pipefd[0]);
printf ("子进程:大功告成!\n");
return 0;
}
// 父进程负责写,所以先关闭读端;
printf ("父进程:关闭读端...\n");
close (pipefd[0]);
printf ("父进程:发送数据...\n");
// 每读一个字符串就发送一个,直到读到感叹号;
for (;;) {
char buf[1024];
gets (buf); // 父进程焦点
if (! strcmp (buf, "!"))
break;
// 把管道当作文件处理,所以可以使用 write;
if (write (pipefd[1], buf, (strlen (buf) + 1) *
sizeof (buf[0])) == -1) {
perror ("write");
return -1;
}
}
printf ("父进程:关闭写端...\n");
close (pipefd[1]);
if (wait (0) == -1) {
perror ("wait");
return -1;
}
printf ("父进程:大功告成!\n");
return 0;
}
注意:无名管道不创建文件;
测试这个文件的时候,不需要打开俩个窗口;
当出现感叹号的时候,父进程先关闭,然后子进程 rb 收到 0,也跟着关闭;
3. XSI 进程间通信
1)IPC 标识
内核为每个进程间通信维护一个结构体形式的 IPC 对象。
该对象可通过一个非负整数的 IPC 标识来引用。
与文件描述符不同,IPC 标识在使用时会持续加 1,
当达到最大值时,向 0 回转。
2)IPC 键值
IPC 标识是 IPC 对象的内部名称。
若多个进程需要在同一个 IPC 对象上会合,
则必须通过键值作为其外部名称来引用该对象。
a. 无论何时,只要创建 IPC 对象,就必须指定一个键值。
b. 键值的数据类型在 sys/types.h 头文件中被定义为 key_t,其原始类型就是长整型。
3)客户机进程与服务器进程在 IPC 对象上的三种会合方式:
a. 服务器进程以 IPC_PRIVATE 为键值创建一个新的 IPC 对象,
并将该 IPC 对象的标识存放在某处 (如文件中),以方便客户机进程读取。
b. 在一个公共头文件中,定义一个客户机进程和服务器进程都认可的键值,
服务器进程用此键值创建 IPC 对象,客户机进程用此键值获取该 IPC 对象。
c. 客户机进程和服务器进程,事先约定好一个路径名和一个项目 ID(0-255),
二者通过 ftok 函数,将该路径名和项目 ID 转换为一致的键值。
#include <sys/types.h>
#include <sys/ipc.h>
key_t ftok (const char* pathname, int proj_id);
pathname - 一个真实存在的文件或目录的路径名。
proj_id - 项目 ID,仅低 8 位有效,其值域为 [0, 255]。
成功返回键值,失败返回 -1。
注意:起作用的是 pathname 参数所表示的路径,而非 pathname 字符串本身。
因此假设当前目录是 /home/soft01/uc/day07,则
ftok (".", 100);
和
ftok ("/home/soft01/uc/day07", 100);
的返回值完全相同。
4)IPC 对象的创建
a. 若以 IPC_PRIVATE 为键值创建 IPC 对象,则永远创建成功。
b. 若所指定的键值在系统范围内未与任何 IPC 对象相结合,
且创建标志包含 IPC_CREAT 位,则创建成功。
c. 若所指定的键值在系统范围内已与某个 IPC 对象相结合,
且创建标志包含 IPC_CREAT 和 IPC_EXCL 位,则创建失败。
5)IPC 对象的销毁/控制
IPC_STAT - 获取 IPC 对象属性
IPC_SET - 设置 IPC 对象属性
IPC_RMID - 删除 IPC 对象
4. 共享内存
1)基本特点
a. 两个或者更多进程,共享同一块由系统“内核“负责维护的内存区域,位于内核里;
其地址空间通常被映射到堆和栈之间的共享内存区域。
b. 优点:无需复制信息,共享内存直接映射到内核,最快的一种 IPC 机制。
c. 缺点:需要考虑同步访问的问题;也就是说,共享内存无法保证一定是写完以后再读;
而 fifo 等不需要考虑,自身控制;所以一般小数据同步用共享内存,而大数据量用管道或者消息队列等;
d. 内核为每个共享内存,维护一个 shmid_ds 结构体形式的共享内存对象。
2)常用函数
#include <sys/shm.h>
创建/获取共享内存:
=========
int shmget (key_t key, size_t size, int shmflg);
A. 该函数以 key 参数为键值创建共享内存,或获取已有的共享内存。
B. size 参数为共享内存的字节数,建议取内存页字节数 (4096) 的整数倍。
若希望创建共享内存,则必需指定 size 参数。
若只为获取已有的共享内存,则 size 参数可取 0。
C. shmflg 取值:
0 - 获取,不存在即失败。
IPC_CREAT - 创建,不存在即创建,已存在即获取,除非 . . .
IPC_EXCL - 排斥,已存在即失败。
D. 成功返回共享内存标识(int),失败返回 -1。
tips:创建共享内存是在内核里面创建,
下面加载共享内存是将内核里的内存和堆栈之间的内存建立映射;
加载(映射)共享内存:
==========
void* shmat (int shmid, const void* shmaddr, int shmflg);
A. 将 shmid 参数所标识的共享内存,映射到调用进程的地址空间。
B. 可通过 shmaddr 参数人为指定映射地址,也可将该参数置 NULL,由系统自动选择。
C. shmflg 取值:
0 - 以读写方式使用共享内存。
SHM_RDONLY - 以只读方式使用共享内存。
SHM_RND - 只在 shmaddr 参数非 NULL 时起作用。
表示对该参数向下取内存页的整数倍,作为映射地址。
D. 成功返回映射地址,失败返回 -1。
E. 内核将该共享内存的加载计数加 1;每调用一次 shmid,就 +1;
卸载(解除映射)共享内存:
============
int shmdt (const void* shmaddr);
A. 从调用进程的地址空间中,取消由 shmaddr 参数所指向的,共享内存映射区域。
B. 成功返回 0,失败返回 -1。
C. 内核将该共享内存的加载计数减 1。
销毁/控制共享内存:
=========
int shmctl (int shmid, int cmd, struct shmid_ds* buf);
struct shmid_ds {
struct ipc_perm shm_perm; // 所有者及其权限
size_t shm_segsz; // 大小(以字节为单位)
time_t shm_atime; // 最后加载时间
time_t shm_dtime; // 最后卸载时间
time_t shm_ctime; // 最后改变时间
pid_t shm_cpid; // 创建进程 PID
pid_t shm_lpid; // 最后加载/卸载进程 PID
shmatt_t shm_nattch; // 当前加载计数
. . .
};
struct ipc_perm {
key_t __key; // 键值
uid_t uid; // 有效属主 ID
gid_t gid; // 有效属组 ID
uid_t cuid; // 有效创建者 ID
gid_t cgid; // 有效创建组 ID
unsigned short mode; // 权限字
unsigned short __seq; // 序列号
};
A. cmd 取值:
IPC_STAT - 获取共享内存的属性,通过 buf 参数输出。
IPC_SET - 设置共享内存的属性,通过 buf 参数输入,仅以下三个属性可设置:
shmid_ds::shm_perm.uid
shmid_ds::shm_perm.gid
shmid_ds::shm_perm.mode
IPC_RMID - 标记删除共享内存。
并非真正删除共享内存,只是做一个删除标记,
禁止其被继续加载,但已有加载依然保留。
只有当该共享内存的加载计数为 0 时,才真正被删除。
B. 成功返回 0,失败返回 -1。
3)编程模型
--------+-----------------+-------------+-----------------+------
步骤| 进程 A | 函数 | 进程 B | 步骤
--------+-----------------+-------------+-----------------+------
1 | 创建共享内存 | shmget | 获取共享内存 | 1
2 | 加载共享内存 | shmat | 加载共享内存 | 2
3 | 使用共享内存 | . . . | 使用共享内存 | 3
4 | 卸载共享内存 | shmdt | 卸载共享内存 | 4
5 | 销毁共享内存 | shctl | --- |
--------+-----------------+-------------+-----------------+------
范例:wshm.c
#include <stdio.h>
#include <sys/shm.h>
int main (void) {
printf ("创建共享内存...\n");
// 创建共享内存之前先得有 key,通过路径+项目 ID 的方式创建;
key_t key = ftok (".", 100);
if (key == -1) {
perror ("ftok");
return -1;
}
// 创建共享内存
int shmid = shmget (key, 4096, 0644 | IPC_CREAT | IPC_EXCL);
// 4096 页大小,0644 权限,如果没有就创建,如果有就报错;
if (shmid == -1) {
perror ("shmget");
return -1;
}
printf ("加载共享内存...\n");
// 加载共享内存,返回共享内存的地址;返回 -1 代表失败;
void* shmaddr = shmat (shmid, NULL, 0);
if (shmaddr == (void*)-1) {
// 左边指针,右边 int,所以需要类型强转;
perror ("shmat");
return -1;
}
printf ("写入共享内存...\n");
sprintf (shmaddr, "我是%u进程写入的数据。", getpid ());
// 等一下,让下面进程 rshm.c 使用共享内存;
printf ("按<回车>卸载共享内存(0x%08x/%d)...", key, shmid);
getchar ();
// 卸载共享内存;
if (shmdt (shmaddr) == -1) {
perror ("shmdt");
return -1;
}
// 同样等一下再销毁共享内存;
printf ("按<回车>销毁共享内存(0x%08x/%d)...", key, shmid);
getchar ();
// 销毁共享内存;
if (shmctl (shmid, IPC_RMID, NULL) == -1) {
perror ("shmctl");
return -1;
}
printf ("大功告成!\n");
return 0;
}
查看共享内存:
# ipcs -m
输出如下:
key shmid owner perms bytes nattch status
0x00000000 229377 tarena 600 393216 2 dest
0x00000000 262146 tarena 600 393216 2 dest
. . .
范例:rshm.c
#include <stdio.h>
#include <sys/shm.h>
// 打印共享内存信息
int shmstat (int shmid) {
struct shmid_ds shm;
if (shmctl (shmid, IPC_STAT, &shm) == -1) {
perror ("shmctl");
return -1;
}
printf ("------------------------------------------------\n");
printf (" 共享内存信息\n");
printf ("----+--------------+--------------------------\n");
printf (" 所 | 键值 | 0x%08x\n", shm.shm_perm.__key);
printf (" 有 | 有效属主ID | %u\n", shm.shm_perm.uid);
printf (" 者 | 有效属组ID | %u\n", shm.shm_perm.gid);
printf (" 及 | 有效创建者ID | %u\n", shm.shm_perm.cuid);
printf (" 其 | 有效创建组ID | %u\n", shm.shm_perm.cgid);
printf (" 权 | 权限字 | %#o\n", shm.shm_perm.mode);
printf (" 限 | 序列号 | %u\n", shm.shm_perm.__seq);
printf ("---+---------------+--------------------------\n");
printf (" 大小(字节) | %u\n", shm.shm_segsz);
printf (" 最后加载时间 | %s", ctime (&shm.shm_atime));
printf (" 最后卸载时间 | %s", ctime (&shm.shm_dtime));
printf (" 最后改变时间 | %s", ctime (&shm.shm_ctime));
printf (" 创建进程ID | %u\n", shm.shm_cpid);
printf (" 最后加载/卸载进程ID | %u\n", shm.shm_lpid);
printf (" 当前加载计数 | %u\n", shm.shm_nattch);
printf ("-------------------+--------------------------\n");
return 0;
}
// 修改共享内存属性
int shmset (int shmid) {
struct shmid_ds shm;
// 先取出当前的属性
if (shmctl (shmid, IPC_STAT, &shm) == -1) {
perror ("shmctl");
return -1;
}
// 修改属性,只能修改 gid,uid 和 mode 三个属性;
shm.shm_perm.mode = 0600;
shm.shm_segsz = 8192; // 修改失败,创建后无法改变大小;
if (shmctl (shmid, IPC_SET, &shm) == -1) {
perror ("shmctl");
return -1;
}
return 0;
}
int main (void) {
printf ("获取共享内存...\n");
// 获取共享内存,不用创建,用同样的路径和项目 ID 就可以获取;
key_t key = ftok (".", 100);
if (key == -1) {
perror ("ftok");
return -1;
}
int shmid = shmget (key, 0, 0);
if (shmid == -1) {
perror ("shmget");
return -1;
}
printf ("加载共享内存...\n");
void* shmaddr = shmat (shmid, NULL, 0);
if (shmaddr == (void*)-1) {
perror ("shmat");
return -1;
}
shmstat (shmid);
printf ("读取共享内存...\n");
printf ("共享内存(0x%08x/%d):%s\n", key, shmid, shmaddr);
printf ("卸载共享内存...\n");
if (shmdt (shmaddr) == -1) {
perror ("shmdt");
return -1;
}
shmstat (shmid);
printf ("设置共享内存...\n");
shmset (shmid);
shmstat (shmid);
printf ("大功告成!\n");
return 0;
}
5. 消息队列
1)基本特点
a. 消息队列是一个由系统内核负责存储和管理(位于内核里),
并通过消息队列标识引用的数据链表;
而有名管道一次只能放一个数据;
b. 可以通过 msgget 函数创建一个新的消息队列,或获取一个已有的消息队列。
通过 msgsnd 函数向消息队列的后端追加消息,
通过 msgrcv 函数从消息队列的前端提取消息。
c. 消息队列中的每个消息单元除包含消息数据外,还包含消息类型和数据长度。
d. 内核为每个消息队列,维护一个 msqid_ds 结构体形式的消息队列对象。
2)常用函数
#include <sys/msg.h>
创建/获取消息队列
=========
int msgget (key_t key, int msgflg);
A. 该函数以 key 参数为键值创建消息队列,或获取已有的消息队列。
B. msgflg 取值:
0 - 获取,不存在即失败。
IPC_CREAT - 创建,不存在即创建,已存在即获取,除非 . . .
IPC_EXCL - 排斥,已存在即失败。
C. 成功返回消息队列标识 msqid,失败返回 -1。
向消息队列发送消息
=========
int msgsnd (int msqid, const void* msgp, size_t msgsz, int msgflg);
A. msgp 参数指向一个包含消息类型和消息数据的内存块(一般是一个结构体,把地址返回给 void*)。
该内存块的前 4 个字节必须是一个大于 0 的整数,代表消息类型,其后紧跟消息数据。
消息数据的字节长度用 msgsz 参数表示。
+----------------+-----------------+
msgp -> | 消息类型 (>0) | 消息数据 |
+----------------+-----------------+
|<----- 4 ----->|<---- msgsz ---->|
注意:msgsz 参数并不包含消息类型的字节数 (4)。
B. 若内核中的消息队列缓冲区有足够的空闲空间,则此函数会将消息拷入该缓冲区并立即返回 0,表示发送成功;
否则此函数会阻塞,直到内核中的消息队列缓冲区有足够的空闲空间为止 (比如有消息被接收)。
C. 若 msgflg 参数包含 IPC_NOWAIT 位,则当内核中的消息队列缓冲区没有足够的空闲空间时,
此函数不会阻塞,而是返回 -1,errno 为 EAGAIN。
D. 成功返回 0,失败返回 -1。
从消息队列接收消息
=========
ssize_t msgrcv (int msqid, void* msgp, size_t msgsz, long msgtyp, int msgflg);
A. msgp参数指向一个包含消息类型 (4 字节),和消息数据的内存块,
其中消息数据缓冲区的字节大小用 msgsz 参数表示。
B. 若所接收到的消息数据字节数大于 msgsz 参数,
即消息太长,且 msgflg 参数包含 MSG_NOERROR 位,
则该消息被截取 msgsz 字节返回,剩余部分被丢弃。
C. 若 msgflg 参数不包含 MSG_NOERROR 位,消息又太长,
则不对该消息做任何处理,直接返回 -1,errno 为 E2BIG。
D. msgtyp 参数表示期望接收哪类消息:
=0 - 返回消息队列中的第一条消息(无论是什么消息类型,只拿第一个消息)。
>0 - 若 msgflg 参数不包含 MSG_EXCEPT 位,则返回消息队列中第一个类型为 msgtyp 的消息;
若 msgflg 参数包含 MSG_EXCEPT 位,则返回消息队列中第一个类型不为 msgtyp 的消息。
<0 - 返回消息队列中类型小于等于 msgtyp 的绝对值的消息;若有多个,则取类型最小者。
+-----+-----+-----+-----+-----+-----+-----+-----+-----+
| 3 | 4 | 2 | 1 | 3 | 2 | 4 | 3 | 3 |
-> rear +- -+- -+- -+- -+- -+- -+- -+- -+- -+ front ->
| . . . | . . . | . . . | . . . | . . . | . . . | . . . | . . . | . . . |
+-----+-----+-----+-----+-----+-----+-----+-----+-----+
^ ^ ^ ^
4 3 2 1
msgrcv (..., ..., ..., 3, ...);
E. 若消息队列中有可接收消息,则此函数会将该消息移出消息队列并立即返回 0,表示接收成功,
否则此函数会阻塞,直到消息队列中有可接收消息为止。
F. 若 msgflg 参数包含 IPC_NOWAIT 位,
则当消息队列中没有可接收消息时,此函数不会阻塞,
而是返回 -1,errno 为 ENOMSG。
G. 成功返回所接收到的消息数据的字节数,失败返回 -1。
销毁/控制消息队列
=========
int msgctl (int msqid, int cmd, struct msqid_ds* buf);
struct msqid_ds {
struct ipc_perm msg_perm; // 权限信息
time_t msg_stime; // 随后发送时间
time_t msg_rtime; // 最后接收时间
time_t msg_ctime; // 最后改变时间
unsigned long __msg_cbytes; // 消息队列中的字节数
msgqnum_t msg_qnum; // 消息队列中的消息数
msglen_t msg_qbytes; // 消息队列能容纳的最大字节数
pid_t msg_lspid; // 最后发送进程PID
pid_t msg_lrpid; // 最后接收进程PID
};
struct ipc_perm {
key_t __key; // 键值
uid_t uid; // 有效属主ID
gid_t gid; // 有效属组ID
uid_t cuid; // 有效创建者ID
gid_t cgid; // 有效创建组ID
unsigned short mode; // 权限字
unsigned short __seq; // 序列号
};
A. cmd 取值:
IPC_STAT - 获取消息队列的属性,通过 buf 参数输出。
IPC_SET - 设置消息队列的属性,通过 buf 参数输入,仅以下四个属性可设置:
msqid_ds::msg_perm.uid
msqid_ds::msg_perm.gid
msqid_ds::msg_perm.mode
msqid_ds::msg_qbytes
IPC_RMID - 立即删除消息队列。
此时所有阻塞在对该消息队列的,msgsnd 和 msgrcv 函数调用,
msgsnd 和 msgrcv 都会立即返回失败,errno 为 EIDRM。
B. 成功返回 0,失败返回 -1。
3)编程模型
--------+-----------------+-----------------+-----------------+------
步骤| 进程 A | 函数 | 进程 B | 步骤
--------+-----------------+-----------------+-----------------+------
1 | 创建消息队列 | msgget | 获取消息队列 | 1
2 | 发送接受消息 | msgsnd/msgrcv | 发送接受消息 | 2
3 | 销毁消息队列 | msgctl | --- |
--------+-----------------+-----------------+-----------------+------
范例:wmsq.c
#include <stdio.h>
#include <string.h>
#include <sys/msg.h>
int main (void) {
printf ("创建消息队列...\n");
key_t key = ftok (".", 100);
if (key == -1) {
perror ("ftok");
return -1;
}
int msqid = msgget (key, 0644 | IPC_CREAT | IPC_EXCL);
if (msqid == -1) {
perror ("msqget");
return -1;
}
printf ("向消息队列(0x%08x/%d)发送数据...\n", key, msqid);
for (;;) {
printf ("> ");
// 发送消息是一个包一个包的发送,所以先定义消息包;
struct {
long mtype; // 消息类型
char mtext[1024]; // 消息数据
} msgbuf = {1234, ""};
gets (msgbuf.mtext); // 获取消息数据
if (! strcmp (msgbuf.mtext, "!"))
break;
// 发送消息
if (msgsnd (msqid, &msgbuf, (strlen (msgbuf.mtext) + 1) *
sizeof (msgbuf.mtext[0]), 0) == -1) {
// strlen 实际发送长度 + 1 位 \0;如果直接写为 sizeof (msgbuf.mtext); 则直接发送 1024 大小;
// 最后一位是 msgflg,为零表示没空间会阻塞;如果设为 IPC_NOWAIT,则不会阻塞;
perror ("msgsnd");
return -1;
}
}
printf ("销毁消息队列(0x%08x/%d)...\n", key, msqid);
if (msgctl (msqid, IPC_RMID, NULL) == -1) {
perror ("msgctl");
return -1;
}
printf ("大功告成!\n");
return 0;
}
查看消息队列:
# ipcs -q
范例:rmsq.c
#include <stdio.h>
#include <errno.h>
#include <sys/msg.h>
int main (void) {
printf ("获取消息队列...\n");
key_t key = ftok (".", 100);
if (key == -1) {
perror ("ftok");
return -1;
}
int msqid = msgget (key, 0);
// 只获取,不创建,所以第二参数为零;
if (msqid == -1) {
perror ("msgget");
return -1;
}
printf ("从消息队列(0x%08x/%d)接收消息...\n", key, msqid);
for (;;) {
// 接收的也是消息结构,所以先定义结构体;
struct {
long mtype;
char mtext[1024];
} msgbuf = {};
ssize_t msgsz = msgrcv (msqid, &msgbuf,
sizeof (msgbuf.mtext) - sizeof (msgbuf.mtext[0]), 1234,
MSG_NOERROR/* | IPC_NOWAIT*/);
/*
此时为阻塞模式,如果或上 IPC_NOWAIT 则是非阻塞模式;
MSG_NOERROR 如果数据过大,则截断;
当数据被截断以后,需要保留一位给 \0;
可以这样写:sizeof (msgbuf.mtext) - 1;
或者:sizeof (msgbuf.mtext) - sizeof (char);
另外 msgsz 用于后面判断返回值情况;
*/
if (msgsz == -1)
// 阻塞模式下,消息队列被销毁时才会有 EIDRM,
// 非阻塞模式下且被销毁时,不会发送 EIDRM,而是是执行 else 后面语句;
if (errno == EIDRM) {
printf ("消息队列(0x%08x/%d)已销毁!\n", key, msqid);
break; // 退出程序;
}
// 在非阻塞模式下且无消息时,才会出现 ENOMESG 情况
else if (errno == ENOMSG) {
printf ("现在没有消息,干点儿别的...\n");
sleep (1);
}
// 非阻塞且已销毁的情况下执行;
else {
perror ("msgrcv");
return -1;
}
else
printf ("%04d< %s\n", msgsz, msgbuf.mtext);
}
printf ("大功告成!\n");
return 0;
}
注意:共享内存、消息队列属于内核级的,需要手动释放,否则永远在内核里,即便进程结束,依然存在;
# ipcrm -q <msqid> // 删除具体某一个消息队列
而程序结束会默认调用 exit();包括 new/delete 等内存级的资源也会随着程序的结束而结束;
练习:基于消息队列的本地银行。
代码:参见 /项目/bank1
6. 信号量
1)基本特点
计数器,用于限制多个进程对有限共享资源的访问。
多个进程获取有限共享资源的操作模式
A. 测试控制该资源的信号量;
B. 若信号量大于 0,则进程可以使用该资源,
为了表示此进程已获得该资源,需将信号量减 1;
C. 若信号量等于 0,则进程休眠等待该资源,
直到信号量大于 0,进程被唤醒,执行步骤 A;
D. 当某进程不再使用该资源时,信号量增 1,
正在休眠等待该资源的其它进程将被唤醒。
2)常用函数
#include <sys/sem.h>
创建/获取信号量
========
int semget (key_t key, int nsems, int semflg);
A. 该函数以 key 参数为键值创建一个信号量集合 (nsems参数表示集合中的信号量数),
或获取已有的信号量集合 (nsems 取 0)。
B. semflg取值:
0 - 获取,不存在即失败。
IPC_CREAT - 创建,不存在即创建,已存在即获取,除非 . . .
IPC_EXCL - 排斥,已存在即失败。
C. 成功返回信号量集合标识(semid),失败返回 -1。
操作信号量
=====
int semop (int semid, struct sembuf* sops, unsigned nsops);
struct sembuf {
unsigned short sem_num; // 信号量下标
short sem_op; // 操作数
short sem_flg; // 操作标记
};
A. 该函数对 semid 参数所标识的信号量集合中,
由 sops 参数所指向的包含 nsops 个元素的,
结构体数组中的每个元素,依次执行如下操作:
a) 若 sem_op 大于 0,则将其加到第 sem_num 个信号量的计数值上,以表示对资源的释放;
b) 若 sem_op 小于 0,则从第 sem_num 个信号量的计数值中减去其绝对值,以表示对资源的获取;
c) 若第 sem_num 个信号量的计数值不够减 (信号量不能为负),
则此函数会阻塞,直到该信号量够减为止,以表示对资源的等待;
d) 若 sem_flg 包含 IPC_NOWAIT 位,则当第 sem_num 个信号量的计数值不够减时,
此函数不会阻塞,而是返回 -1,errno 为 EAGAIN,以便在等待资源的同时还可做其它处理;
e) 若 sem_op 等于 0,则直到第 sem_num 个信号量的计数值为 0 时才返回,
除非 sem_flg 包含 IPC_NOWAIT 位。
B. 成功返回 0,失败返回 -1。
销毁/控制信号量
========
int semctl (int semid, int semnum, int cmd);
int semctl (int semid, int semnum, int cmd, 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
};
struct semid_ds {
struct ipc_perm sem_perm; // Ownership and permissions
time_t sem_otime; // Last semop time
time_t sem_ctime; // Last change time
unsigned short sem_nsems; // No. of semaphores in set
};
struct ipc_perm {
key_t __key; // 键值
uid_t uid; // 有效属主ID
gid_t gid; // 有效属组ID
uid_t cuid; // 有效创建者ID
gid_t cgid; // 有效创建组ID
unsigned short mode; // 权限字
unsigned short __seq; // 序列号
};
A. cmd取值:
IPC_STAT - 获取信号量集合的属性,通过 arg.buf 输出。
IPC_SET - 设置信号量集合的属性,通过 arg.buf 输入,仅以下四个属性可设置:
semid_ds::sem_perm.uid
semid_ds::sem_perm.gid
semid_ds::sem_perm.mode
IPC_RMID - 立即删除信号量集合。
此时所有阻塞在对该信号量集合的,
semop 函数调用,都会立即返回失败,errno 为 EIDRM。
GETALL - 获取信号量集合中每个信号量的计数值,通过 arg.array 输出。
SETALL - 设置信号量集合中每个信号量的计数值,通过 arg.array 输入。
GETVAL - 获取信号量集合中,第 semnum 个信号量的计数值,通过返回值输出。
SETVAL - 设置信号量集合中,第 semnum 个信号量的计数值,通过 arg.val 输入。
注意:只有针对信号量集合中具体某个信号量的操作,才会使用 semnum 参数。
针对整个信号量集合的操作,会忽略 semnum 参数。
B. 成功返回值因 cmd 而异,失败返回 -1。
3)编程模型
--------+-------------+----------+-------------+------
步骤| 进程 A | 函数 | 进程 B | 步骤
--------+-------------+----------+-------------+------
1 | 创建信号量 | semget | 获取信号量 | 1
2 | 初始信号量 | semctl | ---- |
3 | 加减信号量 | semop | 加减信号量 | 2
4 | 销毁信号量 | semctl | ---- |
--------+-------------+----------+-------------+------
范例:csem.c
#include <stdio.h>
#include <errno.h>
#include <sys/sem.h>
// 打印剩余的书本册数;
int pleft (int semid) {
int val = semctl (semid, 0, GETVAL);
// 取第 0 个信号量;
if (val == -1) {
perror ("semctl");
return -1;
}
printf ("还剩%d册。\n", val);
return 0;
}
int main (void) {
printf ("创建信号量...\n");
key_t key = ftok (".", 100);
if (key == -1) {
perror ("ftok");
return -1;
}
// 创建只含有一个信号量的集合
int semid = semget (key, 1, 0644 | IPC_CREAT | IPC_EXCL);
if (semid == -1) {
perror ("semget");
return -1;
}
printf ("初始化信号量...\n");
if (semctl (semid, 0, SETVAL, 5) == -1) {
// SETVAL 给特定信号量赋值;给下标为 0 的信号量初始化为 5;
perror ("semctl");
return -1;
}
int quit = 0;
// 假设上面那个信号量有 5 本《三国演义》;
while (! quit) {
printf ("--------\n");
printf ("三国演义\n");
printf ("--------\n");
printf ("[1] 借阅\n");
printf ("[2] 归还\n");
printf ("[0] 退出\n");
printf ("--------\n");
printf ("请选择:");
int sel = -1;
scanf ("%d", &sel);
switch (sel) {
case 0:
quit = 1;
break;
case 1: {
// printf ("请稍候...\n");
// 借出一本,所以对信号量作减;
struct sembuf sops = {0, -1, /*0*/IPC_NOWAIT};
// 0 指针对信号量集合里下标为 0 的信号量作非阻塞操作;
if (semop (semid, &sops, 1) == -1) {
if (errno == EAGAIN) {
printf ("暂时无书,下回再试。\n");
break;
}
else {
perror ("semop");
return -1;
}
}
printf ("恭喜恭喜,借阅成功。\n");
// 打印剩余图书量
pleft (semid);
break;
}
case 2: {
struct sembuf sops = {0, 1, 0};
// 还书为 +1 操作,且为阻塞模式;
if (semop (semid, &sops, 1) == -1) {
perror ("semop");
return -1;
}
printf ("好借好还,再借不难。\n");
pleft (semid);
break;
}
default:
printf ("无效选择!\n");
// 清空输入缓冲区;
scanf ("%*[^\n]");
scanf ("%*c");
break;
}
}
printf ("销毁信号量...\n");
if (semctl (semid, 0, IPC_RMID) == -1) {
// 销毁所有信号量,所以取值零即可;
perror ("semctl");
return -1;
}
printf ("大功告成!\n");
return 0;
}
范例:gsem.c
#include <stdio.h>
#include <errno.h>
#include <sys/sem.h>
int pleft (int semid) {
int val = semctl (semid, 0, GETVAL);
if (val == -1) {
perror ("semctl");
return -1;
}
printf ("还剩%d册。\n", val);
return 0;
}
int main (void) {
printf ("获取信号量...\n");
key_t key = ftok (".", 100);
if (key == -1) {
perror ("ftok");
return -1;
}
int semid = semget (key, 0, 0);
if (semid == -1) {
perror ("semget");
return -1;
}
int quit = 0;
while (! quit) {
printf ("--------\n");
printf ("三国演义\n");
printf ("--------\n");
printf ("[1] 借阅\n");
printf ("[2] 归还\n");
printf ("[0] 退出\n");
printf ("--------\n");
printf ("请选择:");
int sel = -1;
scanf ("%d", &sel);
switch (sel) {
case 0:
quit = 1;
break;
case 1: {
// printf ("请稍候...\n");
struct sembuf sops = {0, -1, /*0*/IPC_NOWAIT};
if (semop (semid, &sops, 1) == -1) {
if (errno == EAGAIN) {
printf ("暂时无书,下回再试。\n");
break;
}
else {
perror ("semop");
return -1;
}
}
printf ("恭喜恭喜,借阅成功。\n");
pleft (semid);
break;
}
case 2: {
struct sembuf sops = {0, 1, 0};
if (semop (semid, &sops, 1) == -1) {
perror ("semop");
return -1;
}
printf ("好借好还,再借不难。\n");
pleft (semid);
break;
}
default:
printf ("无效选择!\n");
scanf ("%*[^\n]");
scanf ("%*c");
break;
}
}
printf ("大功告成!\n");
return 0;
}
7. IPC 命令
1)显示
ipcs -m - 显示共享内存(m: memory)
ipcs -q - 显示消息队列(q: queue)
ipcs -s - 显示信号量(s: semphore)
ipcs -a - 显示所有IPC对象(a: all)
2)删除
ipcrm -m ID - 删除共享内存
ipcrm -q ID - 删除消息队列
ipcrm -s ID - 删除信号量
何为进程间通信:
进程间通信 (Interprocess Communication, IPC) 是指两个,
或多个进程之间进行数据交换的过程。
进程间通信分类:
1) 简单进程间通信:命令行参数(单向)、环境变量(单向)、信号(双向)、文件(双向)。
2) 传统进程间通信:管道 (fifo/pipe)。
fifo:有名管道;pipe:无名管道/匿名管道;
3) XSI 进程间通信:共享内存、消息队列、信号量。
4) 网络进程间通信:套接字。
2. 传统进程间通信——管道
1)管道是 Unix 系统最古老的进程间通信方式。
2)历史上的管道通常是指半双工管道,只允许数据单向流动。
现代系统大都提供全双工管道,数据可以沿着管道双向流动。
3)有名管道 (fifo):基于有名文件 (管道文件) 的管道通信。
a. 命令形式
# mkfifo fifo // 创建管道文件
# echo hello > fifo // hello 输入重定向到 fifo 里,如果不读取,会处于阻塞状态;
此时程序处于等待状态,可以在另一个窗口里输入以下命令:
# cat fifo
输出:hello
同时,上面窗口自动退出;
b. 编程模型
--------+-------------+-----------------+-------------+------
步骤| 进程 A | 函数 | 进程 B | 步骤
--------+-------------+-----------------+-------------+------
1 | 创建管道 | mkfifo | --- |
2 | 打开管道 | open | 打开管道 | 1
3 | 读写管道 | read/write | 读写管道 | 2
4 | 关闭管道 | close | 关闭管道 | 3
5 | 删除管道 | unlink | --- |
--------+-------------+-----------------+-------------+------
范例:wfifo.c(写管道)
#include <stdio.h>
#include <fcntl.h>
#include <string.h>
#include <sys/stat.h>
// 定义一个管道文件宏,用于创建管道;
#define FIFO_FILE "/tmp/fifo"
int main (void) {
printf ("创建管道...\n");
// 创建管道,不返回文件描述符
if (mkfifo (FIFO_FILE, 0666) == -1) {
perror ("mkfifo");
return -1;
}
printf ("打开管道...\n");
// open 打开文件,返回文件描述符
int fd = open (FIFO_FILE, O_WRONLY);
if (fd == -1) {
perror ("open");
return -1;
}
// 如果此时下面 rfifo.c 文件没打开,程序会阻塞在此;
printf ("发送数据...\n");
for (;;) {
printf ("> ");
// 等待键盘输入字符
char buf[1024];
gets (buf); // 读取字符串;
// 如果输入感叹号退出
if (! strcmp (buf, "!"))
break;
// 逐个字符写入管道
if (write (fd, buf, (strlen (buf) + 1) *
sizeof (buf[0])) == -1) {
perror ("write");
return -1;
}
}
printf ("关闭管道...\n");
if (close (fd) == -1) {
perror ("close");
return -1;
}
// 关闭后还需要删除管道
printf ("删除管道...\n");
if (unlink (FIFO_FILE) == -1) {
perror ("unlink");
return -1;
}
printf ("大功告成!\n");
return 0;
}
范例:rfifo.c(读管道)
#include <stdio.h>
#include <fcntl.h>
#define FIFO_FILE "/tmp/fifo"
int main (void) {
printf ("打开管道...\n");
// wfifo.c 已经创建了管道,这里只需要打开即可;
int fd = open (FIFO_FILE, O_RDONLY);
if (fd == -1) {
perror ("open");
return -1;
}
printf ("接收数据...\n");
for (;;) {
char buf[1024];
ssize_t rb = read (fd, buf, sizeof (buf));
if (rb == -1) {
perror ("read");
return -1;
}
if (! rb) // read 返回 0 表示那边管道被关闭
break;
printf ("< %s\n", buf);
}
printf ("关闭管道...\n");
// 关闭这边管道
if (close (fd) == -1) {
perror ("close");
return -1;
}
// 不需要删除,誰建谁删;
printf ("大功告成!\n");
return 0;
}
分析:fifo 必须两头都打开的时候,才会进行读写;
进一个数据,就出一个数据,不具备保存机制;
如果打开两个 rfifo.c 的时候,那么就无法确定是哪个 rififo.c 在读取;
两个随机读取 wfifo.c 写入的数据;
4)无名管道 (pipe):适用于父子进程之间的通信。
#include <unistd.h>
int pipe (int pipefd[2]);
成功返回 0,失败返回 -1。
通过输出参数 pipefd 返回两个文件描述符,
其中 pipefd[0] 用于读,pipefd[1] 用于写。
一般用法:
A. 调用该函数在内核中创建管道文件,并通过其输出参数,
获得分别用于读和写的两个文件描述符;
B. 调用 fork 函数,创建子进程;
C. 写数据的进程关闭读端 (pipefd[0]),
读数据的进程关闭写端 (pipefd[1]);
D. 传输数据;
E. 父子进程分别关闭自己的文件描述符。
范例:pipe.c
#include <stdio.h>
#include <string.h>
#include <unistd.h>
int main (void) {
printf ("父进程:创建管道...\n");
// 首先创建包含两个文件描述符的数组;
int pipefd[2];
if (pipe (pipefd) == -1) {
perror ("pipe");
return -1;
}
printf ("父进程:创建进程...\n");
pid_t pid = fork ();
if (pid == -1) {
perror ("fork");
return -1;
}
// 父进程负责写,子进程负责读
if (pid == 0) {
printf ("子进程:关闭写端...\n");
close (pipefd[1]);
printf ("子进程:接收数据...\n");
// 通过 for 循环不停的读取数据
for (;;) {
char buf[1024];
ssize_t rb = read (pipefd[0], buf, sizeof (buf)); // 子进程焦点
if (rb == -1) {
perror ("read");
return -1;
}
// rb 等于零,说明写端被关闭
if (! rb)
break; // 关闭子进程
// 否则打印
puts (buf);
}
printf ("子进程:关闭读端...\n");
close (pipefd[0]);
printf ("子进程:大功告成!\n");
return 0;
}
// 父进程负责写,所以先关闭读端;
printf ("父进程:关闭读端...\n");
close (pipefd[0]);
printf ("父进程:发送数据...\n");
// 每读一个字符串就发送一个,直到读到感叹号;
for (;;) {
char buf[1024];
gets (buf); // 父进程焦点
if (! strcmp (buf, "!"))
break;
// 把管道当作文件处理,所以可以使用 write;
if (write (pipefd[1], buf, (strlen (buf) + 1) *
sizeof (buf[0])) == -1) {
perror ("write");
return -1;
}
}
printf ("父进程:关闭写端...\n");
close (pipefd[1]);
if (wait (0) == -1) {
perror ("wait");
return -1;
}
printf ("父进程:大功告成!\n");
return 0;
}
注意:无名管道不创建文件;
测试这个文件的时候,不需要打开俩个窗口;
当出现感叹号的时候,父进程先关闭,然后子进程 rb 收到 0,也跟着关闭;
3. XSI 进程间通信
1)IPC 标识
内核为每个进程间通信维护一个结构体形式的 IPC 对象。
该对象可通过一个非负整数的 IPC 标识来引用。
与文件描述符不同,IPC 标识在使用时会持续加 1,
当达到最大值时,向 0 回转。
2)IPC 键值
IPC 标识是 IPC 对象的内部名称。
若多个进程需要在同一个 IPC 对象上会合,
则必须通过键值作为其外部名称来引用该对象。
a. 无论何时,只要创建 IPC 对象,就必须指定一个键值。
b. 键值的数据类型在 sys/types.h 头文件中被定义为 key_t,其原始类型就是长整型。
3)客户机进程与服务器进程在 IPC 对象上的三种会合方式:
a. 服务器进程以 IPC_PRIVATE 为键值创建一个新的 IPC 对象,
并将该 IPC 对象的标识存放在某处 (如文件中),以方便客户机进程读取。
b. 在一个公共头文件中,定义一个客户机进程和服务器进程都认可的键值,
服务器进程用此键值创建 IPC 对象,客户机进程用此键值获取该 IPC 对象。
c. 客户机进程和服务器进程,事先约定好一个路径名和一个项目 ID(0-255),
二者通过 ftok 函数,将该路径名和项目 ID 转换为一致的键值。
#include <sys/types.h>
#include <sys/ipc.h>
key_t ftok (const char* pathname, int proj_id);
pathname - 一个真实存在的文件或目录的路径名。
proj_id - 项目 ID,仅低 8 位有效,其值域为 [0, 255]。
成功返回键值,失败返回 -1。
注意:起作用的是 pathname 参数所表示的路径,而非 pathname 字符串本身。
因此假设当前目录是 /home/soft01/uc/day07,则
ftok (".", 100);
和
ftok ("/home/soft01/uc/day07", 100);
的返回值完全相同。
4)IPC 对象的创建
a. 若以 IPC_PRIVATE 为键值创建 IPC 对象,则永远创建成功。
b. 若所指定的键值在系统范围内未与任何 IPC 对象相结合,
且创建标志包含 IPC_CREAT 位,则创建成功。
c. 若所指定的键值在系统范围内已与某个 IPC 对象相结合,
且创建标志包含 IPC_CREAT 和 IPC_EXCL 位,则创建失败。
5)IPC 对象的销毁/控制
IPC_STAT - 获取 IPC 对象属性
IPC_SET - 设置 IPC 对象属性
IPC_RMID - 删除 IPC 对象
4. 共享内存
1)基本特点
a. 两个或者更多进程,共享同一块由系统“内核“负责维护的内存区域,位于内核里;
其地址空间通常被映射到堆和栈之间的共享内存区域。
b. 优点:无需复制信息,共享内存直接映射到内核,最快的一种 IPC 机制。
c. 缺点:需要考虑同步访问的问题;也就是说,共享内存无法保证一定是写完以后再读;
而 fifo 等不需要考虑,自身控制;所以一般小数据同步用共享内存,而大数据量用管道或者消息队列等;
d. 内核为每个共享内存,维护一个 shmid_ds 结构体形式的共享内存对象。
2)常用函数
#include <sys/shm.h>
创建/获取共享内存:
=========
int shmget (key_t key, size_t size, int shmflg);
A. 该函数以 key 参数为键值创建共享内存,或获取已有的共享内存。
B. size 参数为共享内存的字节数,建议取内存页字节数 (4096) 的整数倍。
若希望创建共享内存,则必需指定 size 参数。
若只为获取已有的共享内存,则 size 参数可取 0。
C. shmflg 取值:
0 - 获取,不存在即失败。
IPC_CREAT - 创建,不存在即创建,已存在即获取,除非 . . .
IPC_EXCL - 排斥,已存在即失败。
D. 成功返回共享内存标识(int),失败返回 -1。
tips:创建共享内存是在内核里面创建,
下面加载共享内存是将内核里的内存和堆栈之间的内存建立映射;
加载(映射)共享内存:
==========
void* shmat (int shmid, const void* shmaddr, int shmflg);
A. 将 shmid 参数所标识的共享内存,映射到调用进程的地址空间。
B. 可通过 shmaddr 参数人为指定映射地址,也可将该参数置 NULL,由系统自动选择。
C. shmflg 取值:
0 - 以读写方式使用共享内存。
SHM_RDONLY - 以只读方式使用共享内存。
SHM_RND - 只在 shmaddr 参数非 NULL 时起作用。
表示对该参数向下取内存页的整数倍,作为映射地址。
D. 成功返回映射地址,失败返回 -1。
E. 内核将该共享内存的加载计数加 1;每调用一次 shmid,就 +1;
卸载(解除映射)共享内存:
============
int shmdt (const void* shmaddr);
A. 从调用进程的地址空间中,取消由 shmaddr 参数所指向的,共享内存映射区域。
B. 成功返回 0,失败返回 -1。
C. 内核将该共享内存的加载计数减 1。
销毁/控制共享内存:
=========
int shmctl (int shmid, int cmd, struct shmid_ds* buf);
struct shmid_ds {
struct ipc_perm shm_perm; // 所有者及其权限
size_t shm_segsz; // 大小(以字节为单位)
time_t shm_atime; // 最后加载时间
time_t shm_dtime; // 最后卸载时间
time_t shm_ctime; // 最后改变时间
pid_t shm_cpid; // 创建进程 PID
pid_t shm_lpid; // 最后加载/卸载进程 PID
shmatt_t shm_nattch; // 当前加载计数
. . .
};
struct ipc_perm {
key_t __key; // 键值
uid_t uid; // 有效属主 ID
gid_t gid; // 有效属组 ID
uid_t cuid; // 有效创建者 ID
gid_t cgid; // 有效创建组 ID
unsigned short mode; // 权限字
unsigned short __seq; // 序列号
};
A. cmd 取值:
IPC_STAT - 获取共享内存的属性,通过 buf 参数输出。
IPC_SET - 设置共享内存的属性,通过 buf 参数输入,仅以下三个属性可设置:
shmid_ds::shm_perm.uid
shmid_ds::shm_perm.gid
shmid_ds::shm_perm.mode
IPC_RMID - 标记删除共享内存。
并非真正删除共享内存,只是做一个删除标记,
禁止其被继续加载,但已有加载依然保留。
只有当该共享内存的加载计数为 0 时,才真正被删除。
B. 成功返回 0,失败返回 -1。
3)编程模型
--------+-----------------+-------------+-----------------+------
步骤| 进程 A | 函数 | 进程 B | 步骤
--------+-----------------+-------------+-----------------+------
1 | 创建共享内存 | shmget | 获取共享内存 | 1
2 | 加载共享内存 | shmat | 加载共享内存 | 2
3 | 使用共享内存 | . . . | 使用共享内存 | 3
4 | 卸载共享内存 | shmdt | 卸载共享内存 | 4
5 | 销毁共享内存 | shctl | --- |
--------+-----------------+-------------+-----------------+------
范例:wshm.c
#include <stdio.h>
#include <sys/shm.h>
int main (void) {
printf ("创建共享内存...\n");
// 创建共享内存之前先得有 key,通过路径+项目 ID 的方式创建;
key_t key = ftok (".", 100);
if (key == -1) {
perror ("ftok");
return -1;
}
// 创建共享内存
int shmid = shmget (key, 4096, 0644 | IPC_CREAT | IPC_EXCL);
// 4096 页大小,0644 权限,如果没有就创建,如果有就报错;
if (shmid == -1) {
perror ("shmget");
return -1;
}
printf ("加载共享内存...\n");
// 加载共享内存,返回共享内存的地址;返回 -1 代表失败;
void* shmaddr = shmat (shmid, NULL, 0);
if (shmaddr == (void*)-1) {
// 左边指针,右边 int,所以需要类型强转;
perror ("shmat");
return -1;
}
printf ("写入共享内存...\n");
sprintf (shmaddr, "我是%u进程写入的数据。", getpid ());
// 等一下,让下面进程 rshm.c 使用共享内存;
printf ("按<回车>卸载共享内存(0x%08x/%d)...", key, shmid);
getchar ();
// 卸载共享内存;
if (shmdt (shmaddr) == -1) {
perror ("shmdt");
return -1;
}
// 同样等一下再销毁共享内存;
printf ("按<回车>销毁共享内存(0x%08x/%d)...", key, shmid);
getchar ();
// 销毁共享内存;
if (shmctl (shmid, IPC_RMID, NULL) == -1) {
perror ("shmctl");
return -1;
}
printf ("大功告成!\n");
return 0;
}
查看共享内存:
# ipcs -m
输出如下:
key shmid owner perms bytes nattch status
0x00000000 229377 tarena 600 393216 2 dest
0x00000000 262146 tarena 600 393216 2 dest
. . .
范例:rshm.c
#include <stdio.h>
#include <sys/shm.h>
// 打印共享内存信息
int shmstat (int shmid) {
struct shmid_ds shm;
if (shmctl (shmid, IPC_STAT, &shm) == -1) {
perror ("shmctl");
return -1;
}
printf ("------------------------------------------------\n");
printf (" 共享内存信息\n");
printf ("----+--------------+--------------------------\n");
printf (" 所 | 键值 | 0x%08x\n", shm.shm_perm.__key);
printf (" 有 | 有效属主ID | %u\n", shm.shm_perm.uid);
printf (" 者 | 有效属组ID | %u\n", shm.shm_perm.gid);
printf (" 及 | 有效创建者ID | %u\n", shm.shm_perm.cuid);
printf (" 其 | 有效创建组ID | %u\n", shm.shm_perm.cgid);
printf (" 权 | 权限字 | %#o\n", shm.shm_perm.mode);
printf (" 限 | 序列号 | %u\n", shm.shm_perm.__seq);
printf ("---+---------------+--------------------------\n");
printf (" 大小(字节) | %u\n", shm.shm_segsz);
printf (" 最后加载时间 | %s", ctime (&shm.shm_atime));
printf (" 最后卸载时间 | %s", ctime (&shm.shm_dtime));
printf (" 最后改变时间 | %s", ctime (&shm.shm_ctime));
printf (" 创建进程ID | %u\n", shm.shm_cpid);
printf (" 最后加载/卸载进程ID | %u\n", shm.shm_lpid);
printf (" 当前加载计数 | %u\n", shm.shm_nattch);
printf ("-------------------+--------------------------\n");
return 0;
}
// 修改共享内存属性
int shmset (int shmid) {
struct shmid_ds shm;
// 先取出当前的属性
if (shmctl (shmid, IPC_STAT, &shm) == -1) {
perror ("shmctl");
return -1;
}
// 修改属性,只能修改 gid,uid 和 mode 三个属性;
shm.shm_perm.mode = 0600;
shm.shm_segsz = 8192; // 修改失败,创建后无法改变大小;
if (shmctl (shmid, IPC_SET, &shm) == -1) {
perror ("shmctl");
return -1;
}
return 0;
}
int main (void) {
printf ("获取共享内存...\n");
// 获取共享内存,不用创建,用同样的路径和项目 ID 就可以获取;
key_t key = ftok (".", 100);
if (key == -1) {
perror ("ftok");
return -1;
}
int shmid = shmget (key, 0, 0);
if (shmid == -1) {
perror ("shmget");
return -1;
}
printf ("加载共享内存...\n");
void* shmaddr = shmat (shmid, NULL, 0);
if (shmaddr == (void*)-1) {
perror ("shmat");
return -1;
}
shmstat (shmid);
printf ("读取共享内存...\n");
printf ("共享内存(0x%08x/%d):%s\n", key, shmid, shmaddr);
printf ("卸载共享内存...\n");
if (shmdt (shmaddr) == -1) {
perror ("shmdt");
return -1;
}
shmstat (shmid);
printf ("设置共享内存...\n");
shmset (shmid);
shmstat (shmid);
printf ("大功告成!\n");
return 0;
}
5. 消息队列
1)基本特点
a. 消息队列是一个由系统内核负责存储和管理(位于内核里),
并通过消息队列标识引用的数据链表;
而有名管道一次只能放一个数据;
b. 可以通过 msgget 函数创建一个新的消息队列,或获取一个已有的消息队列。
通过 msgsnd 函数向消息队列的后端追加消息,
通过 msgrcv 函数从消息队列的前端提取消息。
c. 消息队列中的每个消息单元除包含消息数据外,还包含消息类型和数据长度。
d. 内核为每个消息队列,维护一个 msqid_ds 结构体形式的消息队列对象。
2)常用函数
#include <sys/msg.h>
创建/获取消息队列
=========
int msgget (key_t key, int msgflg);
A. 该函数以 key 参数为键值创建消息队列,或获取已有的消息队列。
B. msgflg 取值:
0 - 获取,不存在即失败。
IPC_CREAT - 创建,不存在即创建,已存在即获取,除非 . . .
IPC_EXCL - 排斥,已存在即失败。
C. 成功返回消息队列标识 msqid,失败返回 -1。
向消息队列发送消息
=========
int msgsnd (int msqid, const void* msgp, size_t msgsz, int msgflg);
A. msgp 参数指向一个包含消息类型和消息数据的内存块(一般是一个结构体,把地址返回给 void*)。
该内存块的前 4 个字节必须是一个大于 0 的整数,代表消息类型,其后紧跟消息数据。
消息数据的字节长度用 msgsz 参数表示。
+----------------+-----------------+
msgp -> | 消息类型 (>0) | 消息数据 |
+----------------+-----------------+
|<----- 4 ----->|<---- msgsz ---->|
注意:msgsz 参数并不包含消息类型的字节数 (4)。
B. 若内核中的消息队列缓冲区有足够的空闲空间,则此函数会将消息拷入该缓冲区并立即返回 0,表示发送成功;
否则此函数会阻塞,直到内核中的消息队列缓冲区有足够的空闲空间为止 (比如有消息被接收)。
C. 若 msgflg 参数包含 IPC_NOWAIT 位,则当内核中的消息队列缓冲区没有足够的空闲空间时,
此函数不会阻塞,而是返回 -1,errno 为 EAGAIN。
D. 成功返回 0,失败返回 -1。
从消息队列接收消息
=========
ssize_t msgrcv (int msqid, void* msgp, size_t msgsz, long msgtyp, int msgflg);
A. msgp参数指向一个包含消息类型 (4 字节),和消息数据的内存块,
其中消息数据缓冲区的字节大小用 msgsz 参数表示。
B. 若所接收到的消息数据字节数大于 msgsz 参数,
即消息太长,且 msgflg 参数包含 MSG_NOERROR 位,
则该消息被截取 msgsz 字节返回,剩余部分被丢弃。
C. 若 msgflg 参数不包含 MSG_NOERROR 位,消息又太长,
则不对该消息做任何处理,直接返回 -1,errno 为 E2BIG。
D. msgtyp 参数表示期望接收哪类消息:
=0 - 返回消息队列中的第一条消息(无论是什么消息类型,只拿第一个消息)。
>0 - 若 msgflg 参数不包含 MSG_EXCEPT 位,则返回消息队列中第一个类型为 msgtyp 的消息;
若 msgflg 参数包含 MSG_EXCEPT 位,则返回消息队列中第一个类型不为 msgtyp 的消息。
<0 - 返回消息队列中类型小于等于 msgtyp 的绝对值的消息;若有多个,则取类型最小者。
+-----+-----+-----+-----+-----+-----+-----+-----+-----+
| 3 | 4 | 2 | 1 | 3 | 2 | 4 | 3 | 3 |
-> rear +- -+- -+- -+- -+- -+- -+- -+- -+- -+ front ->
| . . . | . . . | . . . | . . . | . . . | . . . | . . . | . . . | . . . |
+-----+-----+-----+-----+-----+-----+-----+-----+-----+
^ ^ ^ ^
4 3 2 1
msgrcv (..., ..., ..., 3, ...);
E. 若消息队列中有可接收消息,则此函数会将该消息移出消息队列并立即返回 0,表示接收成功,
否则此函数会阻塞,直到消息队列中有可接收消息为止。
F. 若 msgflg 参数包含 IPC_NOWAIT 位,
则当消息队列中没有可接收消息时,此函数不会阻塞,
而是返回 -1,errno 为 ENOMSG。
G. 成功返回所接收到的消息数据的字节数,失败返回 -1。
销毁/控制消息队列
=========
int msgctl (int msqid, int cmd, struct msqid_ds* buf);
struct msqid_ds {
struct ipc_perm msg_perm; // 权限信息
time_t msg_stime; // 随后发送时间
time_t msg_rtime; // 最后接收时间
time_t msg_ctime; // 最后改变时间
unsigned long __msg_cbytes; // 消息队列中的字节数
msgqnum_t msg_qnum; // 消息队列中的消息数
msglen_t msg_qbytes; // 消息队列能容纳的最大字节数
pid_t msg_lspid; // 最后发送进程PID
pid_t msg_lrpid; // 最后接收进程PID
};
struct ipc_perm {
key_t __key; // 键值
uid_t uid; // 有效属主ID
gid_t gid; // 有效属组ID
uid_t cuid; // 有效创建者ID
gid_t cgid; // 有效创建组ID
unsigned short mode; // 权限字
unsigned short __seq; // 序列号
};
A. cmd 取值:
IPC_STAT - 获取消息队列的属性,通过 buf 参数输出。
IPC_SET - 设置消息队列的属性,通过 buf 参数输入,仅以下四个属性可设置:
msqid_ds::msg_perm.uid
msqid_ds::msg_perm.gid
msqid_ds::msg_perm.mode
msqid_ds::msg_qbytes
IPC_RMID - 立即删除消息队列。
此时所有阻塞在对该消息队列的,msgsnd 和 msgrcv 函数调用,
msgsnd 和 msgrcv 都会立即返回失败,errno 为 EIDRM。
B. 成功返回 0,失败返回 -1。
3)编程模型
--------+-----------------+-----------------+-----------------+------
步骤| 进程 A | 函数 | 进程 B | 步骤
--------+-----------------+-----------------+-----------------+------
1 | 创建消息队列 | msgget | 获取消息队列 | 1
2 | 发送接受消息 | msgsnd/msgrcv | 发送接受消息 | 2
3 | 销毁消息队列 | msgctl | --- |
--------+-----------------+-----------------+-----------------+------
范例:wmsq.c
#include <stdio.h>
#include <string.h>
#include <sys/msg.h>
int main (void) {
printf ("创建消息队列...\n");
key_t key = ftok (".", 100);
if (key == -1) {
perror ("ftok");
return -1;
}
int msqid = msgget (key, 0644 | IPC_CREAT | IPC_EXCL);
if (msqid == -1) {
perror ("msqget");
return -1;
}
printf ("向消息队列(0x%08x/%d)发送数据...\n", key, msqid);
for (;;) {
printf ("> ");
// 发送消息是一个包一个包的发送,所以先定义消息包;
struct {
long mtype; // 消息类型
char mtext[1024]; // 消息数据
} msgbuf = {1234, ""};
gets (msgbuf.mtext); // 获取消息数据
if (! strcmp (msgbuf.mtext, "!"))
break;
// 发送消息
if (msgsnd (msqid, &msgbuf, (strlen (msgbuf.mtext) + 1) *
sizeof (msgbuf.mtext[0]), 0) == -1) {
// strlen 实际发送长度 + 1 位 \0;如果直接写为 sizeof (msgbuf.mtext); 则直接发送 1024 大小;
// 最后一位是 msgflg,为零表示没空间会阻塞;如果设为 IPC_NOWAIT,则不会阻塞;
perror ("msgsnd");
return -1;
}
}
printf ("销毁消息队列(0x%08x/%d)...\n", key, msqid);
if (msgctl (msqid, IPC_RMID, NULL) == -1) {
perror ("msgctl");
return -1;
}
printf ("大功告成!\n");
return 0;
}
查看消息队列:
# ipcs -q
范例:rmsq.c
#include <stdio.h>
#include <errno.h>
#include <sys/msg.h>
int main (void) {
printf ("获取消息队列...\n");
key_t key = ftok (".", 100);
if (key == -1) {
perror ("ftok");
return -1;
}
int msqid = msgget (key, 0);
// 只获取,不创建,所以第二参数为零;
if (msqid == -1) {
perror ("msgget");
return -1;
}
printf ("从消息队列(0x%08x/%d)接收消息...\n", key, msqid);
for (;;) {
// 接收的也是消息结构,所以先定义结构体;
struct {
long mtype;
char mtext[1024];
} msgbuf = {};
ssize_t msgsz = msgrcv (msqid, &msgbuf,
sizeof (msgbuf.mtext) - sizeof (msgbuf.mtext[0]), 1234,
MSG_NOERROR/* | IPC_NOWAIT*/);
/*
此时为阻塞模式,如果或上 IPC_NOWAIT 则是非阻塞模式;
MSG_NOERROR 如果数据过大,则截断;
当数据被截断以后,需要保留一位给 \0;
可以这样写:sizeof (msgbuf.mtext) - 1;
或者:sizeof (msgbuf.mtext) - sizeof (char);
另外 msgsz 用于后面判断返回值情况;
*/
if (msgsz == -1)
// 阻塞模式下,消息队列被销毁时才会有 EIDRM,
// 非阻塞模式下且被销毁时,不会发送 EIDRM,而是是执行 else 后面语句;
if (errno == EIDRM) {
printf ("消息队列(0x%08x/%d)已销毁!\n", key, msqid);
break; // 退出程序;
}
// 在非阻塞模式下且无消息时,才会出现 ENOMESG 情况
else if (errno == ENOMSG) {
printf ("现在没有消息,干点儿别的...\n");
sleep (1);
}
// 非阻塞且已销毁的情况下执行;
else {
perror ("msgrcv");
return -1;
}
else
printf ("%04d< %s\n", msgsz, msgbuf.mtext);
}
printf ("大功告成!\n");
return 0;
}
注意:共享内存、消息队列属于内核级的,需要手动释放,否则永远在内核里,即便进程结束,依然存在;
# ipcrm -q <msqid> // 删除具体某一个消息队列
而程序结束会默认调用 exit();包括 new/delete 等内存级的资源也会随着程序的结束而结束;
练习:基于消息队列的本地银行。
代码:参见 /项目/bank1
6. 信号量
1)基本特点
计数器,用于限制多个进程对有限共享资源的访问。
多个进程获取有限共享资源的操作模式
A. 测试控制该资源的信号量;
B. 若信号量大于 0,则进程可以使用该资源,
为了表示此进程已获得该资源,需将信号量减 1;
C. 若信号量等于 0,则进程休眠等待该资源,
直到信号量大于 0,进程被唤醒,执行步骤 A;
D. 当某进程不再使用该资源时,信号量增 1,
正在休眠等待该资源的其它进程将被唤醒。
2)常用函数
#include <sys/sem.h>
创建/获取信号量
========
int semget (key_t key, int nsems, int semflg);
A. 该函数以 key 参数为键值创建一个信号量集合 (nsems参数表示集合中的信号量数),
或获取已有的信号量集合 (nsems 取 0)。
B. semflg取值:
0 - 获取,不存在即失败。
IPC_CREAT - 创建,不存在即创建,已存在即获取,除非 . . .
IPC_EXCL - 排斥,已存在即失败。
C. 成功返回信号量集合标识(semid),失败返回 -1。
操作信号量
=====
int semop (int semid, struct sembuf* sops, unsigned nsops);
struct sembuf {
unsigned short sem_num; // 信号量下标
short sem_op; // 操作数
short sem_flg; // 操作标记
};
A. 该函数对 semid 参数所标识的信号量集合中,
由 sops 参数所指向的包含 nsops 个元素的,
结构体数组中的每个元素,依次执行如下操作:
a) 若 sem_op 大于 0,则将其加到第 sem_num 个信号量的计数值上,以表示对资源的释放;
b) 若 sem_op 小于 0,则从第 sem_num 个信号量的计数值中减去其绝对值,以表示对资源的获取;
c) 若第 sem_num 个信号量的计数值不够减 (信号量不能为负),
则此函数会阻塞,直到该信号量够减为止,以表示对资源的等待;
d) 若 sem_flg 包含 IPC_NOWAIT 位,则当第 sem_num 个信号量的计数值不够减时,
此函数不会阻塞,而是返回 -1,errno 为 EAGAIN,以便在等待资源的同时还可做其它处理;
e) 若 sem_op 等于 0,则直到第 sem_num 个信号量的计数值为 0 时才返回,
除非 sem_flg 包含 IPC_NOWAIT 位。
B. 成功返回 0,失败返回 -1。
销毁/控制信号量
========
int semctl (int semid, int semnum, int cmd);
int semctl (int semid, int semnum, int cmd, 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
};
struct semid_ds {
struct ipc_perm sem_perm; // Ownership and permissions
time_t sem_otime; // Last semop time
time_t sem_ctime; // Last change time
unsigned short sem_nsems; // No. of semaphores in set
};
struct ipc_perm {
key_t __key; // 键值
uid_t uid; // 有效属主ID
gid_t gid; // 有效属组ID
uid_t cuid; // 有效创建者ID
gid_t cgid; // 有效创建组ID
unsigned short mode; // 权限字
unsigned short __seq; // 序列号
};
A. cmd取值:
IPC_STAT - 获取信号量集合的属性,通过 arg.buf 输出。
IPC_SET - 设置信号量集合的属性,通过 arg.buf 输入,仅以下四个属性可设置:
semid_ds::sem_perm.uid
semid_ds::sem_perm.gid
semid_ds::sem_perm.mode
IPC_RMID - 立即删除信号量集合。
此时所有阻塞在对该信号量集合的,
semop 函数调用,都会立即返回失败,errno 为 EIDRM。
GETALL - 获取信号量集合中每个信号量的计数值,通过 arg.array 输出。
SETALL - 设置信号量集合中每个信号量的计数值,通过 arg.array 输入。
GETVAL - 获取信号量集合中,第 semnum 个信号量的计数值,通过返回值输出。
SETVAL - 设置信号量集合中,第 semnum 个信号量的计数值,通过 arg.val 输入。
注意:只有针对信号量集合中具体某个信号量的操作,才会使用 semnum 参数。
针对整个信号量集合的操作,会忽略 semnum 参数。
B. 成功返回值因 cmd 而异,失败返回 -1。
3)编程模型
--------+-------------+----------+-------------+------
步骤| 进程 A | 函数 | 进程 B | 步骤
--------+-------------+----------+-------------+------
1 | 创建信号量 | semget | 获取信号量 | 1
2 | 初始信号量 | semctl | ---- |
3 | 加减信号量 | semop | 加减信号量 | 2
4 | 销毁信号量 | semctl | ---- |
--------+-------------+----------+-------------+------
范例:csem.c
#include <stdio.h>
#include <errno.h>
#include <sys/sem.h>
// 打印剩余的书本册数;
int pleft (int semid) {
int val = semctl (semid, 0, GETVAL);
// 取第 0 个信号量;
if (val == -1) {
perror ("semctl");
return -1;
}
printf ("还剩%d册。\n", val);
return 0;
}
int main (void) {
printf ("创建信号量...\n");
key_t key = ftok (".", 100);
if (key == -1) {
perror ("ftok");
return -1;
}
// 创建只含有一个信号量的集合
int semid = semget (key, 1, 0644 | IPC_CREAT | IPC_EXCL);
if (semid == -1) {
perror ("semget");
return -1;
}
printf ("初始化信号量...\n");
if (semctl (semid, 0, SETVAL, 5) == -1) {
// SETVAL 给特定信号量赋值;给下标为 0 的信号量初始化为 5;
perror ("semctl");
return -1;
}
int quit = 0;
// 假设上面那个信号量有 5 本《三国演义》;
while (! quit) {
printf ("--------\n");
printf ("三国演义\n");
printf ("--------\n");
printf ("[1] 借阅\n");
printf ("[2] 归还\n");
printf ("[0] 退出\n");
printf ("--------\n");
printf ("请选择:");
int sel = -1;
scanf ("%d", &sel);
switch (sel) {
case 0:
quit = 1;
break;
case 1: {
// printf ("请稍候...\n");
// 借出一本,所以对信号量作减;
struct sembuf sops = {0, -1, /*0*/IPC_NOWAIT};
// 0 指针对信号量集合里下标为 0 的信号量作非阻塞操作;
if (semop (semid, &sops, 1) == -1) {
if (errno == EAGAIN) {
printf ("暂时无书,下回再试。\n");
break;
}
else {
perror ("semop");
return -1;
}
}
printf ("恭喜恭喜,借阅成功。\n");
// 打印剩余图书量
pleft (semid);
break;
}
case 2: {
struct sembuf sops = {0, 1, 0};
// 还书为 +1 操作,且为阻塞模式;
if (semop (semid, &sops, 1) == -1) {
perror ("semop");
return -1;
}
printf ("好借好还,再借不难。\n");
pleft (semid);
break;
}
default:
printf ("无效选择!\n");
// 清空输入缓冲区;
scanf ("%*[^\n]");
scanf ("%*c");
break;
}
}
printf ("销毁信号量...\n");
if (semctl (semid, 0, IPC_RMID) == -1) {
// 销毁所有信号量,所以取值零即可;
perror ("semctl");
return -1;
}
printf ("大功告成!\n");
return 0;
}
范例:gsem.c
#include <stdio.h>
#include <errno.h>
#include <sys/sem.h>
int pleft (int semid) {
int val = semctl (semid, 0, GETVAL);
if (val == -1) {
perror ("semctl");
return -1;
}
printf ("还剩%d册。\n", val);
return 0;
}
int main (void) {
printf ("获取信号量...\n");
key_t key = ftok (".", 100);
if (key == -1) {
perror ("ftok");
return -1;
}
int semid = semget (key, 0, 0);
if (semid == -1) {
perror ("semget");
return -1;
}
int quit = 0;
while (! quit) {
printf ("--------\n");
printf ("三国演义\n");
printf ("--------\n");
printf ("[1] 借阅\n");
printf ("[2] 归还\n");
printf ("[0] 退出\n");
printf ("--------\n");
printf ("请选择:");
int sel = -1;
scanf ("%d", &sel);
switch (sel) {
case 0:
quit = 1;
break;
case 1: {
// printf ("请稍候...\n");
struct sembuf sops = {0, -1, /*0*/IPC_NOWAIT};
if (semop (semid, &sops, 1) == -1) {
if (errno == EAGAIN) {
printf ("暂时无书,下回再试。\n");
break;
}
else {
perror ("semop");
return -1;
}
}
printf ("恭喜恭喜,借阅成功。\n");
pleft (semid);
break;
}
case 2: {
struct sembuf sops = {0, 1, 0};
if (semop (semid, &sops, 1) == -1) {
perror ("semop");
return -1;
}
printf ("好借好还,再借不难。\n");
pleft (semid);
break;
}
default:
printf ("无效选择!\n");
scanf ("%*[^\n]");
scanf ("%*c");
break;
}
}
printf ("大功告成!\n");
return 0;
}
7. IPC 命令
1)显示
ipcs -m - 显示共享内存(m: memory)
ipcs -q - 显示消息队列(q: queue)
ipcs -s - 显示信号量(s: semphore)
ipcs -a - 显示所有IPC对象(a: all)
2)删除
ipcrm -m ID - 删除共享内存
ipcrm -q ID - 删除消息队列
ipcrm -s ID - 删除信号量