前言
进程通信(Inter-Process Communication,IPC)是指在不同的进程之间传递和共享信息的机制。进程通信是操作系统提供的一种服务,它允许运行在同一台计算机上的不同进程之间进行数据交换。
进程通信的主要作用如下:
- 信息共享:多个进程可以通过进程通信来共享信息。例如,一个进程可以生成一些数据,然后通过进程通信将这些数据发送给另一个进程进行处理。
- 计算加速:通过将一个大任务分解成多个小任务,并将这些小任务分配给多个进程进行处理,可以加速计算过程。这种方法通常被称为并行计算。
- 模块化:通过进程通信,可以将一个大型软件系统分解成多个独立的模块,每个模块作为一个独立的进程运行。这样可以提高软件的可维护性和可复用性。
进程通信的主要方式包括管道(Pipe)、消息队列(Message Queue)、共享内存(Shared Memory)、信号(Signal)、套接字(Socket,本文暂不讨论)等。每种方式都有其特点和适用场景。
基础通信
管道
管道通信是一种常见的进程通信方式,它是一种半双工通信方式(即数据只能由一方流向另外一方)。固定一方为读端一方为写端。此外,管道通信是基于文件系统的管道文件(匿名和命名略有不同)操作,所以使用文件描述符进行读写。
匿名管道
匿名管道具有以下特点:
- 创建方式:由pipe进行系统调用,不需要生成一个真实的文件,而是在内核缓冲区划分的一个伪文件。
- 使用场景:匿名管道只能在具有亲缘关系(父子进程与兄弟进程)的进程之间使用,注意:孙子进程、父进程结束等的情况下是不能通信的。
- 生命周期:生命周期与进程一致,当通信双方进程结束后,管道随之释放。
pipe函数
函数原型:
#include <unistd.h>
int pipe(int pipefd[2]);
函数参数:
pipefd:这是一个包含两个整数的数组。pipefd[0]用于读取数据,pipefd[1]用于写入数据。
函数返回值:
- 成功时返回0,并将一对打开的文件描述符填入
pipefd数组。 - 失败时返回-1,并设置
errno以指示错误类型。
示例:
#include <unistd.h>
#include <sys/types.h>
#include <stdio.h>
#include <string.h>
int main(){
int pipefile[2];
pipe(pipefile);
pid_t pid = fork();
if(pid == 0){
printf("这里是子进程\n");
sleep(1);
close(pipefile[0]);
char* text = "hello world";
while(1){
write(pipefile[1],text,12);
sleep(1);
}
}else{
printf("这里是父进程\n");
sleep(1);
close(pipefile[1]);
char text[12];
while(1){
read(pipefile[0],text,12);
printf("输出>>> %s\n",text);
memset(text,0,12);
sleep(1);
}
}
return 0;
}
执行结果:
这里是父进程
这里是子进程
输出>>> hello world
输出>>> hello world
输出>>> hello world
输出>>> hello world
输出>>> hello world
输出>>> hello world
注意:
匿名管道使用上需要注意的点
- 不要将管道的定义写在进程创建之后,否则会创建两个管道。
- 注意写端和读端的方向,关闭不需要的端口。
- 如果想实现双向通信则需要两个管道,尽管可以切换端口进行不同时的双向通信,但是由于进程执行不同步,在一个管道上进行,则可能导致数据混乱或死锁,所以通常不推荐在一个管道上进行读写段切换实现双向通信。
命名管道(FIFO)
匿名管道具有以下特点:
- 创建方式:使用mkfifo等创建,在文件系统中生成一个真正的特殊文件
- 使用场景:可以在任意进程之间进行通信
- 生命周期:即便进程结束,管道依旧存在,直到被显式删除
命名管道是基于操作系统的文件系统而生成的管道文件进行的通信,本质上就是一个进程将消息写入一个文件,然后另外一个进程读取这个文件,从而到达进程之间的通信。故而通信可以在任意进程之间进行,但是代价就是效率相对较低。
所以,对于命名管道的操作,就是对文件的操作。
mkfifo函数
函数原型:
#include <sys/types.h>
#include <sys/stat.h>
int mkfifo(const char *pathname, mode_t mode);
函数参数:
pathname:有名管道的文件路径。mode:文件权限。
函数返回值
- 成功时返回0。
- 失败时返回-1,并设置
errno以指示错误类型。
示例:
#include <sys/types.h>
#include <sys/stat.h>
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
int main(){
if(mkfifo("./fifofile",0666)){
printf("管道文件创建失败\n");
return 0;
}
int fd = open("fifofile",O_RDWR);
pid_t pid = fork();
if(pid==0){
char* text="abc";
write(fd,text,4);
printf("子进程写入完毕\n");
close(fd);
return 0;
}else {
sleep(1);
char text[4];
read(fd,text,4);
printf("父进程读取:%s\n",text);
close(fd);
wait(NULL); // 等待子进程结束
return 0;
}
return 0;
}
执行结果
子进程写入完毕
父进程读取:abc
如果管道读取时数据为空,那么程序会阻塞。
信号
kill函数
这个函数的作用是在指定的进程或者进程组发送一个信号
函数原型
#include <sys/types.h>
#include <signal.h>
int kill(pid_t pid, int sig);
函数参数
- pid:
pid是进程ID,不过有不同的模式- 如果
pid> 0,那么信号将被发送到pid指定的进程。 - 如果
pid== 0,那么信号将被发送到与发送进程在同一进程组的所有进程。 - 如果
pid== -1,那么信号将被发送到发送进程有权限发送信号的所有进程,除了init进程(进程号为1)和调用kill的进程自身。也就是谁可以被我发送信号,那我就发送给它 - 如果
pid< -1,那么信号将被发送到进程组ID等于pid绝对值的所有进程。
- 如果
- sig:信号,以下是常用的信号:
- SIGHUP(1):这个信号通常表示终端已经断开连接。许多守护进程会在接收到这个信号后重新读取配置文件。
- SIGINT(2):当您在终端中按下Ctrl+C时,前台进程会收到这个信号。通常这会结束进程,但进程可以选择忽略这个信号。
- SIGQUIT(3):这个信号类似于SIGINT,但它还会导致进程在结束时生成一个核心转储文件,以便后续进行调试。
- SIGILL(4):当进程尝试执行一个非法指令时,会收到这个信号。这通常是由于程序错误或数据损坏引起的。
- SIGFPE(8):当发生数学错误或浮点运算错误时,如除以零或溢出,进程会收到这个信号。
- SIGKILL(9):这个信号会立即结束进程,进程无法捕获这个信号进行清理工作。这个信号通常只在
SIGTERM无效时使用。 - SIGALRM(14):这个信号通常由
alarm函数生成,用于在指定的时间后通知进程。 - SIGSTOP(17,19,23):这个信号会暂停进程的执行。进程无法忽略这个信号。
- SIGTSTP(18,20,24):当您在终端中按下Ctrl+Z时,前台进程会收到这个信号。这会使进程停止执行,但进程可以选择忽略这个信号。
- SIGCHLD(17,18,20):当一个子进程停止或结束时,其父进程会收到这个信号。
- SIGABRT(6):这个信号通常由
abort函数生成,用于表示一个严重的程序错误。
函数返回值
- 函数成功时返回0
- 失败时返回-1,并设置
errno以指示错误原因
raise函数
这个函数的作用是给当前进程发送一个信号。
函数原型
#include <signal.h>
int raise(int sig);
函数参数
- sig:信号,与kill函数的参数完全一致
函数返回值
raise函数在成功时返回0- 在失败时返回非零值
signal函数
signal函数是一个用于设置信号处理函数的库函数
函数原型
#include <signal.h>
void (*signal(int sig, void (*func)(int)))(int);
或者
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);
这里讲解一下第一种形式,这是一个返回值为函数指针的函数。
先拆在外壳:void (* )(int) 这里可以看到这个返回值的函数指针的真正样貌。然后看一看内部signal(int sig, void (*func)(int)) 这个很明显,它是一个函数明和形参的定义,接受两个参数,一个是int ,另一个是void (*)(int)指针。第二种形式是第一种形式的另一种简单的写法,为了便于理解而重定义了函数指针,所以第二种形式要清晰得多。
函数参数
- sig/signum :这是一个整数,表示特定的信号编号。例如,SIGINT 表示中断信号,通常由用户按下 Ctrl+C 触发。
- func/handler:这个参数有两个可选参数和自定义处理函数:
SIG_IGN:忽略该信号。SIG_DFL:恢复该信号的默认行为。- 自定义函数:当信号发生时,调用这个函数。
函数返回值
signal函数的返回值是一个函数指针,类型为sighandler_t。这个返回值是之前为该信号设置的处理函数。如果调用signal函数之前没有为该信号设置过处理函数,那么signal函数的返回值将是SIG_DFL,表示该信号的默认行为。signal函数在执行过程中出现错误,那么它会返回SIG_ERR。
sigaction函数
这个函数可以重定义一个信号在当前进程中的部分信号,需要注意的是,并非所有的信号都可以被捕获和处理。有些信号,比如 SIGKILL 和 SIGSTOP,是不能被捕获、阻塞或忽略的。
函数原型
#include <signal.h>
int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
函数参数
int signum:这是一个整数,表示特定的信号编号。例如,SIGINT表示中断信号,通常由用户按下 Ctrl+C 触发。const struct sigaction *act:这是一个指向sigaction结构的指针,该结构定义了新的信号处理方式。这个结构包含了处理函数以及其他一些选项和标志。struct sigaction *oldact:这是一个指向sigaction结构的指针,sigaction函数会将旧的信号处理方式存储在这里。如果你不关心旧的处理方式,你可以将这个参数设置为NULL。
struct sigaction结构体定义如下:
struct sigaction {
void (*sa_handler)(int);//sa_handler 是一个指向信号处理函数的指针,和 signal 函数中的处理函数类似
void (*sa_sigaction)(int, siginfo_t *, void *);//sa_sigaction 是另一个信号处理函数,它可以接收更多的信息。
sigset_t sa_mask;//sa_mask 定义了一个信号集,在处理函数运行期间,这些信号将被阻塞。
int sa_flags;//sa_flags 用于修改额外的行为。
void (*sa_restorer)(void);//sigaction 函数的返回值是一个整数。如果函数成功,返回值为 0;
};
函数返回值
sigaction函数的返回值是一个整数。如果函数成功,返回值为 0- 如果函数失败,返回值为 -1
pause函数
当你调用 pause 函数时,你的进程会被挂起,直到接收到一个信号。被 pause 暂停的进程可以通过接收到一个信号来唤醒。这个信号可以是任何信号,只要这个信号能够被进程接收到。
函数原型
#include <unistd.h>
int pause(void);
函数参数
无
函数返回值
pause函数的返回值是一个整数。由于pause函数只在接收到信号后返回,所以它总是返回 -1,并设置errno为EINTR。
alarm函数
这个函数是一个定时函数,当定时器到达设定的时间后,系统会向进程发送一个 SIGALRM 信号。需要注意的是,每个进程在同一时间只能有一个定时器。如果你在一个定时器还没有触发的时候调用了 alarm 函数,那么之前的定时器会被新的定时器替换。
函数原型
#include <unistd.h>
unsigned int alarm(unsigned int seconds);
函数参数
unsigned int seconds:这是你想要设置的定时器的时间,单位是秒。当这个定时器到达设定的时间后,系统会向进程发送一个SIGALRM信号。
函数返回值
alarm 函数的返回值是一个无符号整数。如果之前已经设置了一个定时器,那么 alarm 函数会返回之前的定时器距离触发还剩下的时间。如果之前没有设置定时器,那么 alarm 函数会返回 0。
假设你首先调用 alarm(10); 设置了一个 10 秒的定时器,然后在 4 秒后,你又调用 unsigned int remaining = alarm(5); 设置了一个新的 5 秒的定时器。那么第二次调用 alarm 函数时,它会返回 6,因为在你设置新的定时器时,之前的定时器还有 6 秒就要触发。
System V IPC
前置
ftok函数
ftok函数主要作用是将在unix/Linux系统中生成一个唯一的键值,这个键值常常被用于IPC(进程间通信)机制,如消息队列、信号量或共享内存。
通俗来说,这个函数利用了文件系统的路径唯一性、索引号等,经过算法获取一个唯一值,而这样的值在对于需要确保唯一性的很多机制中(比如共享内存的编号)非常合适,故常用来用于进程通信作为key值获取的函数。
不过要注意的是,你不能绑定一个不存在的目录,路径绑定可以使用相对路径、绝对路径、路径精确到目录,路径精确到文件。为应对绑定的路径相同的情况,设置了第二个参数,根据第二个参数的值,可以用于生成不同的key值。
函数原型
#include <sys/types.h>
#include <sys/ipc.h>
key_t ftok(const char *pathname, int proj_id);
函数参数
- pathname是指定的文件名,这个文件必须是存在的而且可以访问的。
- proj_id是自定义的子序列号,它是一个8位的整数,即范围是0~255。
函数返回值
- 成功的话返回这个得到的key值(key_t其实就是int)
- 失败的话返回-1,并设置错误码errno
示例
#include <sys/types.h>
#include <sys/ipc.h>
#include <stdio.h>
int main(){
key_t key1 = ftok("./",1);
key_t key2 = ftok("./",2);
printf("第一个key值:%d ,第二个key值:%d\n",key1,key2);
return 0;
}
执行结果
第一个key值:16976010 ,第二个key值:33753226
信号量
信号量用于进程间的同步和通信。当一个进程想要访问共享资源时,它会检查信号量的值。如果值大于零,进程就可以访问资源,并将信号量的值减一。如果信号量的值为零,进程就会等待,直到信号量的值变为非零。这种机制可以防止多个进程同时访问同一共享资源,从而避免数据的不一致。
信号量本身并不传递信息,它的作用是保护共享资源,避免发生数据不一致。
semget函数
semget是一个用于创建或访问一个信号量集(semaphore set)的函数。它在Unix和Linux系统中被广泛使用,是System V IPC(Inter-Process Communication,进程间通信)机制的一部分。注意:与线程中的信号量不同,这个是信号量集,而不是单独的信号量。
简单来说,进程的信号量多闸开关,一个开关上有很多按键,可以分别操控不同的按键。而线程的信号量是单闸开关,也就是只有一个按键,不管计数是设置的多少,比如设置100个信号,也不过是在单闸的多端开关。不过,进程信号量也可以为其中的每一个信号量单独设置初始值(–>跳转)。
注意:该函数仅仅是创建一个信号集,至于信号集中信号量的初始值处于为定义状态,应该使用semctl设置信号量的初始值。(–>跳转)
函数原型
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
int semget(key_t key, int nsems, int semflg);
函数参数
key:这是一个键值,用于标识一个特定的信号量集。你可以使用函数ftok生成一个唯一的键值。num_sems:这是你想要创建或访问的信号量的数量,但是要注意的是,并不是信号集的数量,只会生成一个信号集,这个参数是生成这个信号集中信号量的数量。sem_flags:这是一组标志,用于控制信号量的访问权限和行为,分为标志位与权限位,两者之间可以使用|链接。注意,权限位是必须使用的,但是标志位可以填写,也可以不填写。- 标志位:标志位由以下两个可选项。两者之间可以使用
|链接。IPC_CREAT:如果信号量不存在,则创建一个新的信号量集。IPC_EXCL:这个标志必须和IPC_CREAT一起使用,单独使用没有意义。当这两个标志一起使用时,如果信号量集已经存在,semget函数就会失败并返回错误。- 不填写:尝试访问一个已经存在的信号量,如果访问失败,则会返回错误。
- 权限位:对读写执行权限进行设置,与文件相似,分为用户、组、其他用户,比如
0666(所有用户均可以读写)
- 标志位:标志位由以下两个可选项。两者之间可以使用
函数返回值
- 函数的返回值是一个整数,称为信号量
ID(semid),用于后续的信号量操作。 - 如果函数调用失败,它将返回-1。
semop函数
semop是信号量操作函数,确切来说,这个函数的目的是控制临界区的资源,通过这个函数设置信号量内部的计数器,semop又根据这个计数器是否归零来决定是否允许访问临界区资源。简单来说,这个函数起到了控制的作用,就像是一个停车场的门卫,当车来的时候,会检查停车场是否有空位(计数器是否为0),如果有空位则允许进入,如果没有空位则不允许进入。
函数原型
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
int semop(int semid, struct sembuf *sops, size_t nsops);
函数参数
semid:信号量集的标识符,通过semget获取。sops:指向进行操作的结构体数组的地址,如果传入的数组大于1时,函数会按照数组的顺序进行遍历,注意,数组的顺序不需要和实际信号量的一一对应,比如我的第一个数组存储的结构体可以是操控第二个信号的信息,不过要注意,由于函数是根据数组的顺序执行,所以要确保自己填写的结构体位置会被正确执行。(案例–>跳转)nsops:将要进行操作的信号的个数,它设定了结构体数组执行的最大下标,所以要注意的一点是,如果你设置的操作个数大于了实际结构体数组个数,会引发未定义行为,甚至可能导致进程崩溃。。
其中第二个参数sops是一个结构体:
struct sembuf {
unsigned short sem_num; // 信号在信号集中的索引,0代表第一个信号,1代表第二个信号
short sem_op; // 操作类型
short sem_flg; // 操作标志
};
结构体参数解析:
sem_num:欲操作信号在整个信号集的位置,就像是数组的下标性质差不多。但是要注意的一点:不要越界访问,比如定义3个信号的信号集,索引写“3”,实际上访问第四个元素,这样会导致函数返回错误值。sem_op:这个参数是对资源释放与占用的控制,信号量会根据这个值的正负性对资源总数进行加或者减,请注意,信号量的并没有硬性的上限。换句话说:信号量内部有一个计数器,这个计数器的初始值是你设置的,你调用这个函数对这个计数器的值进行控制(比如一次性加多少,一次性减多少),当计数器为0的时候(计数器不为负数),进程阻塞,但是我可以一直释放资源,甚至超过设定的初始值。但是要注意的是,如果一次获取的资源超过了现有资源,这个进程就会多退少补,剩下资源用完了也不够,就会阻塞。sem_op > 0:信号加上sem_op的值,表示进程释放控制的资源。人话:写多少,就会释放多少资源sem_op = 0:理论上的操作是:“当前资源不为0,则进程立刻陷入阻塞,如果资源为0,则进程唤醒”。不过和结构体第三个参数有关系,如果没有设置IPC_NOWAIT,则调用进程进入睡眠状态,直到信号量的值为0;否则进程不回睡眠,直接返回EAGAIN。sem_op < 0:信号加上sem_op的值。若没有设置IPC_NOWAIT,则调用进程阻塞,直到资源可用;否则进程直接返回EAGAIN。人话:写多少,就占多少,资源够就用,不够就阻塞。
sem_flg:用来设置信号量行为,三种形式,0和IPC_NOWAIT可以与SEM_UNDO进行|操作,但是0和IPC_NOWAIT之间不可以。0:这是默认值,表示阻塞调用。如果信号量操作不能立即完成(例如,尝试获取的资源不足),那么semop函数会阻塞,直到操作可以完成。IPC_NOWAIT:这个标志表示非阻塞调用。如果信号量操作不能立即完成,那么semop函数不会阻塞,而是立即返回一个错误。SEM_UNDO:这个标志使操作系统跟踪当前进程对这个信号量的修改情况。如果这个进程在没有释放该信号量的情况下终止,操作系统将自动释放该进程持有的信号量。这可以防止因进程异常终止而导致的资源泄露。讲人话就是:只要你的semop函数的使用了带有这个参数的结构体,那么不管你是增加过信号量,还是占用过信号量,在进程被终止(可以主动结束也也可以被动结束)后,它都会恢复到这个被semop修改之前的样子,要注意的是,它只会恢复当前semop自己管辖的结构体进行过的操作,也就是说,如果有多个semop,但是只有一个semop使用了SEM_UNDO,只会恢复这个使用了SEM_UNDO的信号量修改(案例–>跳转)。
函数返回值
- 调用成功返回0。
- 失败返回-1。
在存在多个信号量的情况下结构体数组案例
struct sembuf sops[2];
// 设置第一个操作。这将使信号量集中的第一个信号量的值增加1
sops[0].sem_num = 0; // 第一个信号量的索引
sops[0].sem_op = 1; // 增加操作
sops[0].sem_flg = 0; // 操作标志
// 设置第二个操作。这将使信号量集中的第二个信号量的值减少1
sops[1].sem_num = 1; // 第二个信号量的索引
sops[1].sem_op = -1; // 减少操作
sops[1].sem_flg = 0; // 操作标志
// 使用semop函数执行这两个操作
if (semop(semid, sops, 2) == -1) {
// 错误处理
}
SEM_UNDO的案例
//第一个信号量操作
a.sem_num = 0, //指定第一个信号量
a.sem_op = +5, //操作为增加一个信号量的值
a.sem_flg = 0 ;
semop(semid,&a,1); //执行信号量操作
//第二个信号量操作
a.sem_num = 0; //指定第一个信号量
a.sem_op = +2; //操作为增加一个信号量的值
a.sem_flg = 0 | SEM_UNDO;
//第三个信号量操作
semop(semid,&a,1); //执行信号量操作
a.sem_num = 0, //指定第一个信号量
a.sem_op = -3, //操作为增加一个信号量的值
a.sem_flg = 0 ;
semop(semid,&a,1); //执行信号量操作
假设初始信号量是5,那么现在这个进程调用对信号量进行了如上的操作。当进程结束之后,信号量会变成7。因为第二个操作使用了SEM_UNDO,进程结束后,这个操作会被恢复,也就是相当于没有进行过第二个信号量操作。当然,要注意的是:如果进程没结束,那么就不会恢复。没结束的情况下,返回结果就是9.
semctl函数
这个函数也是信号量的操作函数,不过不同的是,它比semop的使用层次更深,确切来说这个叫做信号量控制,用于操作信号量本身,而不是管理基于信号量的值。换句话说,两者的作用对象不一样,semop是为了保护一个资源,然后对信号量进行消息传递,阻塞或者允许其他进程访问资源,而semctl的作用对象是信号量自己,它可以操作信号量自己的特性,设置信号量的特性以便更好的对资源进行控制。
函数原型
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
int semctl(int semid, int semnum, int cmd, ...);
函数参数
semid:信号集的标识符,即是信号表的索引,semget获得。semnum:信号集的索引,用来存取信号集内的某个信号。cmd:需要执行的命令。常用的命令如下:IPC_STAT:获取信号量集的状态信息。(需要在可变参数位填写参数。这个命令需要一个semid_ds结构的指针,用于存储信号量集的状态信息。)IPC_SET:设置信号量集的状态信息。(需要在可变参数位填写参数。这个命令需要一个semid_ds结构的指针,该结构包含了你想要设置的信号量集的新状态信息。)IPC_RMID:删除信号量集,当被设置为这个命令的时候,第二个参数实际上会被忽略,不管指定什么标号的信号,函数都会删除整个信号集,而不是一个单独的信号量。(这个命令不需要任何额外的参数,它会立即删除信号量集。)GETVAL:获取信号量的值。(这个命令会返回信号量的值,不需要任何额外的参数。)SETVAL:设置信号量的值。(需要在可变参数位填写参数,这个命令需要一个整数值,作为新的信号量值。)GETALL:获取所有信号量的值。(需要在可变参数位填写参数,这个命令需要一个无符号短整型数组的指针,用于存储所有信号量的值。)SETALL:设置所有信号量的值。(需要在可变参数位填写参数,这个命令需要一个无符号短整型数组的指针,该数组包含了你想要设置的所有新的信号量值。)
这个函数第四位是一个可变参数,具体填写什么,取决于cmd参数的值。这个参数通常是一个semun联合体,定义如下:
union semun {
int val; /* 用于SETVAL */
struct semid_ds *buf; /* 用于IPC_STAT和IPC_SET */
unsigned short *array; /* 用于GETALL和SETALL */
struct seminfo *__buf; /* 用于IPC_INFO */
};
函数返回值
- 成功执行时,根据不同的命令返回不同的非负值。
- 失败返回-1
综合示例
程序1:
#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
#include <unistd.h>
int main(){
//生成一个唯一键值
key_t key = ftok("/",1);
//创建一个信号量
int semid = semget(key,1,0666 | IPC_CREAT | IPC_EXCL );
if(semid == -1){
perror("出错:");
return 0;
}
//定义规范的共用体
union sumun{
int val;
struct semid_ds *buf;
unsigned short *array;
struct seminfo *__buf;
};
//设置信号量的初始值
union sumun val = {.val = 1};
if(semctl(semid,0,SETVAL,val) == -1){//注意,有些情况下可能看到类似semctl(semid,0,SETVAL,1)的情况,并不建议这种写法,因为这并不是标准的写法,可能存在未知的错误。
//如果设置出错
semctl(semid,0,IPC_RMID);
perror("出错:");
return 0;
}
for(int i = 0;i<3;i++){
struct sembuf a = {
.sem_num = 0, //指定第一个信号量
.sem_op = -1, //操作为减少一个信号量的值
.sem_flg = 0 | SEM_UNDO //该信号量操作类型——阻塞型,进程结束自动消除该进程所做的所有更改
};
semop(semid,&a,1); //执行信号量操作
printf("程序1的响应\n");
}
//等待程序2退出
sleep(5);
printf("程序1退出\n");
//删除信号集
semctl(semid,0,IPC_RMID);
return 0;
}
程序2:
#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
#include <unistd.h>
int main(){
//生成一个键值
key_t key = ftok("/",1);
//拖延时间,等待程序1创建信号集
sleep(1);
//获取一个信号量id
int semid = semget(key,1,0666 | IPC_CREAT);
if(semid == -1){
perror("出错:");
return 0;
}
for(int i = 0;i<3;i++){
printf("任意键盘唤醒程序1>>>");
getchar();
struct sembuf a = {
.sem_num = 0, //指定第一个信号量
.sem_op = +1, //操作为增加一个信号量的值
.sem_flg = 0 | SEM_UNDO //该信号量操作类型——阻塞型,进程结束自动消除该进程所做的所有更改
};
semop(semid,&a,1); //执行信号量操作
printf("程序2的响应\n");
}
printf("程序1退出\n");
return 0;
}
执行结果:
//程序1 //程序2
程序1的响应 任意键盘唤醒程序1>>>
程序1的响应 程序2的响应
程序1的响应 任意键盘唤醒程序1>>>
程序1退出 程序2的响应
任意键盘唤醒程序1>>>
程序2的响应
程序1退出
共享内存
在现代操作系统中,我们通常使用虚拟内存技术,其中逻辑内存通过内存管理单元(MMU)映射到物理地址。进程间通信(IPC)可以通过共享内存来实现,这是一种允许多个进程访问同一块物理内存的方法。
在这种情况下,一个进程会创建一个共享内存段,该段的物理内存地址会被映射到所有共享该内存的进程的虚拟地址空间。不过要注意,不同进程虚拟内存并不一定是相同的,事实上,大概率是不相同的。
还有要注意一点是,通常情况下推荐共享内存设置为4KB的倍数,内存是以页的形式进行管理的,每页通常为4KB。当我们申请内存时,如果申请的内存大小不是4KB的倍数,操作系统会分配最接近且大于申请内存的4KB倍数的内存。例如,如果我们申请1KB的内存,操作系统实际上会分配4KB的内存。但是你只能使用1KB,剩余的空间就出现了浪费。
这种方法的优点是,由于数据不需要在进程之间复制,因此效率很高。然而,这也带来了并发访问的问题,为了避免数据的不一致,进程需要使用某种同步机制(如信号量)来协调对共享内存的访问。
shmget函数
shmget函数是一个系统调用函数,它的主要作用是向操作系统申请创建一个共享内存。
此函数返回一个与共享内存绑定的标识码,不过要注意的是,这个由于此函数通常与ftok函数在一起使用,这里存在一个误区:ftok函数并不是返回一个共享内存标志码,它是返回一个具有唯一性的数字,这个数字可以作为参与共享内存标志码生成的种子,shmget根据这个种子生成一个不会发生键值冲突的共享内存标志码。
函数原型
#include <sys/ipc.h>
#include <sys/shm.h>
int shmget(key_t key, size_t size, int shmflg);
函数参数
- key:参数可以用来指定共享内存段的ID。在通常情况,为了避免共享内存的键值冲突,会使用ftok函数(–>跳转)获取一个唯一的key,并不建议自己设置一个值,虽然可行,但是在项目代码量大了之后,难免出现键值冲突问题。
- size:参数用来指定共享内存段的大小。
- shmflg:参数用来指定权限、创建和打开内存段的方式。换句话说,这个参数分为两部分,分别是权限位和标志位。它们之间使用“
|”链接。- 权限位:这部分定义了不同用户对共享内存的访问权限。它通常由三个八进制数字组成,每个数字对应一种用户类型的权限(所有者、组和其他)。每个数字由三位二进制数表示,分别代表读、写和执行权限。例如,权限位
0600表示所有者有读写权限,而组和其他用户没有任何权限。 - 标志位:标志位预设了几种
shmget函数的操作方式。IPC_CREAT:如果共享内存不存在,则创建新的共享内存。否则,打开已存在的共享内存。这个位单独使用往往出现在已经存在了一个共享内存的情况下,其他进程想与之绑定的时候。IPC_EXCL:与IPC_CREAT一起使用(IPC_CREAT | IPC_EXCL即如果共享内存已经存在,那么shmget函数将返回错误。如果共享内存不存在,那么它将创建一个新的共享内存),如果共享内存已存在,则返回错误。SHM_HUGETLB:创建大页共享内存。SHM_NORESERVE:不预留交换空间。
- 权限位:这部分定义了不同用户对共享内存的访问权限。它通常由三个八进制数字组成,每个数字对应一种用户类型的权限(所有者、组和其他)。每个数字由三位二进制数表示,分别代表读、写和执行权限。例如,权限位
函数返回值
- 如果共享内存创建成功,shmget将返回一个非负整数,即该段共享内存的标识码。
- 如果调用失败,则返回“-1”。
shmat函数
shmat是一个在Unix和Linux系统中用于将共享内存连接到当前进程的地址空间的函数。这个函数的主要作用是启动对已创建的共享内存的访问。不过需要注意的是,如果进程结束,那么被链接的共享空间会自动脱离。
函数原型
#include <sys/types.h>
#include <sys/shm.h>
void *shmat(int shmid, const void *shmaddr, int shmflg);
函数参数
- shmid参数是由shmget函数返回的共享内存标识符。
- shmaddr参数是指定共享内存连接到当前进程中的地址位置。换句话说,你希望自己控制这个共享内存的起始位置,那么你可以将这个位置的地址传入这个参数,以实现对内存更加精细的控制。不过要注意的是:
- 参数性质是建议:也就是说,操作系统不一定会听从你的建议,因为往往还有其它的方面会影响,比如内存对齐。内存对齐可以提高效率,那么操作系统最终给出的地址会与你的期望地址偏移出现偏移(此情况是第三个参数允许偏移,如果没有设置
SHM_RND则会直接返回错误值)。如果你的地址接下来的空间不够,那么会返回错误值。 - 一定要有参数接收函数返回值:因为你的建议操作系统不一定听从,所以就需要一个参数根据函数返回结果查看真正的值。
- 置NULL选项:如果你不在意分配的地址,那么此参数置NULL会完全听从操作系统的分配。
- 参数性质是建议:也就是说,操作系统不一定会听从你的建议,因为往往还有其它的方面会影响,比如内存对齐。内存对齐可以提高效率,那么操作系统最终给出的地址会与你的期望地址偏移出现偏移(此情况是第三个参数允许偏移,如果没有设置
- shmflg参数是一组标志位。
SHM_RDONLY:以只读方式附加共享内存。如果没有设置这个标志,那么共享内存将以读写方式附加。SHM_RND:将shmaddr参数舍入到一个合适的边界。这个标志通常与shmaddr参数一起使用。SHM_REMAP:允许共享内存附加到已存在的内存段上。这个标志在Linux 2.4及以后的版本中可用。- 0:如果不需要特殊的附加方式,那么
shmflg可以设置为0。
函数返回值
- 如果调用成功,shmat函数会返回一个指向共享内存第一个字节的指针。
- 如果调用失败,则返回
(void*)-1。注意,这个报错并不是返回NULL,这里最初可以追随到unix时期,是一个历史遗留问题。
shmdt函数
shmdt是一个在Unix和Linux系统中用于将共享内存从当前进程的地址空间中分离的函数。这个函数的主要作用是停止对已创建的共享内存的访问。不过要注意一点是:这个函数仅仅是将自己与共享内存脱离,共享内存的资源依旧存在,它无法被自动删除,需要显示删除,也就说这个函数只是管理自己的一亩三分地,不会管理共享内存的本体
函数原型
#include <sys/types.h>
#include <sys/shm.h>
int shmdt(const void *shmaddr);
函数参数
- shmaddr是指向共享内存的地址
函数返回值
- 调用成功返回0
- 调用失败返回-1
shmctl函数
shmctl是一个在Unix和Linux系统中用于控制共享内存操作的系统调用函数。它可以用于获取共享内存段的状态信息、设置共享内存段的属性,以及删除共享内存段。简单来说,就是对共享内存本体进行操作,这个函数就是一个操作集合。当进行删除操作的时候,这个函数将会回收被申请的共享内存空间,注意,这个函数调用之前必须确保所有的对这个内存访问的进程均已脱离,因为访问被回收的资源可能导致出现数据覆盖、段错误等。如果不是进行删除操作,也必须避免在对共享内存操作的时候,进程不要对其进行访问。
函数原型
#include <sys/ipc.h>
#include <sys/shm.h>
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
函数参数
- shmid参数是由shmget函数返回的共享内存标识符。
- cmd参数是一个整数,表示要执行的操作。常用的命令包括:
- IPC_STAT(获取共享内存段的状态信息)。
- IPC_SET(修改共享内存段的属性)。
- 权限(shm_perm.mode):你可以改变共享内存的权限,例如读、写和执行权限。这可以影响其他进程是否能够访问共享内存。
- 所有者(shm_perm.uid 和 shm_perm.gid):你可以改变共享内存的所有者和所属的组。这也可以影响其他进程是否能够访问共享内存。
- 大小(shm_segsz):理论上,你可以改变共享内存的大小。但是,大多数系统不允许在创建共享内存后改变其大小。如果你需要更大或更小的共享内存,你可能需要删除旧的共享内存并创建一个新的。
- IPC_RMID(删除共享内存段)。这个情况下函数会忽略第三个参数,所以填写NULL即可。
- buf参数是一个指向struct shmid_ds类型结构的指针。该结构包含共享内存段的状态信息。根据cmd参数的不同,buf参数可以用于输入或输出
函数返回值
- 如果函数执行成功,返回值为0。
- 如果出现错误,返回值为-1,并设置errno来指示具体的错误原因
综合示例
#include <sys/types.h>
#include <sys/ipc.h>
#include <stdio.h>
#include <unistd.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <string.h>
int main(){
//开辟一个共享内存
key_t key = ftok("./",1);
if(key == -1){
printf("种子获取失败\n");
return 0;
}
int id = shmget(key,4096,0666 | IPC_CREAT | IPC_EXCL);
if(id == -1){
printf("共享内存绑定失败\n");
return 0;
}
//开辟进程
pid_t pid = fork();
if(pid == 0){
//子进程
//绑定共享内存
void* pmem = shmat(id,NULL,0);
if(pmem == (void*)-1){
printf("子进程链接共享内存失败\n");
return 0;
}
//发送消息
char* text = "爱来自子进程";
strcpy(pmem,text);
printf("子进程发送数据完毕\n");
sleep(5);
//脱离共享内存
shmdt(pmem);
return 0;
}else if(pid > 0){
//父进程
//绑定共享内存
void* pmem = shmat(id,NULL,0);
if(pmem == (void*)-1){
printf("子进程链接共享内存失败\n");
return 0;
}
//接收消息
sleep(5);
char text[50];
strcpy(text,pmem);
printf("收到消息,消息为:%s\n",text);
sleep(5);
//脱离共享内存
shmdt(pmem);
//销毁共享内存
shmctl(id,IPC_RMID,NULL);
return 0;
}
return 0;
}
执行结果
子进程发送数据完毕
收到消息,消息为:爱来自子进程
消息队列
Linux的消息队列是一种在不同进程或者不同系统之间进行通信的机制,它是一种存放消息的容器,发送方向队列中发送消息,接收方从队列中接收消息。
消息队列的实现分为两种,一种为System V的消息队列,一种是Posix消息队列。消息队列可以认为是一个消息链表,某个进程往一个消息队列中写入消息之前,不需要另外某个进程在该队列上等待消息的达到,这一点与管道和FIFO相反。
System V消息队列和POSIX消息队列都是在Linux中用于进程间通信的机制,但它们之间存在一些显著的差异:
- 读取消息的方式:对于
POSIX消息队列,读操作总是返回队列中优先级最高的最早消息。而对于System V消息队列,读操作可以返回任意指定优先级的消息。 - 消息到达的通知机制:当往一个空队列放置一个消息时,
POSIX消息队列允许产生一个信号或启动一个线程。而System V消息队列则不提供类似的机制。 - 生命周期管理:
POSIX消息队列是引用计数的,只有当所有当前使用队列的进程都关闭了之后才会对队列进程标记以便删除。
本文的消息队列采用的是:System V消息队列
msgget函数
这个函数的作用是创建要给消息队列,并且返回一个消息队列标识码。
函数原型
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
int msgget(key_t key, int msgflg);
函数参数
key:这个参数已经是老朋友了,作为生成消息队列的唯一标识码种子,自然使用ftok函数生成具有唯一性的id更为合适,不多赘述,详情请看–>跳转msgflg:这个参数虽然名称出现了变化,但是也是老朋友了,它就是标志位:两种类型之间可以使用|链接- 标志位:两个参数使用
|相连IPC_CREAT:如果消息队列不存在,则创建一个新的消息队列。IPC_EXCL:这个标志必须和IPC_CREAT一起使用,单独使用没有意义。当这两个标志一起使用时,如果信号量集已经存在,函数就会失败并返回错误。
- 权限位:与文件权限一样,读写执行依旧对应的用户对象,依旧可以采用数字表示,比如所有用户均可读写:0666
- 标志位:两个参数使用
函数返回值
- 如果操作成功,msgget将返回一个非负整数,即该消息队列的标识码。
- 如果失败,则返回“-1”
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:指向要发送的消息的结构体。这个结构体可以是任何类型,但是第一个字段必须是long类型,并且只能为正整数,这个字段表示消息的类型。
- msgsz:要发送消息的大小,不包括消息类型占用的4个字节,消息正文的大小。
- msgflg:控制着当前消息队列满或到达系统上限时将要发生的事情。它有两种可选参数
- 0:当消息队列满时,
msgsnd将会阻塞,直到消息能写进消息队列。 IPC_NOWAIT:当消息队列已满的时候,msgsnd函数不等待立即返回,此时错误码为EAGAIN
- 0:当消息队列满时,
这些参数中比较难以理解的是msgp和msgsz:
msgp参数
msgp参数通俗点讲就是一个数据包,而且对这个数据包的定义没有强制要求,只有两个要求——第一个要求:第一个字段必须是正整数的long类型,第二个要求是必须是一个结构体。这两个要求很好理解,前面我们提到了这个long类型实际上是一个字段类型,不过这个类型不是常规意义上的像char double int这样的数据类型,而是消息类型。就像贺卡一样,必须要有一个主题,可以假设春节主题是1,端午节主题是2等等。第二个要求也好理解,因为发送的数据有了一个主题了,但不可能给人家发一个主题过去不发内容,如果要加上内容,也必须要使用结构体。但是并没有对这个结构体的构造做出要求,因为消息没有固定的格式,可能是发int,可能发char。下面举一个例子:
//定义一个贺卡消息类型
struct msgbuf {
long mtype; //消息类型
char name[10]; //发件人名
char mtext[100]; //消息内容
};
这个消息类型的存在一种解释是为了优化消息的查找效率,消息队列可以看作是一个链表,如果在单条链表上不断延伸,遍历的效率低下,如果进行了分类可以优化查找效率。操作系统会根据用户的需求类型在指定类型链表上查找,严谨的讲,这里的“链表”并不是实际的数据结构,而是一种概念上的理解。实际上,所有的消息都存储在同一个消息队列中,只是通过消息类型这个字段来区分不同的消息。
msgsz参数
这个参数比较好理解,就是正文的大小,因为你发送的数据包,被void*类型接收,void*会导致结构体本身的信息被屏蔽,而由于结构体没有做出限制,所以也没有办法做一个预先的声明,操作系统仅知道数据包存在一个long,那剩下的数据长度则需要用户对其做出通知。
函数返回值
- 如果操作成功,msgsnd将返回0
- 如果失败,则返回-1
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:要接收的消息的大小,不包括消息类型占用的4个字节。
- msgtyp:指定要接收的消息类型。
- 如果
msgtyp为0,那么msgrcv会接收消息队列中的第一个消息,无论这个消息的类型是什么。 - 如果
msgtyp大于0,那么msgrcv会接收消息队列中第一个类型为msgtyp的消息。 - 如果
msgtyp小于0,那么msgrcv会接收消息队列中类型值不大于msgtyp的绝对值且类型值又最小的消息。举一个例子:假设当前的消息有3、5、7、8。如果我的接收类型填写-9,那么操作系统就会找1-9中最小的类型,也就是3。那么最终的消息类型就选择3
- 如果
- msgflg:控制着当前消息队列满或到达系统上限时将要发生的事情。它有四种可选参数
0:这是默认的模式。如果没有可用的消息,msgrcv函数会阻塞,直到有消息可用。IPC_NOWAIT:如果没有消息,msgrcv函数不会阻塞,而是立即返回ENOMSG错误。MSG_NOERROR:如果消息的大小超过msgsz,消息会被截断,而不是返回E2BIG错误。MSG_EXCEPT:当msgtyp大于0且msgflg设置为MSG_EXCEPT,msgrcv会接收类型不等于msgtyp的第一条消息。
函数返回值
- 如果操作成功,msgrcv将返回接收到的消息的长度
- 如果失败,则返回-1
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:将
msqid的消息队列的当前信息复制到buf指向的结构体中。 - IPC_SET:设置消息队列的属性,要设置的属性需先存储在buf中,可设置的属性包括:
- msg_perm.uid。
- msg_perm.gid。
- msg_perm.mode。
- msg_qbytes。
- IPC_RMID:立即删除
msqid标识的消息队列,以及该队列中的所有消息,此时的buf可以设为NULL。
- IPC_STAT:将
- buf:指向一个
msqid_ds结构体的指针,这个结构体中包含了消息队列的信息
这些参数中涉及到了一个叫做msqid_ds 的结构体,该结构体的成员如下:
struct msqid_ds {
struct ipc_perm msg_perm; /* 所有权和权限信息 */
struct msg *msg_first; /* 队列中的第一条消息,未在用户空间程序中使用 */
struct msg *msg_last; /* 队列中的最后一条消息,未在用户空间程序中使用 */
__kernel_time_t msg_stime; /* 最后一次调用msgsnd函数的时间 */
__kernel_time_t msg_rtime; /* 最后一次调用msgrcv函数的时间 */
__kernel_time_t msg_ctime; /* 最后一次改变消息队列状态的时间 */
unsigned long msg_lcbytes; /* 在32位系统中被重用,但在用户空间程序中并未使用 */
unsigned long msg_lqbytes; /* 在32位系统中被重用,但在用户空间程序中并未使用 */
unsigned short msg_cbytes; /* 当前消息队列中的字节数 */
unsigned short msg_qnum; /* 当前消息队列中的消息数 */
unsigned short msg_qbytes; /* 消息队列允许的最大字节数 */
__kernel_ipc_pid_t msg_lspid; /* 最后一次调用msgsnd函数的进程ID */
__kernel_ipc_pid_t msg_lrpid; /* 最后一次接收消息的进程ID */
};
函数返回值
- 如果操作成功,msgctl将返回0
- 如果失败,则返回-1
综合示例
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/ipc.h>
#include <sys/msg.h>
int main(){
pid_t pid = fork();
if(pid == 0){
//等待父进程创建消息队列
sleep(1);
//创建键值
key_t key = ftok("/",1);
//获取队列id
int msgid = msgget(key,IPC_CREAT | 0666);
if(msgid == -1){
return 0;
}
//创建消息结构体
struct msgdata{
long mtype;
char text[20];
};
//实例化消息
struct msgdata mymsg;
//等待父进程消息发送
sleep(3);
//接收消息,选取10以内最小的类型的消息
msgrcv(msgid,&mymsg,20,-10,0);
printf("消息接收:[%s]\n",mymsg.text);
}else if(pid > 0){
//创建键值
key_t key = ftok("/",1);
//获取队列id
int msgid = msgget(key,IPC_CREAT | 0666);
if(msgid == -1){
return 0;
}
//创建消息结构体
struct msgdata{
long mtype;
char text[20];
};
//实例化消息
struct msgdata mymsg;
mymsg.mtype = 1;
strcpy(mymsg.text,"hello world");
//消息发送
msgsnd(msgid,&mymsg,20,0);
printf("消息发送完成\n");
//等待子进程处理消息
sleep(5);
//删除队列
msgctl(msgid,IPC_RMID,NULL);
}
return 0;
}
运行结果
消息发送完成
消息接收:[hello world]
