Linux高性能服务器编程
本文是读书笔记,如有侵权,请联系删除。
参考
Linux高性能服务器编程源码: https://github.com/raichen/LinuxServerCodes
豆瓣: Linux高性能服务器编程
文章目录
- Linux高性能服务器编程
- 第13章 多进程编程
- 13.1 fork 系统调用
- 13.2 exec系列系统调用
- 13.3 处理僵尸进程
- 13.4 管道
- 13.5 信号量
- 13.5.1 信号量原语
- 13.5.2 semget系统调用
- 13.5.3 semop系统调用
- 13.5.4 semctl系统调用
- 13.5.5 特殊键值IPC_PRIVATE
- 13.6 共享内存
- 13.6.1 shmget系统调用
- 13.6.2 shmat和shmdt系统调用
- 13.6.3 shmctl系统调用
- 13.6.4 共享内存的POSIX方法
- 13.7 消息队列
- 13.7.1 msgget系统调用
- 13.7.2 msgsnd系统调用
- 13.7.3 msgrcv系统调用
- 13.7.4 msgctl系统调用
- 13.8 IPC命令
- 13.9 在进程间传递文件描述符
- 后记
第13章 多进程编程
进程是Linux操作系统环境的基础,它控制着系统上几乎所有的活动。本章从系统程序员的角度来讨论Linux多进程编程,包括如下内容:
-
复制进程映像的fork系统调用和替换进程映像的exec系列系统调用。
-
僵尸进程以及如何避免僵尸进程。
-
进程间通信(Inter-Process Communication,IPC)最简单的方式:管道。
-
3种System V进程间通信方式:信号量、消息队列和共享内存。它们都是由AT&T System V2版本的UNIX引入的,所以统称为System V IPC。
-
在进程间传递文件描述符的通用方法:通过UNIX本地域socket传递特殊的辅助数据。
13.1 fork 系统调用
Linux 下创建新进程的系统调用是fork。其定义如下:
#include <sys/types.h>
#include <unistd.h>
pid_t fork( void );
该函数的每次调用都返回两次,在父进程中返回的是子进程的PID,在子进程中则返回 0。该返回值是后续代码判断当前进程是父进程还是子进程的依据。fork调用失败时返回-1,并设置errno。
fork函数复制当前进程,在内核进程表中创建一个新的进程表项。新的进程表项有很多属性和原进程相同,比如堆指针、栈指针和标志寄存器的值。但也有许多属性被赋予了新的值,比如该进程的PPID被设置成原进程的PID,信号位图被清除(原进程设置的信号处理函数不再对新进程起作用)。
子进程的代码与父进程完全相同,同时它还会复制父进程的数据(堆数据、栈数据和静态数据)。数据的复制采用的是所谓的写时复制(copy on writte),即只有在任一进程(父进程或子进程)对数据执行了写操作时,复制才会发生(先是缺页中断,然后操作系统给子进程分配内存并复制父进程的数据)。即便如此,如果我们在程序中分配了大量内存,那么使用fork时也应当十分谨慎,尽量避免没必要的内存分配和数据复制。
此外,创建子进程后,父进程中打开的文件描述符默认在子进程中也是打开的,且文件描述符的引用计数加1。不仅如此,父进程的用户根目录、当前工作目录等变量的引用计数均会加1。
13.2 exec系列系统调用
有时我们需要在子进程中执行其他程序,即替换当前进程映像,这就需要使用如下exec系列函数之一:
#include <unistd.h>
extern char** environ;
int execl( const char* path, const char* arg, ... );
int execlp( const char* file, const char* arg, ... );
int execle( const char* path, const char* arg, .., char* const envp[] );
int execv( const char* path, char* const argv[] );
int execvp( const char* file, char* const argv[] );
int execve( const char* path, char* const argv[], char* const envp[] );
path参数指定可执行文件的完整路径,file参数可以接受文件名,该文件的具体位置则在环境变量PATH中搜寻。arg接受可变参数,argv则接受参数数组,它们都会被传递给新 程序(path或file指定的程序)的main函数。envp参数用于设置新程序的环境变量。如果 未设置它,则新程序将使用由全局变量environ指定的环境变量。
一般情况下,exec函数是不返回的,除非出错。它出错时返回-1,并设置errno。如果没出错,则原程序中exec调用之后的代码都不会执行,因为此时原程序已经被exec的参数 指定的程序完全替换(包括代码和数据)。
exec函数不会关闭原程序打开的文件描述符,除非该文件描述符被设置了类似SOCK_CLOEXEC的属性(见5.2节)。
13.3 处理僵尸进程
对于多进程程序而言,父进程一般需要跟踪子进程的退出状态。因此,当子进程结束运行时,内核不会立即释放该进程的进程表表项,以满足父进程后续对该子进程退出信息的查询(如果父进程还在运行)。在子进程结束运行之后,父进程读取其退出状态之前,我们称该子进程处于僵尸态。另外一种使子进程进入僵尸态的情况是:父进程结束或者异常终止,而子进程继续运行。此时子进程的PPID将被操作系统设置为1,即init进程。init进程接管了该子进程,并等待它结束。在父进程退出之后,子进程退出之前,该子进程处于僵尸态。
由此可见,无论哪种情况,如果父进程没有正确地处理子进程的返回信息,子进程都将停留在僵尸态,并占据着内核资源。这是绝对不能容许的,毕竟内核资源有限。下面这对函数在父进程中调用,以等待子进程的结束,并获取子进程的返回信息,从而避免了僵尸进程的产生,或者使子进程的僵尸态立即结束:
#include <sys/types.h>
#include <sys/wait.h>
pid_t wait( int* stat_loc );
pid_t waitpid(pid_t pid, int* stat_loc, int options );
wait函数将阻塞进程,直到该进程的某个子进程结束运行为止。它返回结束运行的子进程的PID,并将该子进程的退出状态信息存储于stat_loc参数指向的内存中。sys/wait.h头文件中定义了几个宏来帮助解释子进程的退出状态信息,如表13-1所示。
wait函数的阻塞特性显然不是服务器程序期望的,而waitpid函数解决了这个问题。 waitpid只等待由pid参数指定的子进程。如果pid取值为-1,那么它就和wait函数相同,即等待任意一个子进程结束。stat_loc参数的含义和wait函数的stat_loc参数相同。options参数可以控制waitpid函数的行为。该参数最常用的取值是WNOHANG。当options的取值是 WNOHANG时,waitpid调用将是非阻塞的:如果pid指定的目标子进程还没有结束或意外终止,则waitpid立即返回0;如果目标子进程确实正常退出了,则waitpid返回该子进程的 PID。waitpid调用失败时返回-1并设置errno。
8.3节曾提到,要在事件已经发生的情况下执行非阻塞调用才能提高程序的效率。对waitpid函数而言,我们最好在某个子进程退出之后再调用它。那么父进程从何得知某个子进程已经退出了呢?这正是SIGCHLD信号的用途。当一个进程结束时,它将给其父进程发送一个SIGCHLD信号。因此,我们可以在父进程中捕获SIGCHLD信号,并在信号处理函数中调用waitpid函数以“彻底结束”一个子进程.
介绍waitpid函数
waitpid
函数用于等待指定的子进程退出,并返回子进程的退出状态。以下是对 waitpid
函数参数的简要介绍:
-
pid: 要等待的子进程的PID。
- 如果
pid > 0
,则等待具有指定PID的子进程退出。 - 如果
pid = -1
,则等待任意子进程退出。 - 如果
pid = 0
,则等待与调用进程属于同一进程组的任意子进程退出。 - 如果
pid < -1
,则等待进程组ID等于pid
绝对值的任意子进程退出。
- 如果
-
stat_loc: 用于存储子进程的退出状态的变量的指针。如果不关心退出状态,可以传递
NULL
。 -
options: 控制
waitpid
的行为的选项。WNOHANG
:如果指定的子进程没有退出,则立即返回,而不等待。WUNTRACED
:也等待已经停止的子进程的状态。
waitpid
函数返回子进程的PID,如果出错返回 -1。
下面是一个简单的示例,演示如何使用 waitpid
等待子进程退出:
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
int main() {
pid_t pid;
// 创建子进程
if ((pid = fork()) == -1) {
perror("fork");
exit(EXIT_FAILURE);
}
if (pid == 0) {
// 子进程执行的代码
printf("Child process: PID=%d\n", getpid());
sleep(2); // 模拟子进程执行任务
exit(EXIT_SUCCESS);
} else {
// 父进程执行的代码
printf("Parent process: PID=%d\n", getpid());
int status;
// 使用 waitpid 等待子进程退出
pid_t child_pid = waitpid(pid, &status, 0);
if (child_pid == -1) {
perror("waitpid");
exit(EXIT_FAILURE);
}
if (WIFEXITED(status)) {
printf("Child process %d exited with status %d\n", child_pid, WEXITSTATUS(status));
} else if (WIFSIGNALED(status)) {
printf("Child process %d terminated by signal %d\n", child_pid, WTERMSIG(status));
} else if (WIFSTOPPED(status)) {
printf("Child process %d stopped by signal %d\n", child_pid, WSTOPSIG(status));
}
}
return 0;
}
在这个示例中,父进程使用 fork
创建了一个子进程,然后通过 waitpid
等待子进程退出。父进程在等待时可以执行其他任务。子进程模拟执行任务后退出。父进程在 waitpid
返回后获取子进程的退出状态并进行处理。
13.4 管道
第6章中我们介绍过创建管道的系统调用pipe,我们也多次在代码中利用它来实现进程 内部的通信。实际上,管道也是父进程和子进程间通信的常用手段。
管道能在父、子进程间传递数据,利用的是fork调用之后两个管道文件描述符(fd[0] 和fd[1])都保持打开。一对这样的文件描述符只能保证父、子进程间一个方向的数据传输, 父进程和子进程必须有一个关闭fd[0],另一个关闭fd[1]。比如,我们要使用管道实现从父进程向子进程写数据,就应该按照图13-1所示来操作。
显然,如果要实现父、子进程之间的双向数据传输,就必须使用两个管道。socket编程接口提供了一个创建全双工管道的系统调用:socketpair。
管道(Pipe):
-
概述: 管道是一种进程间通信的机制,允许一个进程将数据发送给另一个进程。它创建了一个在两个相关进程之间共享数据的通道。管道通常有两种类型:匿名管道和命名管道。匿名管道适用于有亲缘关系的进程,而命名管道允许无关联的进程进行通信。
-
创建: 在C语言中,可以使用
pipe
系统调用创建匿名管道,它返回两个文件描述符,一个用于读取,一个用于写入。
#include <unistd.h>
int pipe(int pipefd[2]);
Socket Pair(socketpair):
-
概述:
socketpair
函数用于创建一对相互连接的套接字(socket),这对套接字之间允许进行双向通信。与管道类似,socketpair
主要用于进程间通信。 -
创建:
socketpair
返回两个套接字的文件描述符,一个用于读取,一个用于写入。这两个套接字被连接在一起,允许双向通信。
#include <sys/socket.h>
int socketpair(int domain, int type, int protocol, int sv[2]);
比较:
-
通信方式:
- 管道: 适用于有亲缘关系的进程,只支持单向通信(单个读端和单个写端)。
- socketpair: 适用于有亲缘关系或无亲缘关系的进程,支持双向通信。
-
通信方向:
- 管道: 单向通信,数据流只能在一个方向上流动。
- socketpair: 双向通信,数据可以在两个方向上流动。
-
数据类型:
- 管道: 适用于传递任意类型的数据。
- socketpair: 适用于传递网络字节序的二进制数据,也可用于传递文件描述符。
-
连接性:
- 管道: 适用于有亲缘关系的进程。
- socketpair: 适用于有亲缘关系或无亲缘关系的进程。
示例:
以下是一个简单的示例,演示如何使用 socketpair
创建双向通信的套接字对:
#include <sys/socket.h>
#include <stdio.h>
#include <unistd.h>
int main() {
int sockfd[2];
// 创建套接字对
if (socketpair(AF_UNIX, SOCK_STREAM, 0, sockfd) == -1) {
perror("socketpair");
return -1;
}
pid_t pid = fork();
if (pid == -1) {
perror("fork");
return -1;
} else if (pid == 0) {
// 子进程
close(sockfd[0]); // 关闭子进程的读端
write(sockfd[1], "Hello from child", sizeof("Hello from child"));
close(sockfd[1]);
} else {
// 父进程
close(sockfd[1]); // 关闭父进程的写端
char buffer[50];
read(sockfd[0], buffer, sizeof(buffer));
printf("Parent received: %s\n", buffer);
close(sockfd[0]);
}
return 0;
}
在这个示例中,socketpair
创建了一对套接字,父进程和子进程分别通过这对套接字进行通信。
13.5 信号量
13.5.1 信号量原语
当多个进程同时访问系统上的某个资源的时候,比如同时写一个数据库的某条记录,或者同时修改某个文件,就需要考虑进程的同步问题,以确保任一时刻只有一个进程可以拥有对资源的独占式访问。通常,程序对共享资源的访问的代码只是很短的一段,但就是这一段代码引发了进程之间的竞态条件。我们称这段代码为关键代码段,或者临界区。对进程同步,也就是确保任一时刻只有一个进程能进入关键代码段。
要编写具有通用目的的代码,以确保关键代码段的独占式访问是非常困难的。有两个名为Dekker算法和Peterson算法的解决方案,它们试图从语言本身(不需要内核支持)解决并发问题。但它们依赖于忙等待,即进程要持续不断地等待某个内存位置状态的改变。这种方式下CPU利用率太低,显然是不可取的。
Dijkstra提出的信号量(Semaphore)概念是并发编程领域迈出的重要一步。信号量是一种特殊的变量,它只能取自然数值并且只支持两种操作:等待(wait)和信号(signal)。不过在Linux/UNIX中,“等待”和“信号”都已经具有特殊的含义,所以对信号量的这两种操作更常用的称呼是P、V操作。这两个字母来自于荷兰语单词passeren(传递,就好像进入临界区)和vrijgeven(释放,就好像退出临界区)。假设有信号量SV,则对它的P、V操作含义如下:
P(SV),如果SV的值大于0,就将它减1;如果SV的值为0,则挂起进程的执行。
V(SV),如果有其他进程因为等待SV而挂起,则唤醒之;如果没有,则将SV加1。
信号量的取值可以是任何自然数。但最常用的、最简单的信号量是二进制信号量,它只能取0和1这两个值。本书仅讨论二进制信号量。使用二进制信号量同步两个进程,以确保 关键代码段的独占式访问的一个典型例子如图13-2所示。
在图13-2中,当关键代码段可用时,二进制信号量SV的值为1, 进程A和B都有机会进入关键代码段。如果此时进程A执行了P(SV)操作将SV减1,则进程B若再执行P(SV)操作就会被挂起。直到进程A离开关键代码段,并执行V(SV)操作将SV加1,关键代码段才重新变得可用。如果此时进程B因为等待SV 而处于挂起状态,则它将被唤醒,并进入关键代码段。同样,这时进程A如果再执行P(SV)操作,则也只能被操作系统挂起以等待进程B退出关键代码段。
注意使用一个普通变量来模拟二进制信号量是行不通的,因为所有高级语言都没有一个原子操作可以同时完成如下两步操作:检测变量是否为true/false,如果是则再将它设置为 false/true。
Linux信号量的API都定义在sys/sem.h头文件中,主要包含3个系统调用:semget、 semop和semctl。它们都被设计为操作一组信号量,即信号量集,而不是单个信号量,因此这些接口看上去多少比我们期望的要复杂一点。我们将分3小节依次讨论之。
13.5.2 semget系统调用
semget系统调用创建一个新的信号量集,或者获取一个已经存在的信号量集。其定义如下:
#include <sys/sem.h>
int semget( key_t key, int num_sems, int sem_flags );
key参数是一个键值,用来标识一个全局唯一的信号量集,就像文件名全局唯一地标识一个文件一样。要通过信号量通信的进程需要使用相同的键值来创建/获取该信号量。num_sems参数指定要创建/获取的信号量集中信号量的数目。如果是创建信号量,则该值必须被指定;如果是获取已经存在的信号量,则可以把它设置为0。sem_flags参数指定一组标志。它低端的9个比特是该信号量的权限,其格式和含义都与系统调用open的mode参数相同。此外,它还可以和IPC_CREAT标志做按位“或” 运算以创建新的信号量集。此时即使信号量已经存在,semget也不会产生错误。我们还可以联合使用IPC_CREAT和IPC_EXCL标志来确保创建一组新的、唯一的信号量集。在这种情况下,如果信号量集已经存在,则semget返回错误并设置errno为EEXIST。 这种创建信号量的行为与用O_CREAT和O_EXCL标志调用open来排他式地打开一个文件相似。
semget成功时返回一个正整数值,它是信号量集的标识符;semget失败时返回-1,并设置errno。如果semget用于创建信号量集,则与之关联的内核数据结构体 semid_ds将被创建并初始化。semid_ds结构体的定义如下:
semget 对semid_ds结构体的初始化包括:
将 sem_perm.cuid 和sem_perm.uid 设置为调用进程的有效用户 ID。
将sem_perm.cgid 和sem_perm.gid 设置为调用进程的有效组ID。
将sem_perm.mode的最低9位设置为sem_fiags参数的最低9位。
将sem_nsems设置为num_sems。
将sem_otime 设置为0。
将sem_ctime设置为当前的系统时间。
下面是semget的用例:
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
#include <stdio.h>
#include <stdlib.h>
int main() {
key_t key;
int semid;
// 生成键值
if ((key = ftok("/tmp", 'A')) == -1) {
perror("ftok");
exit(EXIT_FAILURE);
}
// 创建或获取信号量集
if ((semid = semget(key, 1, IPC_CREAT | 0666)) == -1) {
perror("semget");
exit(EXIT_FAILURE);
}
printf("Semaphore ID: %d\n", semid);
return 0;
}
在这个示例中,程序使用 ftok
函数生成一个键值,然后使用 semget
函数创建或获取一个包含一个信号量的信号量集。如果成功,semget
返回一个非负整数,表示信号量集的标识符。这个标识符可以用于后续的信号量操作。
13.5.3 semop系统调用
semop系统调用改变信号量的值,即执行P、V操作。在讨论semop之前,我们需要先介绍与每个信号量关联的一些重要的内核变量:
unsigned short semval; /*信号量的值*/
unsigned short semzcnt; /*等待信号量值变为0的进程数量*/
unsigned short semncnt; /*等待信号量值增加的进程数量*/
pid_t sempid; /*最后一次执行semop操作的进程ID*/
semop对信号量的操作实际上就是对这些内核变量的操作。semop的定义如下:
#include <sys/sem.h>
int semop( int sem_id, struct sembuf* sem_ops, size_t num_sem_ops );
sem_id参数是由semget调用返回的信号量集标识符,用以指定被操作的目标信号量集。 sem_ops参数指向一个sembuf结构体类型的数组,sembuf 结构体的定义如下:
struct sembuf
{
unsigned short int sem_num;
short int sem_op;
short int sem_flg;
}
其中,sem_num成员是信号量集中信号量的编号,0表示信号量集中的第一个信号量。sem_op成员指定操作类型,其可选值为正整数、0和负整数。每种类型的操作的行为又受到 sem_flg成员的影响。sem_flg的可选值是IPC_NOWAIT和SEM_UNDO。IPC_NOWAIT的 含义是,无论信号量操作是否成功,semop调用都将立即返回,这类似于非阻塞I/O操作。SEM_UNDO的含义是,当进程退出时取消正在进行的semop操作。具体来说,sem_op和 sem_fg将按照如下方式来影响semop的行为:
-
如果sem_op大于0,则semop将被操作的信号量的值semval增加sem_op。该操作要求调用进程对被操作信号量集拥有写权限。此时若设置了SEM_UNDO标志,则系统将更新进程的semadj变量(用以跟踪进程对信号量的修改情况)。
-
如果sem_op等于0,则表示这是一个“等待0”(wait-for-zero)操作。该操作要求调用进程对被操作信号量集拥有读权限。如果此时信号量的值是0,则调用立即成功返 回。如果信号量的值不是0,则semop失败返回或者阻塞进程以等待信号量变为0。 在这种情况下,当IPC_NOWAIT标志被指定时,semop立即返回一个错误,并设置 errno为EAGAIN。如果未指定IPC_NOWAIT标志,则信号量的semzcnt值加1,进程被投入睡眠直到下列3个条件之一发生:信号量的值semval变为0,此时系统将该信号量的semzcnt值减1;被操作信号量所在的信号量集被进程移除,此时semop调 用失败返回,errno被设置为EIDRM;调用被信号中断,此时semop调用失败返回, errno被设置为EINTR,同时系统将该信号量的semzcnt值减1。
-
如果sem_op小于0,则表示对信号量值进行减操作,即期望获得信号量。该操作要求调用进程对被操作信号量集拥有写权限。如果信号量的值semval大于或等于sem_op的绝对值,则semop操作成功,调用进程立即获得信号量,并且系统将 该信号量的semval值减去sem_op的绝对值。此时如果设置了SEM_UNDO标志,则系统将更新进程的semadj变量。如果信号量的值semval小于sem_op的绝对值,则semop失败返回或者阻塞进程以等待信号量可用。在这种情况下,当IPC NOWAIT标志被指定时,semop立即返回一个错误,并设置errno为EAGAIN。如 果未指定IPC_NOWAIT标志,则信号量的semncnt值加1,进程被投入睡眠直到下 列3个条件之一发生:信号量的值semval变得大于或等于sem_op的绝对值,此时系统将该信号量的semncnt值减1,并将semval 减去sem_op的绝对值,同时,如 果SEM_UNDO标志被设置,则系统更新semadj变量;被操作信号量所在的信号量集被进程移除,此时semop调用失败返回,errno被设置为EIDRM;调用被信号 中断,此时semop调用失败返回,errno被设置为EINTR,同时系统将该信号量的 semncnt值减1。
semop系统调用的第3个参数num_sem_ops 指定要执行的操作个数,即sem_ops数组中 元素的个数。semop对数组sem_ops中的每个成员按照数组顺序依次执行操作,并且该过程 是原子操作,以避免别的进程在同一时刻按照不同的顺序对该信号集中的信号量执行semop 操作导致的竞态条件。
semop成功时返回0,失败则返回-1并设置errno。失败的时候,sem_ops数组中指定的 所有操作都不被执行。
chatGPT对semop的介绍:
semop
是用于对信号量进行操作的系统调用,它允许进程执行多个操作(如等待、发送信号等)来操作信号量。该调用可以用于实现进程间同步和互斥。
以下是 semop
函数的原型:
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
int semop(int sem_id, struct sembuf* sem_ops, size_t num_sem_ops);
sem_id
:要操作的信号量集的标识符。sem_ops
:一个指向sembuf
结构数组的指针,表示要执行的一系列操作。num_sem_ops
:sem_ops
数组中的元素数量。
结构体 sembuf:
struct sembuf {
short sem_num; // 信号量在集合中的索引
short sem_op; // 操作:正数表示V操作(释放资源),负数表示P操作(请求资源)
short sem_flg; // 操作标志,通常为 SEM_UNDO(系统会跟踪操作,防止进程异常终止时的资源泄漏)
};
返回值:
- 成功时,返回 0。
- 失败时,返回 -1,并设置
errno
来指示错误类型。
示例:
以下是一个简单的示例,演示如何使用 semop
来进行信号量操作:
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
#include <stdio.h>
#include <stdlib.h>
int main() {
key_t key;
int semid;
// 生成键值
if ((key = ftok("/tmp", 'A')) == -1) {
perror("ftok");
exit(EXIT_FAILURE);
}
// 创建或获取信号量集
if ((semid = semget(key, 1, IPC_CREAT | 0666)) == -1) {
perror("semget");
exit(EXIT_FAILURE);
}
// 定义信号量操作结构体
struct sembuf sem_op;
// 初始化信号量操作
sem_op.sem_num = 0; // 操作的信号量在集合中的索引
sem_op.sem_op = 1; // V操作,增加信号量的值
sem_op.sem_flg = 0; // 操作标志,通常为0
// 执行信号量操作
if (semop(semid, &sem_op, 1) == -1) {
perror("semop");
exit(EXIT_FAILURE);
}
printf("Semaphore operation successful\n");
return 0;
}
在这个示例中,程序使用 semget
函数创建或获取一个信号量集,然后使用 semop
函数对信号量进行操作。在这个例子中,通过执行一个 V 操作(增加信号量的值),程序增加了信号量的值。在实际应用中,sem_op
结构体的 sem_op
成员可以是正数(V操作)或负数(P操作,请求资源)。
13.5.4 semctl系统调用
semctl系统调用允许调用者对信号量进行直接控制。其定义如下:
#include <sys/sem.h>
int semctl( int sem_id, int sem_num, int command, ... );
sem_id参数是由semget调用返回的信号量集标识符,用以指定被操作的信号量集。sem_num参数指定被操作的信号量在信号量集中的编号。command参数指定要执行的命令。 有的命令需要调用者传递第4个参数。第4个参数的类型由用户自己定义,但sys/sem.h头文件给出了它的推荐格式,具体如下:
下面是chatGPT对semctl的介绍:
semctl
是一个用于对信号量进行控制操作的系统调用。它提供了对信号量集的各种操作,包括获取信号量信息、设置信号量信息、删除信号量等。semctl
被广泛用于进程间同步和互斥的实现。
以下是 semctl
函数的原型:
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
int semctl(int sem_id, int sem_num, int cmd, ...);
sem_id
:信号量集的标识符。sem_num
:信号量在集合中的索引。cmd
:要执行的操作,可以是以下命令之一:GETVAL
:获取信号量的值。SETVAL
:设置信号量的值。IPC_RMID
:删除信号量集。IPC_STAT
:获取信号量的状态。IPC_SET
:设置信号量的状态。
- 可选参数:根据
cmd
的不同,可能需要传递额外的参数。
返回值:
- 对于
GETVAL
,返回信号量的当前值。 - 对于其他命令,成功时返回 0,失败时返回 -1,并设置
errno
。
示例:
以下是一个简单的示例,演示如何使用 semctl
进行不同的信号量操作:
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
#include <stdio.h>
#include <stdlib.h>
int main() {
key_t key;
int semid;
// 生成键值
if ((key = ftok("/tmp", 'A')) == -1) {
perror("ftok");
exit(EXIT_FAILURE);
}
// 创建或获取信号量集
if ((semid = semget(key, 1, IPC_CREAT | 0666)) == -1) {
perror("semget");
exit(EXIT_FAILURE);
}
// 设置信号量的值
int set_value = 5;
if (semctl(semid, 0, SETVAL, set_value) == -1) {
perror("semctl SETVAL");
exit(EXIT_FAILURE);
}
printf("Semaphore value set to %d\n", set_value);
// 获取信号量的值
int get_value = semctl(semid, 0, GETVAL);
if (get_value == -1) {
perror("semctl GETVAL");
exit(EXIT_FAILURE);
}
printf("Current semaphore value: %d\n", get_value);
// 删除信号量集
if (semctl(semid, 0, IPC_RMID) == -1) {
perror("semctl IPC_RMID");
exit(EXIT_FAILURE);
}
printf("Semaphore set deleted\n");
return 0;
}
在这个示例中,程序使用 semctl
进行设置信号量值、获取信号量值和删除信号量集的操作。这些操作可以通过指定不同的 cmd
参数来实现。
13.5.5 特殊键值IPC_PRIVATE
semget的调用者可以给其key参数传递一个特殊的键值IPC_PRIVATE(其值为0),这样无论该信号量是否已经存在,semget都将创建一个新的信号量。使用该键值创建的信号量并非像它的名字声称的那样是进程私有的。其他进程,尤其是子进程,也有方法来访问这个信号量。所以semget的man手册的BUGS部分上说,使用名字IPC_PRIVATE有些误导(历史原因),应该称为IPC_NEW。比如下面的代码清单13-3就在父、子进程间使用一个IPC_PRIVATE信号量来同步。
13-3sem.cpp
#include <sys/sem.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
// 定义一个联合体,用于 semctl 函数中设置和获取信号量的值
union semun
{
int val; // 用于 SETVAL 命令,设置信号量的值
struct semid_ds* buf; // 用于 IPC_STAT 和 IPC_SET 命令,获取和设置信号量的状态
unsigned short int* array;// 用于 GETALL、SETALL 和 GETNCNT 命令,获取和设置信号量数组的值
struct seminfo* __buf; // 用于 IPC_INFO 命令,获取信号量的信息
};
// 执行 P 操作和 V 操作的函数
// op 为 -1 时执行 P 操作(等待资源),op 为 1 时执行 V 操作(释放资源)
void pv(int sem_id, int op) {
struct sembuf sem_b;
sem_b.sem_num = 0;
sem_b.sem_op = op;
sem_b.sem_flg = SEM_UNDO;
semop(sem_id, &sem_b, 1); // 执行 semop 函数,对信号量进行操作
}
int main(int argc, char* argv[]) {
// 创建一个包含一个二进制信号量的信号量集
int sem_id = semget(IPC_PRIVATE, 1, 0666);
// 初始化信号量的值为 1
union semun sem_un;
sem_un.val = 1;
semctl(sem_id, 0, SETVAL, sem_un);
// 创建子进程
pid_t id = fork();
if (id < 0) {
return 1; // 失败处理
} else if (id == 0) {
// 子进程
printf("child try to get binary sem\n");
pv(sem_id, -1); // 执行 P 操作,等待资源
printf("child get the sem and would release it after 5 seconds\n");
sleep(5); // 子进程等待 5 秒
pv(sem_id, 1); // 执行 V 操作,释放资源
exit(0);
} else {
// 父进程
printf("parent try to get binary sem\n");
pv(sem_id, -1); // 执行 P 操作,等待资源
printf("parent get the sem and would release it after 5 seconds\n");
sleep(5); // 父进程等待 5 秒
pv(sem_id, 1); // 执行 V 操作,释放资源
}
waitpid(id, NULL, 0); // 父进程等待子进程结束
semctl(sem_id, 0, IPC_RMID, sem_un); // 删除信号量集
return 0;
}
详细注释:
-
union semun
结构体: 用于设置和获取信号量的值。val
用于 SETVAL 命令,buf
用于 IPC_STAT 和 IPC_SET 命令,array
用于 GETALL、SETALL 和 GETNCNT 命令,__buf
用于 IPC_INFO 命令。 -
pv
函数: 封装了对信号量的 P 操作和 V 操作。P 操作通过等待资源来保护临界区,V 操作通过释放资源来结束对临界区的保护。 -
主函数
main
:- 创建包含一个二进制信号量的信号量集。
- 初始化信号量的值为 1。
- 创建子进程,子进程尝试获取信号量,等待 5 秒后释放。
- 父进程也尝试获取信号量,等待 5 秒后释放。
- 父进程等待子进程结束。
- 删除信号量集。
13.6 共享内存
共享内存是最高效的IPC机制,因为它不涉及进程之间的任何数据传输。这种高效率带来的问题是,我们必须用其他辅助手段来同步进程对共享内存的访问,否则会产生竞态条件。因此,共享内存通常和其他进程间通信方式一起使用。Linux共享内存的API都定义在sys/shm.h头文件中,包括4个系统调用:shmget、 shmat、shmdt和shmctl。我们将依次讨论之。
13.6.1 shmget系统调用
shmget系统调用创建一段新的共享内存,或者获取一段已经存在的共享内存。其定义如下:
#include <sys/shm.h>
int shmget( key_t key, size_t size, int shmflg );
和semget系统调用一样,key参数是一个键值,用来标识一段全局唯一的共享内存。 size参数指定共享内存的大小,单位是字节。如果是创建新的共享内存,则size值必须被指定。如果是获取已经存在的共享内存,则可以把size设置为0。shmflg参数的使用和含义与semget系统调用的sem_flags参数相同。不过shmget支持两个额外的标志——SHM_HUGETLB和SHM_NORESERVE。它们的含义如下:
-
SHM_HUGETLB,类似于mmap的MAP_HUGETLB标志,系统将使用“大页面” 来为共享内存分配空间。
-
SHM_NORESERVE,类似于mmap的MAP_NORESERVE标志,不为共享内存保留交换分区(swap空间)。这样,当物理内存不足的时候,对该共享内存执行写操作将 触发SIGSEGV信号。
shmget成功时返回一个正整数值,它是共享内存的标识符。shmget失败时返回-1,并设置errno。如果shmget用于创建共享内存,则这段共享内存的所有字节都被初始化为0,与之关联 的内核数据结构shmid_ds将被创建并初始化。shmid_ds结构体的定义如下:
shmget对 shmid_ds结构体的初始化包括:
将shm_perm.cuid 和shm_perm.uid 设置为调用进程的有效用户 ID。
将shm_perm.cgid和shm_perm.gid 设置为调用进程的有效组ID。
将shm_perm.mode的最低9位设置为shmflg参数的最低9位。
将shm_segsz设置为size。
将shm_lpid、shm_nattach、shm_atime、shm_dtime 设置为0。
将shm_ctime设置为当前的时间。
13.6.2 shmat和shmdt系统调用
共享内存被创建/获取之后,我们不能立即访问它,而是需要先将它关联到进程的地址空间中。使用完共享内存之后,我们也需要将它从进程地址空间中分离。这两项任务分别由如下两个系统调用实现:
#include <sys/shm.h>
void* shmat( int shm_id, const void* shm_addr, int shmflg );
int shmdt( const void* shm_addr );
其中,shm_id参数是由shmget 调用返回的共享内存标识符。shm_addr参数指定将共享内存关联到进程的哪块地址空间,最终的效果还受到shmflg参数的可选标志SHM_RND的 影响:
- 如果shm_addr为NULL,则被关联的地址由操作系统选择。这是推荐的做法,以确保代码的可移植性。
- 如果shm_addr非空,并且SHM_RND标志未被设置,则共享内存被关联到addr指定 的地址处。
- 如果shm_addr非空,并且设置了SHM_RND标志,则被关联的地址是
[shm_addr- (shm_addr%SHMLBA)]
。SHMLBA的含义是“段低端边界地址倍数”(Segment Low Boundary Address Multiple),它必须是内存页面大小(PAGE_SIZE)的整数倍。现在的Linux内核中,它等于一个内存页大小。SHM_RND的含义是圆整 (round),即将共享内存被关联的地址向下圆整到离shm_addr最近的SHMLBA的整数倍地址处。
除了SHM_RND标志外,shmflg参数还支持如下标志:
- SHM_RDONLY。进程仅能读取共享内存中的内容。若没有指定该标志,则进程可同 时对共享内存进行读写操作(当然,这需要在创建共享内存的时候指定其读写权限)。
- SHM_REMAP。如果地址shmaddr已经被关联到一段共享内存上,则重新关联。
- SHM_EXEC。它指定对共享内存段的执行权限。对共享内存而言,执行权限实际上 和读权限是一样的。
shmat 成功时返回共享内存被关联到的地址,失败则返回(void*)-1并设置errno。shmat 成功时,将修改内核数据结构shmid_ds的部分字段,如下:将shm_nattach加1。将shm_lpid设置为调用进程的PID。将shm_atime设置为当前的时间。
shmdt函数将关联到shm_addr处的共享内存从进程中分离。它成功时返回0,失败则返 回-l并设置errno。shmdt在成功调用时将修改内核数据结构shmid_ds的部分字段,如下:将shm_nattach减1。将shm_lpid设置为调用进程的PID。将shm_dtime设置为当前的时间。
chatGPT的介绍:
1. void* shmat(int shm_id, const void* shm_addr, int shmflg);
-
功能: 用于将共享内存连接到调用进程的地址空间。
-
参数:
shm_id
:共享内存标识符,由shmget
函数返回。shm_addr
:指定连接的地址,通常设置为NULL
,让系统自动选择合适的地址。shmflg
:标志位,通常设置为 0。
-
返回值:
- 连接成功时返回共享内存的起始地址,连接失败时返回
(void*)-1
。
- 连接成功时返回共享内存的起始地址,连接失败时返回
-
使用示例:
#include <sys/shm.h> #include <stdio.h> int main() { int shm_id = shmget(IPC_PRIVATE, 1024, IPC_CREAT | 0666); // 创建共享内存 void* shm_ptr = shmat(shm_id, NULL, 0); // 将共享内存连接到进程地址空间 // 使用共享内存 // ... shmdt(shm_ptr); // 断开共享内存连接 // 删除共享内存 shmctl(shm_id, IPC_RMID, NULL); return 0; }
2. int shmdt(const void* shm_addr);
-
功能: 用于断开共享内存与调用进程地址空间的连接。
-
参数:
shm_addr
:要断开连接的共享内存的起始地址。
-
返回值:
- 成功时返回 0,失败时返回 -1。
-
使用示例:
#include <sys/shm.h> #include <stdio.h> int main() { int shm_id = shmget(IPC_PRIVATE, 1024, IPC_CREAT | 0666); // 创建共享内存 void* shm_ptr = shmat(shm_id, NULL, 0); // 将共享内存连接到进程地址空间 // 使用共享内存 // ... shmdt(shm_ptr); // 断开共享内存连接 // 删除共享内存 shmctl(shm_id, IPC_RMID, NULL); return 0; }
共享内存操作流程:
- 使用
shmget
创建或获取共享内存,得到共享内存标识符。 - 使用
shmat
将共享内存连接到进程地址空间,得到共享内存的起始地址。 - 在进程中使用共享内存进行读写操作。
- 使用
shmdt
断开共享内存与进程地址空间的连接。 - 可选:使用
shmctl
删除共享内存。
注意:
- 连接共享内存后,进程对共享内存的修改会影响其他连接到同一共享内存的进程。
- 在完成对共享内存的使用后,需要使用
shmdt
断开连接,防止内存泄漏。
13.6.3 shmctl系统调用
shmctl
是用于对共享内存进行控制操作的系统调用。它提供了对共享内存的各种操作,包括获取共享内存的状态信息、设置共享内存的状态、删除共享内存等。
以下是 shmctl
函数的原型:
#include <sys/shm.h>
int shmctl(int shm_id, int cmd, struct shmid_ds *buf);
shm_id
:共享内存标识符,由shmget
函数返回。cmd
:指定要执行的操作,可以是以下命令之一:IPC_STAT
:获取共享内存的状态信息,将结果存储在buf
中。IPC_SET
:设置共享内存的状态信息,使用buf
中的信息。IPC_RMID
:删除共享内存。
buf
:指向shmid_ds
结构体的指针,用于存储或提供共享内存的状态信息。
返回值:
- 执行成功时返回 0。
- 执行失败时返回 -1,并设置
errno
。
示例:
#include <sys/shm.h>
#include <stdio.h>
#include <stdlib.h>
int main() {
key_t key = ftok("/tmp", 'A');
int shm_id = shmget(key, 1024, IPC_CREAT | 0666); // 创建共享内存
// 获取共享内存的状态信息
struct shmid_ds shmid_ds_info;
if (shmctl(shm_id, IPC_STAT, &shmid_ds_info) == -1) {
perror("shmctl IPC_STAT");
exit(EXIT_FAILURE);
}
// 打印共享内存的状态信息
printf("Size of the shared memory: %lu bytes\n", shmid_ds_info.shm_segsz);
printf("Last operation by process ID: %d\n", shmid_ds_info.shm_lpid);
// 设置共享内存的状态信息
shmid_ds_info.shm_perm.uid = 1000; // 设置 UID
if (shmctl(shm_id, IPC_SET, &shmid_ds_info) == -1) {
perror("shmctl IPC_SET");
exit(EXIT_FAILURE);
}
// 删除共享内存
if (shmctl(shm_id, IPC_RMID, NULL) == -1) {
perror("shmctl IPC_RMID");
exit(EXIT_FAILURE);
}
return 0;
}
这个示例演示了如何使用 shmctl
对共享内存进行状态信息的获取、设置和删除操作。
13.6.4 共享内存的POSIX方法
6.5节中我们介绍过mmap函数。利用它的MAP_ANONYMOUS标志我们可以实现父、子进程之间的匿名内存共享。通过打开同一个文件,mmap也可以实现无关进程之间的内存共享。Linux提供了另外一种利用mmap在无关进程之间共享内存的方式。这种方式无须任何文件的支持,但它需要先使用如下函数来创建或打开一个POSIX共享内存对象:
#include <sys/mman.h>
#include <sys/stat.h>
#include <fcntl.h>
int shm_open( const char* name, int oflag, mode_t mode );
shm_open的使用方法与open系统调用完全相同。
name参数指定要创建/打开的共享内存对象。从可移植性的角度考虑,该参数应该使用 “/somename”的格式:以“/”开始,后接多个字符,且这些字符都不是“/”;以“\0”结 尾,长度不超过NAME MAX(通常是255)。
oflag参数指定创建方式。它可以是下列标志中的一个或者多个的按位或:
- ORDONLY。以只读方式打开共享内存对象。
- O_RDWR。以可读、可写方式打开共享内存对象。
- OCREAT。如果共享内存对象不存在,则创建之。此时mode参数的最低9位将指 定该共享内存对象的访问权限。共享内存对象被创建的时候,其初始长度为0。
- O_EXCL。和O_CREAT一起使用,如果由name指定的共享内存对象已经存在,则 shm_open 调用返回错误,否则就创建一个新的共享内存对象。
- O_TRUNC。如果共享内存对象已经存在,则把它截断,使其长度为0。
shm_open 调用成功时返回一个文件描述符。该文件描述符可用于后续的mmap 调用, 从而将共享内存关联到调用进程。shm_open失败时返回-1,并设置 errno。和打开的文件最后需要关闭一样,由shm_open创建的共享内存对象使用完之后也需要 被删除。这个过程是通过如下函数实现的:
#include <sys/mman.h>
#include <sys/stat.h>
#include <fcntl.h>
int shm_unlink( const char *name );
该函数将name参数指定的共享内存对象标记为等待删除。当所有使用该共享内存对象 的进程都使用ummap将它从进程中分离之后,系统将销毁这个共享内存对象所占据的资源。如果代码中使用了上述POSIX共享内存函数,则编译的时候需要指定链接选项-lrt。
chatgpt对共享内存POSIX方法的介绍
POSIX提供了一种用于共享内存的机制,称为POSIX共享内存。在POSIX标准中,使用 shm_open
和 mmap
函数来创建和映射共享内存。
以下是使用POSIX共享内存的一般步骤:
-
创建或打开共享内存对象:
- 使用
shm_open
函数创建或打开具有唯一名称的共享内存对象。如果已存在同名的共享内存对象,则会打开它;否则,将创建一个新的对象。
#include <fcntl.h> #include <sys/mman.h> int shm_open(const char *name, int oflag, mode_t mode);
name
:共享内存对象的名称。oflag
:标志,控制共享内存对象的打开方式。mode
:权限掩码,用于创建新的共享内存对象。
- 使用
-
设置共享内存大小:
- 使用
ftruncate
函数设置共享内存对象的大小。
#include <unistd.h> int ftruncate(int fd, off_t length);
fd
:共享内存对象的文件描述符。length
:设置的共享内存大小。
- 使用
-
映射共享内存到进程地址空间:
- 使用
mmap
函数将共享内存对象映射到进程的地址空间。
#include <sys/mman.h> void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);
addr
:指定映射的起始地址(通常设置为NULL
)。length
:映射的长度。prot
:保护位,指定对映射区域的保护。flags
:标志位,指定映射的属性。fd
:共享内存对象的文件描述符。offset
:指定从共享内存对象的哪个位置开始映射。
- 使用
-
使用共享内存:
- 现在,可以在进程的地址空间中使用共享内存,进行读写等操作。
-
解除映射和关闭共享内存对象:
- 当不再需要共享内存时,使用
munmap
函数解除映射。
#include <sys/mman.h> int munmap(void *addr, size_t length);
-
addr
:映射的起始地址。 -
length
:映射的长度。 -
使用
close
函数关闭共享内存对象的文件描述符。
#include <unistd.h> int close(int fd);
fd
:共享内存对象的文件描述符。
- 当不再需要共享内存时,使用
-
删除共享内存对象(可选):
- 如果不再需要共享内存对象,可以使用
shm_unlink
函数删除它。
#include <sys/mman.h> int shm_unlink(const char *name);
name
:共享内存对象的名称。
- 如果不再需要共享内存对象,可以使用
这些函数提供了一种在POSIX系统中创建和使用共享内存的方法。POSIX共享内存是一种具有灵活性和跨平台性的机制,适用于需要多进程之间共享数据的场景。
13.7 消息队列
消息队列是在两个进程之间传递二进制块数据的一种简单有效的方式。每个数据块都有 一个特定的类型,接收方可以根据类型来有选择地接收数据,而不一定像管道和命名管道那 样必须以先进先出的方式接收数据。Linux消息队列的API都定义在sys/msg.h头文件中,包括4个系统调用:msgget、 msgsnd、msgrcv和msgctl。我们将依次讨论之。
13.7.1 msgget系统调用
msgget
是一个用于创建或获取 System V 消息队列的系统调用。System V IPC(Inter-Process Communication)是一组在Unix-like操作系统上用于进程间通信的API。消息队列是其中的一种通信机制,允许不同进程通过在队列中发送和接收消息来进行通信。
下面是 msgget
系统调用的基本介绍:
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
int msgget(key_t key, int msgflg);
key
是消息队列的标识符。多个进程可以通过相同的key
来访问同一个消息队列。msgflg
是一组标志,用于指定队列的创建和访问权限。
msgget
的功能主要有两个方面:
-
创建消息队列:
如果指定的key
对应的消息队列不存在,则会创建一个新的消息队列。如果已经存在,则返回该消息队列的标识符。 -
获取消息队列:
如果指定的key
对应的消息队列已经存在,但调用进程没有足够的权限访问它,那么根据msgflg
的设置,可能会返回错误或者创建一个新的队列。
使用 msgget
后,通常会使用其他系统调用如 msgsnd
(发送消息)和 msgrcv
(接收消息)来实际进行消息的发送和接收。
注意:msgget
和相关的消息队列系统调用在现代的Unix-like系统中通常已经被 POSIX 消息队列(mq_open
、mq_send
、mq_receive
等)所取代。
以下是一个简单的示例,演示如何使用 msgget
创建或获取消息队列的过程:
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#define MSGKEY 1234 // 指定消息队列的key
int main() {
int msgid;
// 创建或获取消息队列
msgid = msgget((key_t)MSGKEY, IPC_CREAT | 0666);
if (msgid == -1) {
perror("msgget");
exit(EXIT_FAILURE);
}
printf("Message Queue ID: %d\n", msgid);
// 其他操作,如发送和接收消息
// 删除消息队列
if (msgctl(msgid, IPC_RMID, NULL) == -1) {
perror("msgctl");
exit(EXIT_FAILURE);
}
return 0;
}
在此示例中,我们使用 msgget
创建或获取一个消息队列,并指定了一个键值 MSGKEY
。然后,可以在这个消息队列上执行其他的消息发送、接收等操作。最后,通过 msgctl
删除消息队列。
请注意,这只是一个基本的示例,实际使用中可能需要更多的错误检查和具体的消息发送/接收逻辑。
13.7.2 msgsnd系统调用
msgsnd
是用于向 System V 消息队列发送消息的系统调用。System V IPC(Inter-Process Communication)是一组在 Unix-like 操作系统上用于进程间通信的 API。消息队列是其中的一种通信机制,允许不同进程通过在队列中发送和接收消息来进行通信。
下面是 msgsnd
系统调用的基本介绍:
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);
msqid
是消息队列的标识符,它是由msgget
返回的。msgp
是指向要发送的消息的指针。msgsz
是消息的大小(以字节为单位)。msgflg
是一组标志,用于指定发送消息的行为。
msgsnd
的主要功能是将消息发送到消息队列中。具体步骤如下:
- 调用
msgget
获取或创建消息队列,获得msqid
。 - 准备要发送的消息,并将消息内容放入一个结构体中,通常为用户定义的结构体。
- 调用
msgsnd
将消息发送到消息队列。
以下是一个简单的示例,演示如何使用 msgsnd
发送消息:
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <string.h>
#define MSGKEY 1234 // 指定消息队列的key
// 用户定义的消息结构体
struct msgbuf {
long mtype; // 消息类型,必须大于0
char mtext[256]; // 消息内容
};
int main() {
int msgid;
struct msgbuf msg;
// 创建或获取消息队列
msgid = msgget((key_t)MSGKEY, IPC_CREAT | 0666);
if (msgid == -1) {
perror("msgget");
exit(EXIT_FAILURE);
}
// 准备消息
msg.mtype = 1; // 设置消息类型
strcpy(msg.mtext, "Hello, Message Queue!");
// 发送消息
if (msgsnd(msgid, &msg, sizeof(msg.mtext), 0) == -1) {
perror("msgsnd");
exit(EXIT_FAILURE);
}
printf("Message sent to the queue.\n");
return 0;
}
在此示例中,我们首先创建或获取一个消息队列,然后准备一个消息结构体 msg
,设置消息类型和内容,最后使用 msgsnd
将消息发送到消息队列中。请注意,实际使用中可能需要更多的错误检查和复杂的消息内容处理。
13.7.3 msgrcv系统调用
msgrcv
是用于从 System V 消息队列接收消息的系统调用。System V IPC(Inter-Process Communication)是一组在 Unix-like 操作系统上用于进程间通信的 API。消息队列是其中的一种通信机制,允许不同进程通过在队列中发送和接收消息来进行通信。
下面是 msgrcv
系统调用的基本介绍:
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp, int msgflg);
msqid
是消息队列的标识符,它是由msgget
返回的。msgp
是指向存储接收消息的缓冲区的指针。msgsz
是缓冲区的大小,即接收消息的最大字节数。msgtyp
是要接收的消息类型。如果指定为 0,则表示接收队列中的第一条消息。msgflg
是一组标志,用于指定接收消息的行为。
msgrcv
的主要功能是从消息队列中接收消息。具体步骤如下:
- 调用
msgget
获取或创建消息队列,获得msqid
。 - 准备一个缓冲区,用于存储接收到的消息。
- 调用
msgrcv
接收消息。
以下是一个简单的示例,演示如何使用 msgrcv
接收消息:
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <string.h>
#define MSGKEY 1234 // 指定消息队列的key
// 用户定义的消息结构体
struct msgbuf {
long mtype; // 消息类型,必须大于0
char mtext[256]; // 消息内容
};
int main() {
int msgid;
struct msgbuf msg;
// 创建或获取消息队列
msgid = msgget((key_t)MSGKEY, IPC_CREAT | 0666);
if (msgid == -1) {
perror("msgget");
exit(EXIT_FAILURE);
}
// 接收消息
if (msgrcv(msgid, &msg, sizeof(msg.mtext), 0, 0) == -1) {
perror("msgrcv");
exit(EXIT_FAILURE);
}
// 打印接收到的消息
printf("Received Message: %s\n", msg.mtext);
return 0;
}
在此示例中,我们首先创建或获取一个消息队列,然后准备一个消息结构体 msg
,最后使用 msgrcv
从消息队列中接收消息。请注意,实际使用中可能需要更多的错误检查和适应不同消息类型的处理。
13.7.4 msgctl系统调用
msgctl
是用于控制 System V 消息队列的系统调用。System V IPC(Inter-Process Communication)是一组在 Unix-like 操作系统上用于进程间通信的 API。消息队列是其中的一种通信机制,允许不同进程通过在队列中发送和接收消息来进行通信。
下面是 msgctl
系统调用的基本介绍:
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
int msgctl(int msqid, int cmd, struct msqid_ds *buf);
msqid
是消息队列的标识符,它是由msgget
返回的。cmd
是要执行的操作,可以是以下值之一:IPC_STAT
:获取消息队列的状态信息,并将其存储在struct msqid_ds
结构中。IPC_SET
:设置消息队列的状态信息,使用struct msqid_ds
中的数据。IPC_RMID
:删除消息队列。
buf
是指向struct msqid_ds
结构的指针,用于存储或提供消息队列的状态信息。
msgctl
的主要功能包括获取消息队列状态信息、设置消息队列状态信息和删除消息队列。
以下是一个简单的示例,演示如何使用 msgctl
获取消息队列状态信息、设置消息队列状态信息和删除消息队列:
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#define MSGKEY 1234 // 指定消息队列的key
int main() {
int msgid;
struct msqid_ds msqstat;
// 创建或获取消息队列
msgid = msgget((key_t)MSGKEY, IPC_CREAT | 0666);
if (msgid == -1) {
perror("msgget");
exit(EXIT_FAILURE);
}
// 获取消息队列状态信息
if (msgctl(msgid, IPC_STAT, &msqstat) == -1) {
perror("msgctl IPC_STAT");
exit(EXIT_FAILURE);
}
printf("Message Queue ID: %d\n", msgid);
printf("Messages in Queue: %lu\n", msqstat.msg_qnum);
// 设置消息队列状态信息(示例中将最大字节数设置为1024)
msqstat.msg_qbytes = 1024;
if (msgctl(msgid, IPC_SET, &msqstat) == -1) {
perror("msgctl IPC_SET");
exit(EXIT_FAILURE);
}
printf("Max Bytes in Queue set to 1024\n");
// 删除消息队列
if (msgctl(msgid, IPC_RMID, NULL) == -1) {
perror("msgctl IPC_RMID");
exit(EXIT_FAILURE);
}
printf("Message Queue deleted.\n");
return 0;
}
在此示例中,我们首先创建或获取一个消息队列,然后使用 msgctl
获取消息队列的状态信息,并打印队列中的消息数量。接着,通过 msgctl
设置消息队列的状态信息,将最大字节数设置为1024。最后,使用 msgctl
删除消息队列。请注意,实际使用中可能需要更多的错误检查和更复杂的状态设置。
13.8 IPC命令
sudo ipcs
是一个用于显示 System V IPC 对象(消息队列、信号量、共享内存)的命令。System V IPC(Inter-Process Communication)是一组在 Unix-like 操作系统上用于进程间通信的 API。
具体而言,ipcs
命令的输出包括当前系统上存在的 IPC 对象的列表及其相关信息。使用 sudo
是为了获得足够的权限来查看系统的 IPC 对象信息。
以下是 ipcs
命令的一般格式:
sudo ipcs [options]
常用的选项包括:
-q
:显示消息队列的信息。-s
:显示信号量的信息。-m
:显示共享内存的信息。-a
:显示所有 IPC 对象的信息。
示例用法:
sudo ipcs -q # 显示消息队列信息
sudo ipcs -s # 显示信号量信息
sudo ipcs -m # 显示共享内存信息
sudo ipcs -a # 显示所有 IPC 对象信息
输出的信息包括对象的标识符、权限、创建者、大小等。以下是示例输出的一部分:
------ Message Queues --------
key msqid owner perms ...
0x00000000 32768 user 666 ...
------ Shared Memory Segments --------
key shmid owner perms ...
0x00000000 65536 user 666 ...
------ Semaphore Arrays --------
key semid owner perms nsems ...
0x00000000 98304 user 666 1 ...
在上面的示例中,列出了消息队列、共享内存和信号量的相关信息。标识符(key、msqid、shmid、semid)用于唯一标识 IPC 对象,owner 表示创建者,perms 表示权限,nsems 表示信号量数组的数量等。
13.9 在进程间传递文件描述符
由于fork调用之后,父进程中打开的文件描述符在子进程中仍然保持打开,所以文件描述符可以很方便地从父进程传递到子进程。需要注意的是,传递一个文件描述符并不是传递一个文件描述符的值,而是要在接收进程中创建一个新的文件描述符,并且该文件描述符和发送进程中被传递的文件描述符指向内核中相同的文件表项。那么如何把子进程中打开的文件描述符传递给父进程呢?或者更通俗地说,如何在两个不相干的进程之间传递文件描述符呢?在Linux下,我们可以利用UNIX域socket在进程间传递特殊的辅助数据,以实现文件描述符的传递。
在Linux下,可以使用UNIX域套接字(UNIX domain socket)在两个进程之间传递文件描述符。以下是一个简单的代码示例,演示了如何在子进程中打开一个文件,然后将文件描述符传递给父进程:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <fcntl.h>
#define SOCK_PATH "/tmp/file_fd_socket"
int send_fd(int socket, int fd) {
struct msghdr msg = {0};
struct cmsghdr *cmsg;
char buf[CMSG_SPACE(sizeof(int))];
char dummy = '*';
struct iovec io = {
.iov_base = &dummy,
.iov_len = 1
};
msg.msg_iov = &io;
msg.msg_iovlen = 1;
msg.msg_control = buf;
msg.msg_controllen = sizeof(buf);
cmsg = CMSG_FIRSTHDR(&msg);
cmsg->cmsg_level = SOL_SOCKET;
cmsg->cmsg_type = SCM_RIGHTS;
cmsg->cmsg_len = CMSG_LEN(sizeof(int));
*((int *)CMSG_DATA(cmsg)) = fd;
return sendmsg(socket, &msg, 0);
}
int recv_fd(int socket) {
struct msghdr msg = {0};
struct cmsghdr *cmsg;
char buf[CMSG_SPACE(sizeof(int))];
char dummy;
struct iovec io = {
.iov_base = &dummy,
.iov_len = 1
};
msg.msg_iov = &io;
msg.msg_iovlen = 1;
msg.msg_control = buf;
msg.msg_controllen = sizeof(buf);
if (recvmsg(socket, &msg, 0) < 0) {
perror("recvmsg");
return -1;
}
cmsg = CMSG_FIRSTHDR(&msg);
return *((int *)CMSG_DATA(cmsg));
}
int main() {
int sockfd, client_fd;
struct sockaddr_un server_addr, client_addr;
socklen_t client_len = sizeof(client_addr);
// 创建UNIX域套接字
sockfd = socket(AF_UNIX, SOCK_STREAM, 0);
if (sockfd == -1) {
perror("socket");
exit(EXIT_FAILURE);
}
// 绑定地址
server_addr.sun_family = AF_UNIX;
strcpy(server_addr.sun_path, SOCK_PATH);
if (bind(sockfd, (struct sockaddr*)&server_addr, sizeof(server_addr)) == -1) {
perror("bind");
exit(EXIT_FAILURE);
}
// 监听连接
if (listen(sockfd, 5) == -1) {
perror("listen");
exit(EXIT_FAILURE);
}
printf("Server listening on %s\n", SOCK_PATH);
// 等待客户端连接
client_fd = accept(sockfd, (struct sockaddr*)&client_addr, &client_len);
if (client_fd == -1) {
perror("accept");
exit(EXIT_FAILURE);
}
// 在子进程中打开文件,并传递文件描述符给父进程
if (fork() == 0) {
close(sockfd); // 关闭在子进程中不需要的套接字
int file_fd = open("example.txt", O_RDONLY);
if (file_fd == -1) {
perror("open");
exit(EXIT_FAILURE);
}
// 传递文件描述符给父进程
if (send_fd(client_fd, file_fd) == -1) {
perror("send_fd");
exit(EXIT_FAILURE);
}
close(file_fd); // 子进程不再需要文件描述符
exit(EXIT_SUCCESS);
} else {
close(client_fd); // 在父进程中关闭在子进程中不需要的套接字
// 接收子进程传递的文件描述符
int received_fd = recv_fd(sockfd);
if (received_fd == -1) {
perror("recv_fd");
exit(EXIT_FAILURE);
}
// 在父进程中使用接收到的文件描述符
char buffer[1024];
ssize_t bytesRead = read(received_fd, buffer, sizeof(buffer));
if (bytesRead == -1) {
perror("read");
exit(EXIT_FAILURE);
}
printf("Received data from file: %.*s\n", (int)bytesRead, buffer);
close(received_fd); // 关闭文件描述符
close(sockfd); // 关闭套接字
}
return 0;
}
在这个例子中,父进程创建了一个UNIX域套接字,并在其中监听连接。子进程打开一个文件,并通过UNIX域套接字将文件描述符传递给父进程。在父进程中,接收到的文件描述符被用于读取文件内容。请注意,实际使用中可能需要更多的错误检查和边界处理。
后记
截至2024年1月24日14点40分,学习完多进程一章,感觉都是一些系统调用API的介绍,学习到的不多。