进程间通信(Inter-Process Communication,IPC)是指不同的进程之间进行数据交换和通信的机制。在操作系统中,进程是独立运行的程序实例,通过IPC机制可以实现进程之间的数据共享、同步和通信,以实现协作和协调。
进程间通信目的
- 数据传输:一个进程需要将它的数据发送给另一个进程
- 资源共享:多个进程之间共享同样的资源。
- 通知事件:一个进程需要向另一个或一组进程发送消息,通知它(它们)发生了某种事件(如进程终止 时要通知父进程)。
- 进程控制:有些进程希望完全控制另一个进程的执行(如Debug进程),此时控制进程希望能够拦截另 一个进程的所有陷入和异常,并能够及时知道它的状态改变
进程间通信分类
- 管道
- 匿名管道pipe :管道是一种半双工的通信机制,可以在具有亲缘关系的进程之间进行通信。它有两种类型:匿名管道(使用
pipe
函数)和命名管道(使用mkfifo
函数),用于在进程之间传递字节流数据。 - 命名管道:FIFO(命名管道):FIFO是一种有名字的管道,可以在不具有亲缘关系的进程之间进行通信。通过使用
mkfifo
函数创建一个命名管道文件,多个进程可以通过打开该文件并进行读写操作来实现通信。
- System V IPC
- System V 消息队列
- System V 共享内存
- System V 信号量
- POSIX IPC
- 消息队列(Message Queue):消息队列是一种通过在进程之间传递消息实现通信的机制。进程可以将消息放入队列中,其他进程可以从队列中读取消息。消息队列提供了一种异步通信方式。
- 共享内存(Message Queue):共享内存是一种高效的进程间通信方式。它允许多个进程共享同一块物理内存区域,进程可以直接读写共享内存,避免了数据的复制和传输开销。
- 信号量(Semaphore):信号量是一种用于进程间同步和互斥的机制。它可以用于控制对临界资源的访问,并确保不同进程之间的顺序执行。
- 互斥量
- 条件变量
- 读写锁
匿名管道
匿名管道(Anonymous Pipe)是一种无名的、半双工的进程间通信机制,用于在具有亲缘关系的父子进程之间传递字节流数据。它是Unix和类Unix系统中常见的IPC方式之一。
匿名管道特点:
-
半双工通信:匿名管道是单向的,一般用于父进程向子进程传递数据。它只支持单向的数据传输,需要在父进程和子进程之间建立两个管道,分别用于父进程向子进程传递数据和子进程向父进程传递数据。
-
无名管道:匿名管道是无名的,没有文件系统中的路径和文件名。它是在创建进程时自动创建的,不需要显式地在文件系统中创建管道文件。
-
文件描述符:匿名管道通过文件描述符进行引用。在创建管道时,使用
pipe()
系统调用会返回两个文件描述符,一个用于读取数据,另一个用于写入数据。 -
内存缓冲区:匿名管道的数据传输是在内存中进行的,通过内核内存缓冲区进行数据的临时存储。数据从写入端写入缓冲区,然后从读取端读取。
-
阻塞式通信:匿名管道是阻塞式的通信机制。如果没有数据可读,则读取操作会阻塞进程,直到有数据到达。类似地,如果管道已满,则写入操作会阻塞进程,直到有空间可用。
匿名管道是一种简单而有效的进程间通信方式,适用于具有亲缘关系的父子进程之间的通信。它的数据传输是在内存中进行的,速度较快。但由于是单向的,只能支持父进程向子进程或子进程向父进程的数据传输。
我们先来见识一下管道
who查看当前服务器有哪些用户在使用,wc统计文本行有多少行,|是管道,管道前后的两个命令是两个进程,前面的进程通过管道将数据给后面的进程
sleep 30000后面的&是让他们后台运行
可以看到三个sleep确实是三个进程,父进程相同都是bash
匿名管道的原理
我通过who | wc -1
来讲一下管道的原理
因为Linux下一切皆文件,管道也一样是文件,创建一个管道,等同于打开一个文件,who进程以写的方式打开管道并将自己的标准输出重定向到管道,然后wc -1进程以读的方式打开管道并将自己的标准输入重定向到管道,这样就可以让这两个进程进行通信
这个管道是匿名管道,管道文件的缓冲区是OS提供的内存级的文件,是一个临时文件,本来一个文件是要在磁盘创建然后在打开的,但是这个文件在磁盘中不存在,是OS直接在内存中创建给管道用的,r是以读的方式打开管道文件,w以写的方式打开管道文件
fork后创建子进程,拷贝task_struct,文件描述符表,但是被打开的文件不会被拷贝,文件描述符表里面放的都是这些文件的地址,所以父子进程共同使用这些被打开的文件,这就相当于浅拷贝,这下就满足了通信的一个前提,看到同一份资源
接下来确定数据的流向,比如父进程的信息通过管道传给子进程,就要关闭父进程读( r)打开的管道文件,关闭子进程写(w)打开的管道文件,让父进程只能写进管道,子进程只能读取管道
这样的管道只能单向通信,因为管道的缓冲区只有一个,想要双向,就要再打开一个管道
为什么父进程要同时以写(w)和读( r)的方式打开管道文件? -> 因为子进程要继承读和写这两种打开的管道文件,如果父进程只以读的方式打开文件,那么子进程就只能跟着父进程一起以读的方式打开文件,这样就不可以让父子进程进行通信了
使用匿名管道通信
首先我们要认识一个函数int pipe(pipefd[2])
这个函数是OS提供的用来打开管道文件的函数
#include <unistd.h>
int pipe(int pipefd[2]);
参数中pipe[2]
是一个输出型参数,在pipe函数内部会以读和写的方式分别打开管道文件,然后将读的文件描述符放在数组的第一个元素,写的文件描述符放在数组第二个元素
创建管道代码
int pipefd[2] = {0};
//1.创建管道
if(n < 0)
{
cout << "pipe errno," << errno << strerror(errno) << endl;
return -1;
}
创建子进程和子进程代码
//创建子进程
pid_t id = fork();
assert(id != -1);
if(id == 0)
{
//子进程
//让子进程写入,就要关闭子进程的读端,0下标对应的文件描述符就是读端
close(pipefd[0]);
//通信
const string str = "我是子进程";
int cnt = 1;
char buffer[1024];
while(1)
{
snprintf(buffer, sizeof(buffer), "%s,计数器:%d,我的PID:%d\n", str.c_str(), cnt++, getpid());
write(pipefd[1], buffer, strlen(buffer));
sleep(1);
}
close(pipefd[1]);
exit(0);
}
父进程代码
//父进程
//父进程
//让父进程读取
close(pipefd[1]);
//通信
char buffer[1024];
while(1)
{
int n = read(pipefd[0], buffer, sizeof(buffer) - 1);
if(n > 0)
{
buffer[n] = '\0';
cout << "我是父进程,子进程说:" << buffer << endl;
}
}
close(pipefd[0]);
总代码
#include <iostream>
#include <cerrno>
#include <unistd.h>
#include <cstring>
#include <sys/types.h>
#include <cassert>
#include <string>
using namespace std;
int main()
{
int pipefd[2] = {0};
//1.创建管道
int n = pipe(pipefd);
if (n < 0)
{
cout << "pipe errno," << errno << ";" << strerror(errno) << endl;
return -1;
}
//创建子进程
pid_t id = fork();
assert(id != -1);
if(id == 0)
{
//子进程
//让子进程写入,就要关闭子进程的读端
close(pipefd[0]);
//通信
const string str = "我是子进程";
int cnt = 1;
char buffer[1024];
while(1)
{
snprintf(buffer, sizeof(buffer), "%s,计数器:%d,我的PID:%d\n", str.c_str(), cnt++, getpid());
write(pipefd[1], buffer, strlen(buffer));
sleep(1);
}
close(pipefd[1]);
exit(0);
}
//父进程
//让父进程读取
close(pipefd[1]);
//通信
char buffer[1024];
while(1)
{
int n = read(pipefd[0], buffer, sizeof(buffer) - 1);
if(n > 0)
{
buffer[n] = '\0';
cout << "我是父进程,子进程说:" << buffer << endl;
}
}
close(pipefd[0]);
return 0;
}
根据代码,子进程将信息写入管道,父进程从管道读取并打印,只有父进程会打印,看到结果,可以知道通信成功了
匿名管道的特点
- 五种特点
- 单向通信
- 管道的本质是文件,生命周期和进程是一样的
- 管道通信,可以在具有“血缘”关系的进程间通信,如父子进程,爷孙进程,兄弟进程的等,常在父子进程间通信
- 管道通信中,读可以一次全读下来,可以读一定量子节,写也是,读和写可以随便定,读写的次数的多少没有强相关(子节流)
- 具有一定的协同能力,让读和写按照一定的步骤通信(自带同步机制)
- 四种场景
- 如果读的很快,写的很慢,那么读的进程就要等待写的进程去写入,就是一个进程读的时候管道里没有信息,就要等待另一个进程去写入
- 管道是有大小的,不能无限制写,写满了就不能再写了,要等另一个进程把信息读完才可以写入
- 写的进程写完关闭,读的进程读完之后信息后再读,read会返回0,表示读到了文件结尾
- 写的进程一直写,读的进程读了几次之后不读了,直接关闭,OS会直接发送13信号
SIGPIPE
杀死一直在写的进程,因为OS不会维护无意义,低效率,浪费资源的事,读的进程关闭了,写的进程却仍然在写,又没有进程可以读到这些信息了,所以这样写入是无意义的,会被OS杀死
子进程代码不变,修改一下父进程的代码,让父进程先退出然后等待子进程退出,得到子进程退出收到的13信号
- 原子性:就是保证你写入的数据的完整,比如说我写入hello world,就是保证写完hello world之后再读,而不是写一个hello就读了
当要写入的数据量不大于PIPE_BUF
时,Linux将保证写入的原子性。
当要写入的数据量大于PIPE_BUF
时,Linux将不再保证写入的原子性。
特点:
- 只能用于具有共同祖先的进程(具有亲缘关系的进程)之间进行通信;通常,一个管道由一个进程创建,然后该进程调用fork,此后父、子进程之间就可应用该管道。
- 管道提供流式服务
- 一般而言,进程退出,管道释放,所以管道的生命周期随进程
- 一般而言,内核会对管道操作进行同步与互斥
- 管道是半双工的,数据只能向一个方向流动;需要双方通信时,需要建立起两个管道
半双工:比如说两个说话,就是一个人说一个人听
全双工:就是两个人吵架,不会等着别人说完再说,两个人对着喷
命名管道
命名管道(Named Pipe),也称为FIFO(First-In-First-Out),是一种有名字的管道用于进程间通信。与匿名管道不同,命名管道可以在文件系统中创建,并且可以被多个进程同时访问。
命名管道具有以下特点:
-
有名字的管道:命名管道在文件系统中有一个路径和文件名,可以通过文件系统中的路径来引用。它与普通文件类似,但其数据传输方式是按照先进先出的顺序进行。
-
文件系统中的创建:命名管道使用
mkfifo
命令或mkfifo()
函数在文件系统中创建。创建后,可以像操作普通文件一样对其进行读写操作。 -
多进程访问:命名管道可以被多个进程同时访问,不限于具有亲缘关系的进程。不同进程可以通过打开同一个命名管道文件来进行读写操作。
-
阻塞式通信:命名管道是阻塞式的通信机制。如果没有数据可读,则读取操作会阻塞进程,直到有数据到达。类似地,如果管道已满,则写入操作会阻塞进程,直到有空间可用。
命名管道是一种灵活且功能强大的进程间通信方式,适用于不具有亲缘关系的进程之间的通信。它可以在文件系统中创建,支持多进程访问,并且具有阻塞式的通信特性。
命令行创建命名管道
命令mkfifo (文件名)
即可创建一个命名管道,man
手册中查到
创建的文件类型以p开头,表示管道文件
我们向文件fifo中写入一些东西发现卡在这里了
创建一个会话,发现fifo的内容还是0,而管道文件里明明有内容,这是因为管道文件有种特殊的特性,虽然在磁盘中创建了fifo,但是管道文件是内存级文件,所以并没有写到磁盘中,只是写到了管道文件里
为什么是内存级的文件,因为管道文件就是给进程通信用的,如果把文件内容刷到磁盘中,就会浪费时间,降低通信速度,没有必要
系统调用创建命名管道
系统调用接口也是mkfifo
,在man
手册,3号可以查到
#include <sys/types.h>
#include <sys/stat.h>
int mkfifo(const char *pathname, mode_t mode);
pathname
:要创建的命名管道的路径和文件名。mode
:权限模式,用于指定创建的命名管道的权限。通常使用八进制表示的权限模式,例如0666表示读写权限。
函数返回值为0表示创建命名管道成功,-1表示创建失败。
我们来使用系统调用创建命名管道
makefile文件内容
.PHONY:all
all:server client
server:server.cc
g++ -o $@ $^ -std=c++11
client:client.cc
g++ -o $@ $^ -std=c++11
.PHONY:clean
clean:
rm -f server client
server.cc文件内容
#include <iostream>
#include <cstring>
#include <cerrno>
#include <sys/types.h>
#include <sys/stat.h>
#include <string>
using namespace std;
const string fifoname = "./fifo";
mode_t mode =0666;
int main()
{
//1.创建管道文件
int n = mkfifo(fifoname.c_str(), mode);
if(n != 0)
{
cout << errno << ":" << strerror(errno) << endl;
return -1;
}
return 0;
}
client.cc内容
可以看到成功创建出来了命名管道,但是命名管道文件的权限对不上
因为系统的umask
文件掩码影响了文件权限
可以通过系统调用来在进程中设置umask
掩码,并不会影响到系统默认的umask
掩码,只会改变进程中的umask
掩码
client.cc完整代码
#include <iostream>
#include <cstring>
#include <cerrno>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <cstdio>
#include <cassert>
#include <string>
using namespace std;
const string fifoname = "./fifo";
int main()
{
//1.不用创建管道文件,只需要打开同一个管道文件
int wfd = open(fifoname.c_str(), O_WRONLY);
if(wfd < 0)
{
cout << errno << ":" << strerror(errno) << endl;
return -1;
}
//2.通信
char buffer[1024] = {0};
while(1)
{
cout << "输入你的消息:";
char* msg = fgets(buffer, sizeof(buffer), stdin);
assert(msg);
(void)msg;
//去除msg字符串中最后带着的'\n'
buffer[strlen(buffer) - 1] = 0;
ssize_t n = write(wfd, buffer, sizeof(buffer));
assert(n >= 0);
(void)n;
}
close(wfd);
return 0;
}
server.cc完整代码
#include <iostream>
#include <cstring>
#include <cerrno>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string>
using namespace std;
const string fifoname = "./fifo";
mode_t mode =0666;
int main()
{
//1.创建管道文件
umask(0);
int n = mkfifo(fifoname.c_str(), mode);
if(n != 0)
{
cout << errno << ":" << strerror(errno) << endl;
return -1;
}
//2.服务端打开管道文件
int rfd = open(fifoname.c_str(), O_RDONLY);
if(rfd < 0)
{
cout << errno << ":" << strerror(errno) << endl;
return -2;
}
//3.通信
char buffer[1024] = {0};
while(1)
{
buffer[0] = 0;
ssize_t n = read(rfd, buffer, sizeof(buffer) - 1);
if(n > 0)
{
buffer[n] = 0;
cout << "client say:" << buffer << endl;
}
else if(n == 0)
{
cout << "client exit" << endl;
}
else
{
cout << errno << ":" << strerror(errno) << endl;
break;
}
}
//关闭管道文件
close(rfd);
return 0;
}
system V共享内存
共享内存就是在物理空间上开辟以一个内存块,然后通过页表映射到两个进程的地址空间的共享区中,然后两个进程就可以看到同一份资源,这样两个进程就可以通信了
共享内存是所有通信中最快的,管道通信是两进程通过看到相同的文件通信,需要使用系统接口,还要多次拷贝,而共享内存直接映射在两进程的地址空间中,两进程都可以直接通信,不需要拷贝
管道有同步机制,如果一方没有写,另一方就要等写了才能读,而共享内存没有这样的机制,共享内存的两个进程可以随时访问,就算数据为空或者乱码,也可以读
shmget
创建共享内存
我们先直接来看看共享内存的使用
#include <sys/ipc.h>
#include <sys/shm.h>
int shmget(key_t key, size_t size, int shmflg);
-
key
是一个可以标定唯一性的数,可以随便设置,但是我们一般用key_t ftok(const char *pathname, int proj_id)
根据用户设定的路径和id
,ftok
函数会结合传入的路径和id
,用算法生成一个冲突概率很低的数,并返回给用户 -
size
就是设置申请的共享内存块的大小,可以任意设置大小是以PAGE页(4KB)为单位的,会向上对齐如果设置4097字节,那么操作系统会申请8KB,但是并不代表你能使用8KB,只能使用4097字节,并且ipcs查询后也只显示4097字节
-
shmflg
就是传选项IPC_CREAT
:单独传这个宏,就是创建一个共享内存,如果共享内存不存在,就创建,如果已经存在,就获取已经存在的共享内存并返回IPC_EXEL
:不能单独使用,要和IPC_CREAT
一起使用,如果共享内存不存在,就创建,如果存在就立马出错并返回
-
返回值
创建成功返回共享内存的标识符,创建失败就返回-1,并设置错误码
使用方法:
server.cc的代码
#include <iostream>
#include <string>
#include <cstdio>
#include <cerrno>
#include <cstring>
#include <sys/types.h>
#include <sys/shm.h>
#include <sys/ipc.h>
using namespace std;
const string PATHNAME = ".";
const int PROJID = 0x2345;
const int shmsize = 4096;
key_t getkey()
{
key_t k = ftok(PATHNAME.c_str(), PROJID);
if(k == -1)
{
cout << errno << ":" << strerror(errno) << endl;
exit(1);
}
return k;
}
int createshm(key_t k, int size)
{
//创建一个新的共享内存
int shmid = shmget(k, size, IPC_CREAT | IPC_EXCL);
if(shmid == -1)
{
cout << errno << ":" << strerror(errno) << endl;
exit(2);
}
return shmid;
}
int main()
{
//1.创建key
key_t k = getkey();
printf("server key:0x%x\n", k);
//2.创建共享内存
int shmid = createshm(k, shmsize);
printf("server shmid:%d\n", shmid);
return 0;
}
client.cc的代码
#include <iostream>
#include <string>
#include <cstdio>
#include <cerrno>
#include <cstring>
#include <sys/types.h>
#include <sys/shm.h>
#include <sys/ipc.h>
using namespace std;
const string PATHNAME = ".";
const int PROJID = 0x2345;
const int shmsize = 4096;
key_t getkey()
{
key_t k = ftok(PATHNAME.c_str(), PROJID);
if(k == -1)
{
cout << errno << ":" << strerror(errno) << endl;
exit(1);
}
return k;
}
int getshm(int k, int size)
{
//获取共享内存
int shmid = shmget(k, size, IPC_CREAT);
if(shmid == -1)
{
cout << errno << ":" << strerror(errno) << endl;
exit(2);
}
return shmid;
}
int main()
{
//1.获取key
key_t k = getkey();
printf("client key:0x%x\n", k);
//2.获取共享内存
int shmid = getshm(k, shmsize);
printf("client shmid:%d\n", shmid);
return 0;
}
makefile代码
.PHONY:all
all:server client
client:client.cc
g++ -o $@ $^ -std=c++11
server:server.cc
g++ -o $@ $^ -std=c++11
.PHONY:clean
clean:
rm -f client server
共享内存生命周期随操作系统
-
指令删共享内存
第一次运行如上代码的时候,可以创建成功共享内存,但是删除程序,重新运行的时候,刚刚创建的共享内存并不会被删掉,共享内存的生命周期不随进程,随操作系统,所以之后运行的时候都会报文件已存在这个错误
我们可以证明共享内存被创建了之后不会自动删除,会一直存在
ipcs
(ipc是进程间通信- -Inter-Process Communication)可以查询进程间通信设施的状态,从到下依次为消息队列,共享内存,信号量
ipcs -m
是单独查询共享内存,perms
(permission权限),bytes
共享内存的大小,nattch
共享内存与几个进程有连接,status
共享内存状态修改权限就是要在创建共享内存的参数flag中按位或上权限,要设置umask掩码
指令
ipcrm -m shmid
删除共享内存 -
shmctl删共享内存
int shmctl(int shmid, int cmd, struct shmid_ds *buf)
第一个参数:想要操作的共享内存的id
,第二个参数:操作的选项IPC_STAT
:获取共享内存段的信息,并将其存储在buf
指向的shmid_ds
结构中。
IPC_SET
:设置共享内存段的信息,使用buf
指向的shmid_ds
结构中的值来更新。
IPC_RMID
:删除共享内存段。第三个参数:存放共享内存的属性
系统调用删除共享内存
shmat
,shmdt
,连接和断连接共享内存和进程
连接上了共享内存之后就可以直接对其解引用访问
shmat
:
#include <sys/types.h>
#include <sys/shm.h>
void *shmat(int shmid, const void *shmaddr, int shmflg);
shmid
:共享内存的标识符,通过shmget()
函数获取。shmaddr
:指定共享内存连接的地址,通常设为NULL
,由系统自动分配地址。shmflg
:标志参数,用于指定共享内存的连接方式和权限。
server.cc:
char* attachshm(int shmid)
{
char* start = (char*)shmat(shmid, nullptr, 0);
return start;
}
int main()
{
//1.创建key
key_t k = getkey();
printf("server key:0x%x\n", k);
//2.创建共享内存
int shmid = createshm(k, shmsize);
printf("server shmid:%d\n", shmid);
//3.连接共享内存
char* start = attachshm(shmid);
//4.删除共享内存
delshm(shmid);
return 0;
}
client.cc
char* attachshm(int shmid)
{
char* start = (char*)shmat(shmid, nullptr, 0);
return start;
}
int main()
{
//1.获取key
key_t k = getkey();
printf("client key:0x%x\n", k);
//2.获取共享内存
int shmid = getshm(k, shmsize);
printf("client shmid:%d\n", shmid);
//3.连接共享内存
char* start = attachshm(shmid);
return 0;
}
shmdt
:
#include <sys/types.h>
#include <sys/shm.h>
int shmdt(const void *shmaddr);
信号量(semaphore)
信号量(Semaphore)是一种用于进程间同步和互斥的机制。
信号量的主要作用是控制对共享资源的访问,以避免多个进程同时访问共享资源而导致的数据不一致或冲突。它可以用来实现进程间的互斥(Mutex)和同步(Synchronization)。
信号量的基本操作有两个:P 操作和 V 操作。
- P(Proberen)操作,也称为申请操作或减操作,用于申请资源或减少信号量的值。如果信号量的值大于 0,则将其减 1;如果信号量的值为 0,则阻塞当前进程,直到信号量的值大于 0。
- V(Verhogen)操作,也称为释放操作或增操作,用于释放资源或增加信号量的值。它将信号量的值加 1,并通知等待该信号量的其他进程。
信号量是一种用于进程间同步和互斥的机制,它通过 P 操作和 V 操作来实现资源的申请和释放。信号量可以用来控制对共享资源的访问,避免竞态条件和数据不一致问题。在并发编程中,信号量是一种重要的同步工具,用于实现进程间的互斥和同步。
互斥等4个概念
- 互斥:任何一个时刻,都只允许一个执行流在进行共享资源的访问
- 我们把任何一个时刻,都只运行一个执行流访问的公共资源叫做临界资源
- 临界资源是通过代码访问的,凡是访问临界资源的代码,叫做临界区
- 原子性,只有两种确定状态的属性
信号量是资源的预定机制,信号量本质上是一个计数器
任何一个执行流要向访问临界资源的一个子资源的时候,不能直接访问,要先申请信号量
申请成功后,信号量减少(count-=1),申请信号量成功后,一定可以拿到一个子资源。释放信号量资源,就是增加信号量(count+=1)
信号量必须保证自己的增加和减少是原子性的,申请信号量是P操作,释放信号量是V操作
如果信号量计数器为1,那就是二元信号量,实现的互斥功能,互斥本质将临界资源独立使用
查询信号量ipcs -s
删除信号量量ipcrm -s -semid
semget
获取信号量
semget
函数可用于创建新的信号量集或获取现有的信号量集
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
int semget(key_t key, int nsems, int semflg);
key
:
用于标识一个唯一的信号量集通常使用ftok()函数生成。nsems
:
信号量的个数,操作系统允许一次申请多个信号量,创建的是一个信号量集semflg
:IPC_CREAT
:如果指定的键值对应的信号量集合不存在,则创建一个新的信号量集合。如果信号量集合已存在,则该标志被忽略。IPC_EXCL
:与IPC_CREAT
标志一起使用时,如果指定的键值对应的信号量集合已存在,则返回错误。0666
:指定创建的信号量集合的权限。
semctl
semctl
函数可用于获取信号量的信息、修改信号量的属性和删除信号量集
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
int semctl(int semid, int semnum, int cmd, ...);
semctl()
函数是一个系统调用,用于对信号量集合进行控制操作,如获取或设置信号量集合中的属性、获取或设置信号量的值等。它的函数原型如下:
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
int semctl(int semid, int semnum, int cmd, ...);
semctl()
函数接受四个参数:
semid
:信号量集合的标识符,由semget()
函数返回。semnum
:要操作的信号量的编号,对于单个信号量集合来说,一般为 0。cmd
:指定要执行的操作,可以是以下命令之一:GETVAL
:获取信号量的值。SETVAL
:设置信号量的值。GETPID
:获取最后一次执行semop()
操作的进程 ID。GETNCNT
:获取等待信号量值增加的进程数量。GETZCNT
:获取等待信号量值减少的进程数量。IPC_RMID
:删除信号量集合。
...
:可选参数,根据不同的命令可能需要传递额外的参数。
semop
semop
函数可用于对信号量集执行一系列原子操作。每个操作由sembuf
结构中的sem_num
、sem_op
和sem_flg
字段定义,就是对信号量加或者减(count++或count–)
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
int semop(int semid, struct sembuf *sops, size_t nsops);
semop()
函数是一个系统调用,用于对信号量集合执行操作。它可以用于对一个或多个信号量进行 P(等待)操作或 V(释放)操作,以实现进程间的同步和互斥。semop()
函数的函数原型如下:
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
int semop(int semid, struct sembuf *sops, size_t nsops);
semop()
函数接受三个参数:
semid
:信号量集合的标识符,由semget()
函数返回。sops
:指向一个sembuf
结构体数组的指针,每个结构体表示一个信号量操作。nsops
:待执行的信号量操作的数量。
sops
结构体要自己去创建
sembuf
结构体定义如下:
struct sembuf {
unsigned short sem_num; // 信号量的编号
short sem_op; // 信号量操作的值
short sem_flg; // 操作的标志
};
sembuf
结构体的成员解释如下:
sem_num
:要执行操作的信号量的编号,对于单个信号量集合来说,一般为 0。sem_op
:信号量操作的值,可以是正数、负数或零。- 正数:表示执行 V(释放)操作,将信号量的值增加。
- 负数:表示执行 P(等待)操作,将信号量的值减少。
- 零:表示执行 Z(阻塞)操作,如果信号量的值为零,则阻塞等待,直到信号量的值大于零。
sem_flg
:操作的标志,可以是以下标志的按位或运算:SEM_UNDO
:在进程退出时自动撤销操作,避免出现死锁。IPC_NOWAIT
:非阻塞操作,如果无法进行操作,则立即返回错误。