进程间通信介绍
为了保护进程的独立性,OS是不允许进程间直接进行数据交换的,但我们又有进程间通信的需求,例如:
- 数据传输:一个进程需要将他的数据发送给另一个进程
- 资源共享:多个进程之间需要共享某一个资源
- 通知事件:一个进程需要向另一个或一组进程发送消息,通知他们出现了某种情况,如子进程终止需要通知父进程
- 进程控制:一个进程希望完全控制另一个进程的执行(如debug进程),此时就需要控制进程可以拦截另一个进程的所有陷入和异常,并能够及时地知道被控制的进程的状态的改变
进程间通信的发展过程大致是:由管道方式 到 System V进程间通信,再到POSIX进程间通信,无论如何,进程间通信都应该要具备以下条件:
- 在内存中有交换数据的空间
- 该空间应该由OS提供,而不能是由通信进程的任何一方提供
OS提供的进程间交换数据的空间的样式不同,决定了进程间通信方式的不同:
1.管道:
· 匿名管道
· 命名管道
2.System V IPC:
· System V 消息队列
· System V 共享队列
· System V 信号量
3.POSIX IPC:
· 消息队列
· 共享内存
· 信号量
· 互斥量
· 条件变量
· 读写锁
4种经典的进程间通信方式
管道
我们把从一个进程连接到另一个进程的一个数据流称为一个管道,管道是Unix中最古老的进程间通信方式。
我们知道当进程打开一个文件时其内核的示意图如下所示:
管道的通信原理可以先理解为当不同的进程打开同一个文件时,尽管进程拥有自己的files_struct、file* fd_array、file对象,但他们会共享同一个内核级缓冲区,指向同一块磁盘空间,这样双方进程就可以以共享的空间作为中间媒介进行数据的交换,实现进程间的通信。
匿名管道
如果我们创建了一个磁盘中不存在且不需要向磁盘进行数据刷新的文件时,就称这种内存级的文件为匿名文件,而匿名管道就是一种特殊的匿名文件,专门用于进程间的通信。当父进程使用系统调用pipe()创建一个匿名管道后,不同于普通的文件系统,其会分别为读端和写端创建2个匿名管道文件表项(不同于普通文件的文件表项(file对象)),这2个文件表项指向同一块缓冲区,同时pcb使用2个文件描述符指向这2个文件表项,这样读端和写端各自管理读写操作避免了读写混乱。
接着当我们创建一个子进程时,则其子进程的pcb会继承父进程的pcb的数据,他们会共享父进程创建子进程前打开的文件的文件表项,这样父子进程就可以看到同一块内存空间了,这样就实现了匿名管道的通信方式,但为了避免管道里面数据读写混乱,我们应该将管道用于单向通信(即将数据流向设置为只能单向流动,可以关闭父进程的读端(或写端),关闭子进程的写端(或者读端)),如果想要双向通信则需创建2个匿名管道。
创建匿名管道的系统调用函数为:
#include<unistd.h>
int pipe(int fd[2])
使用时需要包含头文件<unistd.h>,fd是文件描述符数组,fd[0]表示管道的读端,fd[1]表示管道的写端,如果函数调用成功返回0,失败则返回错误码。
#include<iostream>
#include<string>
#include<unistd.h>
#include<stdlib.h>
using namespace std;
int main()
{
int fd[2];
if(pipe(fd))
{
perror("pipe error");
return 0;
}
pid_t pid=fork();
if(pid<0)
{
perror("pipe error");
return 0;
}
if(0==pid)
{
char s[60];
int i=5;
while(i--)
{
read(fd[0],s,50);
cout<<"child process "<<getpid()<<" "<<s<<endl<<endl;
sleep(1);
}
}
char s[60]="hello i am father process!!!";
int i=5;
while(i--)
{
s[28]=i+'0';
write(fd[1],s,30);
sleep(1);
}
return 0;
}
匿名管道特点:
- 只能用于拥有血缘关系的进程进行单向通信
- 数据一旦被读取后就释放
- 管道的生命周期随进程,一旦进程退出,管道自行释放
- 内核会对管道进行同步与互斥,即读写端有序进行
- 管道是面向字节流的,即一次读取多少数据与一次写入多少数据无关
- 管道是半双工的,数据只能向一个方向流动,需要双向通信时需要建立2个管道
命名管道
匿名管道被限制在只能具有血缘关系的进程才可以进行通信,如果想任意两个进程进行单向通信,可以使用命名管道,其是FIFO文件,是一种特殊类型的文件,有文件名和路径,但数据不会刷新到磁盘中。当我们在进程A中使用系统调用函数mkfifo创建了一个命名管道后,如果我们在使用进程B打开该命名管道,此时进程B会创建自己的与该命名管道相关联的文件表项,A、B进程中这2个对应的文件表项都指向同一块内核级缓冲区,这样就实现了命名管道的通信方式(如果使用普通文件进行进程间的通信,不仅实现困难,且安全性难以保证)。
内核理解:
需要注意的是,尽管命名管道不涉及磁盘的IO操作,但其依旧需要inode结构,该结构提供了文件系统级别的识别、权限控制、路径解析等功能,使得命名管道可以像其他类型的文件一样被系统和程序识别并使用。
1.创建命名管道:
方法①:使用命令
mkfifo [filename]
方法②:使用系统调用
#include<sys/types.h>
#include<sys/stat.h>
int mkfifo(const char* filename,mode_t mode)
该函数调用成功返回0,失败返回-1。参数说明:
filename
:命名管道的文件路径,如果只给文件名则默认是在当前路径下创建
mode
:表示管道的默认权限,
2.打开匿名管道:
创建命名管道后,使用 open()
打开匿名管道,在打开命名管道时其打开规则如下(可参考:初闻的博客):
- ①当打开操作是为读打开FIFO时:如果在open时设置为非阻塞模式,其会立刻返回成功;如果在open时设置为阻塞模式,其会阻塞直到有相应的进程为写而打开该FIFO文件,默认的打开模式为阻塞模式。
- ②当打开操作是为写打开FIFO时:如果在open时设置为非阻塞模式,其会立刻返回失败,错误码为ENXIO;如果在open时设置为阻塞模式,其会阻塞直到有相应的进程为读而打开该FIFO文件,默认的打开模式为阻塞模式。
不应以读写方式打开命名管道,否则容易出现无法预见的错误。
3.删除命名管道:
①在命令行窗口使用命令rm
②使用系统调用
#include <unistd.h>
int unlink(const char *pathname);
该函数执行成功时返回0,失败时返回-1,并设置全局变量errno以指示错误原因,参数说明:
pathname
:要删除的文件或目录项的路径名,可以是相对路径或绝对路径
//代码示例
#include<iostream>
#include<string>
#include<stdlib.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<unistd.h>
#include<fcntl.h>
using namespace std;
int main()
{
if(mkfifo("corres.txt",0664))
{
perror("mkfifo");
}
pid_t pid=fork();
if(pid<0)
{
perror("fork");
return 0;
}
if(0==pid)
{
int fid=open("corres.txt",O_RDONLY);
if(fid<0)
{
perror("open");
return 0;
}
char s[60];
int i=5;
while(i--)
{
read(fid,s,50);
cout<<"child process "<<getpid()<<" "<<s<<endl<<endl;
sleep(1);
}
return 0;
}
int fid=open("corres.txt",O_WRONLY);
if(fid<0)
{
perror("open");
return 0;
}
char s[60]="hello i am father process!!!";
int i=5;
while(i--)
{
s[27]=i+'0';
write(fid,s,30);
sleep(1);
}
return 0;
}
命名管道特点:
- 只能单向通信
- 数据一旦被读取后就释放
- 其生命周期与进程是否退出无关,直至使用unlink()系统调用显示删除命名管道
- 内核会对管道进行同步与互斥,即读写端有序进行
- 管道是面向字节流的,即一次读取多少数据与一次写入多少数据无关
- 管道是半双工的,数据只能向一个方向流动,需要双向通信时需要建立2个管道
匿名管道和命名管道的区别:
- 匿名管道由pipe函数创建并打开。
- 命名管道由mkfifo函数创建,打开用open()
FIFO(命名管道)与pipe(匿名管道)之间唯一的区别在它们创建与打开的方式不同,一但这些工作完成之后,它们具有相同的语义,同时无论是匿名管道还是命名管道其大小都是有限的,一般为64KB。
2种管道通信方式都有可能会出现以下4种情况:
- 1.如果管道内部没有数据且写端进程不关闭该管道文件,此时读端会进行阻塞等待,直至管道有数据
- 2.如果管道已经写满且读端进程不关闭该管道文件,此时写端会进行阻塞等待直至读端读取数据
- 3.如果写端关闭了该管道文件,此时读端会将管道中的数据读完,直至读取的返回值为0
- 4.如果读端关闭了该管道文件且写端还在写,此时OS会直接向写端进程发送SIGPIPE信号终止写端的进程。
共享内存
我们知道进程的地址空间中有一段共享区,其通过页表可以映射到一段共享空间,如果我们让2个进程将同一块空间通过页表映射到各自的进程地址空间中,这样就可以实现2个进程间的通信了,这种通信方式称为共享内存。
因此实现共享内存的步骤为:
①让OS在内存中开辟一段空间
②构建映射:让OS填充页,表将2个进程的虚拟地址映射开辟的空间中。
共享内存不需要像管道一样需要进行文件操作,一旦OS开辟的空间映射到共享它的进程的地址空间,那么进程间的数据传递就可以不再涉及内核,通信的进程可以不再通过执行进入内核的系统调用进行数据传递,即在管道通信中,写方需要调用系统调用write()将数据拷贝管道中,读方也需要调用系统调用read()将数据拷贝到自己到自己身上才可以读取到数据,而共享内存通信方式只需要写方将数据拷贝一次到共享内存中,因此其是最快的进程间通信(IPC)形式。
1.创建共享内存:
#include<sys/shm.h>
int shmget(key_t key,size_t size,int shmflg);
该系统调用函数用于创建一段连续的空间,成功返回一个非负整数,代表该共享内存段的标识码,失败返回-1,参数说明:
size
:共享内存的大小,单位为字节
shmflg
:权限标志位,当将shmflg添加IPC_CREAT(例如:IPC_CREAT | 0666,注意不能直接将shnflg设为IPC_CREAT,否则就会导致进程对该空间的访问权限不足引发问题)时,如果共享内存已经存在则直接获取它,否则就创建共享内存;当将shmflg添加IPC_CREAT | IPC_EXCL时,如果共享内存已经存在则出错返回,否则就创建共享内存(即意味着如果创建成功则该共享内存一定是新的)
key
:该共享内存的名字,是一个整数,相当于一个标识符,因为共享内存在内核中可能会有多份以满足多个进程的通信的需求,唯一标识符key值是为了保证需要通信的进程看到的是同一份共享内存,一般需要调用系统调用 ftok()
生成一个key值:
#include<sys/shm.h>
key_t ftok(const char *pathname, int proj_id);
该系统调用执行成功时返回一个key_t类型的key值,失败时返回-1,并设置相应的errno以指示错误,参数说明:
pathname
:用于接收任一字符串,通常传文件名即可
proj_id
:用于接收一个数字,用作项目的标识符,用户可以随意传
shmget()的参数key一般不建议用户直接传一个整数,而是应该调用ftok()产生,否则很容易与已有的内存标识产生冲突。key值不能由系统自动创建而是必须由用户传的原因是现在是用户想要2个进程之间进行通信,那么用户就必须要向2个进程发送该key值这2个进程才能看到同一份共享空间从而进行通信,如果由系统自动创建key值,系统无法知道用户希望哪2个进程进行通信,用户也无法获取key值让2个进程建立通信。
2.将共享内存连接到进程地址空间:
#include<sys/shm.h>
void *shmat(int shmid, const void *shmaddr, int shmflg);
该系统调用用于将共享内存连接到进程地址空间,执行成功返回一个指针,指向共享内存的第一个字节,失败返回-1,参数说明:
shmid
:共享内存的标识,即shmget()系统调用的返回值
shmflg
:权限标记位,设为0即可,表示可读可写,如果设为SHM_RDONLY表示只读,如果设为SHM_RND且shmaddr不为nullptr的话,表示对齐共享内存段
shmaddr
:用于指定连接的地址,一般设为nullptr即可,表示让系统自动选择一个地址。
此时我们就可以让进程进行通信了,如果我们想某个进程取消与另一个进程的通信,就需要将共享内存段与当前进程脱离。
3.将共享内存段与当前进程脱离:
#include<sys/shm.h>
int shmdt(const void *shmaddr);
该系统调用用于将共享内存段与当前进程脱离,成功返回0,失败返回-1,参数说明:
shmaddr
:共享内存起始地址,即shmat()的返回值
需要注意将共享内存段与当前进程脱离不等于释放共享内存。
4.控制共享内存:
#include<sys/shm.h>
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
该系统调用用于对共享内存进行控制,包括释放共享内存等,成功返回0,失败返回-1,参数说明:
shmid
:shmget()返回的内存标识码
cmd
:用于指定要执行的命令,命令包括:
- IPC_RMID:删除共享内存段。当共享内存段被删除时,它并不会立即消失,而是等到所有附加到该共享内存段的进程都分离它之后,系统才会真正删除它,即只有在当前映射连接数为0时才会被删除释放
- IPC_STAT:获取共享内存段的当前状态,并将其存储在 buf 指向的 shmid_ds 结构中 (系统将共享内存的信息存放在数据类型为shmid_ds的结构中)。
- IPC_SET:设置共享内存段的属性,使用 buf 指向的 shmid_ds 结构中的信息。可以设置的属性包括共享内存段的权限和所有者。
buf
:是指向 shmid_ds 结构的指针,用于存储或获取共享内存段的属性信息。如果 cmd 是 IPC_STAT,则 buf 用于存储信息;如果 cmd 是 IPC_SET,则 buf 用于提供要设置的信息。如果 cmd 是 IPC_RMID,则 buf 可以设置为 NULL。
在使用共享内存时,需要注意:
- 共享内存读方不会为写方进行阻塞等待,即共享内存不提供进程间的协同机制,这是共享内存的一个缺点,这会导致数据不一致问题,即写方数据还没有写完,读方就将数据读走了,因此需要用户自己提供协同机制,可行的方法有管道和信号量。
- 共享内存的生命周期随内核,如果共享内存用户不主动释放,即使进程退出其也存在,除非OS重启才会自动释放。
- 共享内存的大小是以4KB为基本单位的,用户在调用shmget()给的大小是用户可以使用的有效空间的大小,其实际会开n*4KB大小的空间,但多开的空间用户不能使用,相当于被浪费了,因此建议开辟的共享内存的大小应为n*4KB。
- key是给内核使用用来唯一标识一段共享内存空间的,shmid是给用户使用的用来标识一段共享空间并控制它的。
用户可以使用命令 ipcs
查看共享的资源,使用 ipcrm
命令释放共享内存资源
ipcs -m//查看共享内存
ipcrm -m xxx(shmid)//释放shmid为xxx的共享内存
#include<iostream>
#include<string>
#include<unistd.h>
#include<sys/shm.h>
using namespace std;
int main()
{
int pid = fork();
if(pid<0)
{
perror("fork");
return -1;
}
key_t k = ftok("mycodetest",1);
int shmid=shmget(k,4096,IPC_CREAT|0666);//一定要注意访问权限
if(0==pid)
{
//子进程,读取
int t=10;
while(t--)
{
char* com=(char*)shmat(shmid,nullptr,0);
printf("%s\n",com);
sleep(1);
}
}
if(pid>0)
{
//父进程,写入
int t=10;
while(t--)
{
char* com=(char*)shmat(shmid,nullptr,0);
string tmp("hello,i am father process");
tmp+=t+'0';
int i=0;
while(i<tmp.size())
{
com[i]=tmp[i];
++i;
}
com[i]='\0';
sleep(1);
}
}
shmctl(shmid,IPC_RMID,nullptr);
return 0;
}
消息队列
消息队列是一种已经被淘汰的通信方式,这里简单了解一下即可。
简单来说就是OS在内核中维护了一个队列,当进程写入一个消息时,OS就会在消息队列中添加一个消息块,当进程读取消息后,就将该消息块从消息队列中移除,数据的类型由通信双方约定好。由于通信双方进程都可以向消息队列读写消息块,因此为了双方进程都可以准确拿到数据,会在消息块中添加一个标识。
消息队列的生命周期也是随内核,消息一旦读取就删除,每个消息块的大小有限,消息队列的长度有限,系统的消息队列的个数也有限。
1.建立消息队列:
#include <sys/msg.h>
int msgget(key_t key, int msgflg);
该系统调用用于创建或打开消息队列,成功返回队列ID,失败返回-1,参数说明:
key
:也是由ftok()获得
msgflg
:权限位,当添加IPC_CREAT时,如果消息队列已经存在则直接获取它,否则就创建消息队列,当在shmflg中添加IPC_CREAT | IPC_EXCL时,如果消息队列已经存在则出错返回,否则就创建消息队列,当添加IPC_NOWAIT时,表示当读写消息队列无法满足要求时,不进行阻塞等待。
2.读写消息:
①写消息
#include <sys/msg.h>
int msgsnd(int msgid, const void *msgp, size_t msgsz, int msgflg);
该系统调用用于向消息队列写消息,成功返回0,失败返回-1,参数说明:
msgid
:消息队列的ID,即函数msgget()的返回值
msgp
:指向存放消息的msgbuf结构体指针
msgsz
:写入数据的大小(以字节为单位)
msgflag
:表示函数的控制属性,当其为IPC_NOWAIT时,表示当消息无法写入消息队列时,msgsnd()函数立即返回,当将msgflag设为0时,表示进行阻塞直至条件满足,msgflag一般设为0即可。
struct msgbuf
{
long mtype;//用于指示消息类型,由用户自己约定
char mtext[];//消息正文
};
②读消息
#include <sys/msg.h>
ssize_t msgrcv(int msqid, struct msgbuf *msgp, size_t msgsz, long msgtyp,int msgflg);
该系统调用用于从消息队列中读取一个消息,成功则返回读取到的数据的大小,失败返回-1,参数说明:
msgid
:消息队列的ID,即函数msgget()的返回值
msgp
:这是一个输出型参数,用于存储接收的消息
msgsz
:前一个参数msgp指向的magbuf结构的大小,这个大小包括了 mtype 成员和 mtext 数组的总大小
msgtyp
:用于指定要读取的消息的数据类型,如果将其设为0则默认读取消息队列第一条信息,如果将其设为小于0的值,则读取数据类型的值小于该负数的绝对值的消息块,如果有多个,则取类型值最小的消息
msgflag
:表示函数的控制属性,其可以设为以下值:
-
MSG_NOERROR:如果msgsz小于队列中消息的实际大小,则截断消息,而不是产生错误
-
IPC_NOWAIT:调用进程会立即返回.若没有收到消息则返回-1.
-
0:msgrcv调用阻塞直到条件满足为止
3.控制消息队列:
#include <sys/msg.h>
int msgctl(int msqid, int cmd, struct msqid_ds *buf);
该系统调用用于控制消息队列:成功返回0,失败返回-1,参数说明:
msgid
:消息队列的ID,即函数msgget()的返回值,
buf
:消息队列缓冲区
cmd
:控制选项,其可设为以下值:
- IPC_STAT:此时参数buf为输出型参数,将msqid相关的数据结构中各个元素的当前值存入到由buf指向的结构中.
- IPC_SET:此时参数buf为输入型参数,将msqid相关的数据结构中的元素设置为由buf指向的结构中的对应值.
- IPC_RMID:删除由msqid指示的消息队列,buf参数此时传nullptr即可。
#include<iostream>
#include<string>
#include<unistd.h>
#include<sys/msg.h>
#include<string.h>
using namespace std;
#define CHILD_PROCESS_WRITE 1
#define FATHER_PROCESS_WRITE 2
//最好自己定义msgbuf,别用系统自带的
struct mymsgbuf
{
long mtype;
char mtext[90];
};
int main()
{
int pid = fork();
if(pid<0)
{
perror("fork");
return -1;
}
key_t k = ftok("mycodetest",1);
int msgid=msgget(k,IPC_CREAT|0666);
if(0==pid)
{
//子进程,读写消息
char s[50]="hello,i am child process";
int t=10;
while(t--)
{
//发送消息
s[24]=t+'0';
s[25]='\0';
struct mymsgbuf mb_w;
mb_w.mtype=CHILD_PROCESS_WRITE;
strcpy(mb_w.mtext,s);
if(-1==msgsnd(msgid,&mb_w,strlen(mb_w.mtext)+1,0))
{
perror("msgsnd");
}
//接收消息
struct mymsgbuf mb_r;
if(-1==msgrcv(msgid,&mb_r,sizeof(mb_r.mtext),FATHER_PROCESS_WRITE,0))
{
perror("msgrcv");
}
else
{
printf("child process read:%s\n",mb_r.mtext);
sleep(1);
}
}
return 0;
}
if(pid>0)
{
//父进程,读写消息
char s[50]="hello,i am father process";
int t=10;
while(t--)
{
//发送消息
struct mymsgbuf mb_w;
s[24]=t+'0';
s[25]='\0';
mb_w.mtype=FATHER_PROCESS_WRITE;
strcpy(mb_w.mtext,s);
if(-1==msgsnd(msgid,&mb_w,strlen(mb_w.mtext)+1,0))
{
perror("msgsnd");
}
//接收消息
struct mymsgbuf mb_r;
if(-1==msgrcv(msgid,&mb_r,sizeof(mb_r.mtext),CHILD_PROCESS_WRITE,0))
{
perror("msgrcv");
}
else
{
printf("father process read:%s\n",mb_r.mtext);
sleep(1);
}
}
}
msgctl(msgid,IPC_RMID,NULL);
return 0;
}
信号量
概念:
①互斥:在访问公共资源时,该公共资源不允许多个进程同时访问。如果进程访问公共资源是互斥的,那么该公共资源就是安全的。
②同步:访问公共资源时在安全的前提下,进程按一定的顺序依次访问。
只有对公共资源的访问是互斥且同步的,那么对公共资源的访问才是安全且合理的。
③临界资源:系统中不允许多个进程同时使用的资源就是临界资源
④临界区:对临界资源进行访问的代码段就是临界区
⑤原子性:如果某一个操作只有2种状态:要么还没有开始,要么已经完成,就称该操作具有原子性,可以简单理解成该操作翻译成汇编代码只有一条指令。
如果某一份公共资源可以被划分成多份小资源,那么进程想访问该公共资源只需要看是否还有空闲的小资源即可(相当于一个人想去电影院看电影只要看是否还有空闲的座位即可),因此如果想要安全的访问公共资源,需要做到:
①.限制访问该公共资源的进程量
②.合理分配该公共资源
而信号量本质就是一个临界资源的计数器,用来记录空闲的临界资源的数量,这意味着信号量必须可以被不同的进程同时看到,这符合进程间通信的定义,因此信号量也是进程间通信方式的一种,只不过该通信方式是以进程间进行协同为目的的,而不是进行普通消息的传递。申请一个信号量就是对计数进行减减操作,称为P操作,这时就完成了对该资源的预定,释放一个信号量就是对计数进行加加操作,称为V操作,这时就完成了对该资源归还,为了防止信号量在加减过程中出现不一致问题,对信号量的申请和释放必须是原子性的(由OS实现)。
利用信号量就可以限制访问公共资源的进程量,至于如何合理地分配公共资源由开发者自己控制。申请信号量再使用公共资源,使用完公共资源后释放信号量是每一个开发者应该遵守的规则。
1.获取信号量:
#include <sys/sem.h>
int semget(key_t key, int nsems, int semflg);
该系统调用用于获取一个或多个信号量,成功返回该信号集的标识符,失败返回-1,参数说明:
key
:ftok()的返回值
nsems
:要创建的信号量的数量
semflg
:权限位,用于指定semget()的行为和权限,它可以添加以下标志:
- IPC_CREAT:如果信号量集不存在,则创建一个新的信号量集。
- IPC_EXCL:与IPC_CREAT结合使用,如果信号量集已经存在,则函数调用失败。
2.对信号量进行PV操作:
#include <sys/sem.h>
int semop(int semid, struct sembuf *sops, size_t nsops);
该系统调用执行成功返回0,失败返回-1,参数说明:
semid
:信号集合标识符,即semget()的返回值
sops
:一个指向sembuf结构数组的指针,sembuf结构描述了一个对信号量的操作,其定义如下:
struct sembuf
{
unsigned short sem_num; // 信号量编号,在信号量集中的位置
short sem_op; // 对信号量的操作(正数、零或负数)
short sem_flg; // 操作标志
};
sem_num
:指定要操作的信号量在信号量集中的编号,信号量集可以包含多个信号量,每个信号量都有一个唯一的编号,从0开始。sem_op
:指定对信号量的操作。如果sem_op
是正数,表示释放(V操作)相应的信号量,增加信号量的值;如果sem_op
是负数,表示获取(P操作)相应的信号量,减少信号量的值,如果信号量的值小于或等于sem_op
的绝对值,则调用进程会阻塞直到信号量的值增加;如果sem_op
是零,表示调用进程会阻塞直到信号量的值为零。sem_flg
:这是一个位掩码,可以添加以下标志:
-SEM_UNDO
:当进程终止时,系统会自动撤销该进程对信号量所做的所有操作,这样可以避免进程异常终止时导致的资源永久锁定。
-IPC_NOWAIT
:如果操作不能立即执行(例如,信号量的值不足以减少),则semop()
调用不会阻塞,而是立即返回错误。
参数nsops用于指定sops
数组中sembuf
结构的数量,即要执行的操作的数量。
3.控制信号量:
#include <sys/sem.h>
int semctl(int semid, int semnum, int cmd, ...);
该系统调用执行成功返回0或所需的信号量值,失败返回-1,参数说明如下:
semid
:这是一个信号量集的标识符,由之前的semget()
系统调用返回。semnum
:指定要操作的信号量在信号量集中的编号。如果操作影响整个信号量集,则这个参数通常被设置为0。cmd
:指定要执行的操作。这个参数可以是以下几种命令之一:-
GETVAL
:返回信号量集中指定编号的信号量的值。 -
SETVAL
:设置信号量集中指定编号的信号量的值。 -
GETPID
:返回最后一个执行semop()
操作的进程的PID。 -
GETNCNT
:返回正在等待信号量集中指定编号的信号量增加的进程数。 -
GETZCNT
:返回正在等待信号量集中指定编号的信号量变为0的进程数。 -
IPC_RMID
:立即删除信号量集。 -
IPC_STAT
:将信号量集的当前状态信息复制到由第四个参数指定的semid_ds
结构中。 -
IPC_SET
:将第四个参数指定的semid_ds
结构中的值设置为信号量集的属性。
-
- 第四个参数(可选):根据
cmd
的不同,这个参数可以是以下几种类型之一:union semun
:这是一个联合体,用于传递额外的数据,例如设置信号量的值或获取信号量集的状态信息。semun
通常在用户程序中定义,如下所示:union semun { int val; // 用于SETVAL命令 struct semid_ds *buf; // 用于IPC_STAT和IPC_SET命令 unsigned short *array; // 用于GETALL和SETALL命令 };
struct semid_ds *buf
:这是一个指向semid_ds
结构的指针,用于获取或设置信号量集的属性信息。
semctl()
函数成功时返回0或所需的信号量值,失败时返回-1,并设置errno来表示错误原因。
semctl()
系统调用可以用来获取和设置信号量的值,获取信号量集的信息,以及删除信号量集。在实际应用中,通常需要结合semget()
和semop()
系统调用一起使用,以创建、操作和控制信号量集。
消息队列、共享队列、信号量都是System V IPC(即System V进程间通信方式),OS在管理这3种通信时用的是同一种结构体进行描述,对这3中通信方式,用户都可以通过 ipcs
查看进程间通信资源 ipcrm
删除进程间通信资源,需要使用以下选项:
-
-m
针对共享内存的操作 -
-q
针对消息队列的操作 -
-s
针对信号量的操作 -
-a
针对所有资源的操作