进程间通信:进程之间的沟通交流
进程间为什么要沟通交流?
在实际工作中往往会出现在一个系统中好几个进程协同工作,那么这些进程就需要沟通交流,完成协作,而由于进程的独立性, 进程间的沟通变得困难,复杂。因此就产生了各种进程间通信方式,来解决如何进行进程间通信的问题。
进程间通信的目的:
数据传输:一个进程需要将它的数据发送给另一个进程;
资源共享:多个进程间共享同样的资源;
通知事件:一个进程需要向另一个或一组进程发消息,通知它们发生了某种事件(如进程终止时要通知父进程)。
进程控制:有些进程希望完全控制另一个进程的执行(如Debug进程),此时控制进程希望能够拦截另一个进程的所有陷入和异常,并能够及时知道它的状态改变。
进程间通信方式:
1.管道
管道:传输资源。本质上是内核的一块缓冲区。(特性:半双工,单向通信)。 Linux一切皆文件,操作系统为管道提供操作的方法:文件操作
⽤用fork来共享管道原理 :
站在⽂文件描述符⾓角度-深度理解管道 :
管道分为:匿名管道/命名管道
匿名管道
没有名字的管道(仅用于具有亲缘关系(父子,兄弟等)的进程间通信)。
创建匿名管道:pipe
#include <unistd.h>
int pipe(int pipefd[2]);
pipefd:用于接受匿名管道创建成功后返回的两个描述符,两个描述符用于对管道进行操作(文件io操作)
pipefd[0]:用于从管道读取数据;
pipefd[1]:用于向管道写入数据
返回值: 返回:0 失败: -1
匿名管道原理:以父子进程为例:创建一个子进程,子进程复制了父进程的描述符表,因此子进程也有描述符表,并且他们指向的是同一个管道,由于父子进程都能访问这个管道,就可以通信。
因为管道是半双工单向通信,因此在通信前要确定数据流向:即关闭父子进程各自一端不用的读写。如果一方是读数据就关闭写的描述符。
//这是一个匿名管道实现:功能:从父进程写入数据,子进程读取数据
#include<stdio.h>
#include<unistd.h>
#include<string.h>
#include<errno.h>
int main()
{
int fd[2];
//管道需要创建在创建子进程前,这样才能复制
if(pipe(fd)<0)
{
perror("pipe errno");
return -1;
}
int pid=-1;
pid=fork();//创建子进程,对于父进程会返回子进程id,子进程会返回0,创建失败会返回0
if(pid<0)
{
perror("fork errno");
return -1;
}
else if(pid==0)
{
//子进程 读取数据-> fd[0]
close(fd[1]);//fd[1]是向管道写入数据,子进程不用写入数据,需要关闭管道写入端
char buff[1024]={0};
read(fd[0],buff,1024);//如果管道没数据会等待,然后读取数据,默认阻塞等待直至有数据
printf("buff:%s\n",buff);
close(fd[0]);
}
else
{
//父进程 :写入数据->fd[1]
close(fd[0]); //fd[0]是读取数据,父进程不用读取数据,需要关闭管道读取端,由于父子进程相互独立,关闭一方描述符对另一方无影响
write(fd[1],"happy day",10);
close(fd[1]);
}
return 0;
}
实现ps -f | grep ssh
//实现 ps -ef | grep ssh
// 父进程将ps -ef 写入管道,子进程读取管道里的数据
#include<stdio.h>
#include<unistd.h>
#include<error.h>
#include<string.h>
int main()
{
int pipefd[2];
if(pipe(pipefd)<0)
{
perror("pipe error");
return -1;
}
int pid=fork();
if(pid<0){
perror("fork error");
return -1;
}
else if(pid==0)
{
//子进程 grep ssh
//grep这个进程原本从标准输入(0)读取数据,但是现在需要从管道读取数据,所以需要把标准输入重新定向到管道读取端
close(pipefd[1]); //关闭子进程写入端
dup2(pipefd[0],0);//标准输入0定向为管道读取端
execlp("grep","grep","ssh",NULL);//替换
close(pipefd[0]);
}
else
{
//子进程 ps -f
// ps 这个进程原本把数据写入到标准输出,即打印到显示器上,但是现在需要把数据写入到管道,所以需要把标准输出重新定向为到管道写入端
close(pipefd[0]); //关闭父进程读取端
dup2(pipefd[1],1); //标准输出1定向为管道写入端
execlp("ps","ps","-ef",NULL); //替换
close(pipefd[1]);
}
return 0;
}
匿名管道特性:
1.只能用于具有亲缘关系的进程间通信;
2.管道是半双工单向通信;(两个文件描述符,用一个,另一个不用,不用的文件描述符就要close)
3.管道的生命周期随进程(打开管道的所有进程退出,管道释放);
4.管道是面向字节流传输数据。(面向字节流:数据无规则,没有明显边界,收发数据比较灵活:对于用户态,可以一次性发送也可以分次发送,当然接受数据也如此;而面向数据报:数据有明显边界,数据只能整条接受 )
5.内核会对管道操作进行同步与互斥;
临界资源: 大家都能访问到的共享资源
临界区: 对临界资源进行操作的代码
同步: 临界资源访问的可控时序性(一个操作完另一个才可以操作)
互斥: 对临界资源同一时间的唯一访问性(保护临界资源安全)
匿名管道读写规则:
1.管道无数据读取(read): 如果描述符是默认的阻塞特性,读取将会阻塞挂起等待,直到管道有数据;
2.管道被写满(write):如果描述符是默认的阻塞特性,写入操作会阻塞挂起等到,直到有数据被取走;如果描述符被设置为非阻塞特性,写入操作不具备条件,直接报错返回-1,错误码:EAGAIN。
3.如果所有管道写入端对应的文件描述符被关闭,则读取完管道中数据,然后read返回0;
4.如果所有管道读取端对应的文件描述符被关闭,则write操作会触发异常(因为没有人读数据),操作系统会给进程发送SIGPIPE,进程收到这个进程会退出;
5.当要写入的数据量不大于PIPE_BUF(512字节)时,linux将保证写入的原子性(操作不会被打断,一步完成);
6.当要写入的数据量大于PIPE_BUF时,linux将不再保证写入的原子性。
命名管道
命名管道:文件系统可见,是一个特殊类型(管道类型)文件,命名管道可以应用于同一主机上任意进程间通信。
命名管道创建:
1.命令创建:mkfifo 管道名
2.代码创建:
#include <sys/types.h>
#include <sys/stat.h>
int mkfifo(const char *pathname, mode_t mode);
pathname: 管道文件的路径名;
mode: 管道文件的权限;
成功返回 : 0,失败返回 :-1
命名管道打开特性:
1. 如果用只读打开命名管道,open函数将阻塞等待直至有其他进程以写的方式打开这个命名管道,如果没有进程以写的方式发开这个命名管道,程序将停在此处
2.如果用只写打开命名管道,open函数将阻塞等到直至有其他进程以读的方式打开这个命名管道,如果没有进程以读的方式发开这个命名管道,程序将停在此处;
3.如果用读写打开命名管道,则不会阻塞(但是管道是单向)
// 命名管道代码操:作,从命名管道中读取数据打印
#include<stdio.h>
#include<unistd.h>
#include<errno.h>
#include<string.h>
#include<fcntl.h>
int main()
{
//用mkfifo创建管道
umask(0); //将掩码设置为0
if(mkfifo("./test.fifo",0664)<0)
{
if(errno==EEXIST);//由于mkdir只能创建不存在命名管道,如果存在会报错,在这里如果存在继续走下面代码
else
{
perror("mkfifo error");
return -1;
}
}
//用open打开管道
int fd=open("./test.fifo",O_RDONLY);//用只读打开命名管道,open函数将阻塞等到直至有其他进程以写的方式打开这个命名管道,如果没有进程以写的方式打开这个命名管道,程序将停在此处
if(fd<0)
{
perror("open error");
return -1;
}
printf("open fifo sucess and start read\n");
//从管道中读取数据
while(1)
{
char buff[1024]={0};
int ret= read(fd,buff,1024); //管道是缓冲区,所以读取数据从缓冲区开始读;并且是单工通信(读或写一种)
if(ret>0)
printf("[%s]\n",buff);
// 管道特性:如果所有写端(echo 写完退出test.fifo,就是关闭了写端)关闭,那么读取时返回0
else if(ret==0)
{
printf("all write closed\n");
sleep(3);
}
}
//关闭管道
close(fd);
return 0;
}
例:用命名管道实现server&client通信(一个写数据,一个将数据输出)
//fifo_a.c(以写的方式打开)
#include<stdio.h>
#include<unistd.h>
#include<errno.h>
#include<string.h>
#include<fcntl.h>
int main()
{
//用mkfifo创建管道
umask(0); //将掩码设置为0
if(mkfifo("./test.fifo",0664)<0)
{
if(errno==EEXIST);//由于mkdir只能创建不存在命名管道,如果存在会报错,在这里如果存在继续走下面代码
else
{
perror("mkfifo error");
return -1;
}
}
//用open打开管道
int fd=open("./test.fifo",O_WRONLY);//用只写打开命名管道,open函数将阻塞等到直至有其他进程以读的方式打开这个命名管道,如果没有进程以读的方式发开这个命名管
道,程序将停在此处
if(fd<0)
{
perror("open error");
return -1;
}
//从管道中读取数据
while(1)
{
char buff[1024]={0};
scanf("%s",buff);
write(fd,buff,strlen(buff));
sleep(1);
}
//关闭管道
close(fd);
return 0;
}
命名管道和匿名管道区别和联系:
1.区别:匿名管道用int pipe(int pipefd[2]); 创建并打开匿名管道返回描述符
命名管道用mkfifo或者 int mkfifo(const char *pathname, mode_t mode);创建,并没有打开,如果打开需要open;
匿名管道是具有亲缘关系进程间通信的媒介,而命名管道作为同一主机任意进程间通信的媒介;
匿名管道不可见文件系统,命名管道可见于文件系统,是一个特殊类型(管道类型)文件。
2.联系:匿名管道和命名管道都是内核的一块缓冲区,并且都是单向通信;另外当命名管道打开(open)后,所有特性和匿名管道一样(上文匿名管道读写规则与管道特性):两者自带同步(临界资源访问的时序性)与互斥(临界资源同一时间的唯一访问性),管道生命周期随进程退出而结束。
消息队列(System V IPC)
消息队列实际上是操作系统在内核为我们创建的一个队列,通过这个队列的标识符key,每一个进程都可以打开这个队列,每个进程都可以通过这个队列向这个队列中插入一个结点或者获取一个结点来完成不同进程间的通信。
如何传输数据:
用户组织一个带有类型的数据块,添加到队列中,其他的进程从队列中获取数据块,即消息队列发送的是一个带有类型的数据块;消息队列是一个全双工通信,可读可写(可以发送数据,也可以接受数据)
消息队列生命周期随内核,如果没有释放消息队列或者没有关闭操作系统,消息队列会一直存在。
创建消息队列:
msgget:
#include <sys/ipc.h>
#include <sys/msg.h>
int msgget(key_t key, int msgflg);
key: 内核中消息队列的标识
msgflg:
IPC_CREAT :不存在消息队列则创建,存在则打开
IPC_EXCL :与 IPC_CREAT 同用时,存在则报错
mode :权限
返回值:代码操作的句柄 ,失败: -1
接收数据:
msgrcv:是从一个消息队列接收数据
#include <sys/ipc.h>
#include <sys/msg.h>
ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtype, int msgflg);
msgrcv 默认阻塞的获取数据(即没有数据,会一直等待)
msqid: msgget返回的操作句柄;
msgp :用于接受数据
msgsz: 指定接受数据的大小(不包括mtype)
msgtype: 指定接受的数据类型
msgtype=0 取队列中字一个节点,不分类型;
msgtype>0 取指定类型数据块的第一个节点;
msgtype<0 取队列中第一条类型数据块小于等于msgtype绝对值的节点,并且是满足条件的消息类型最小的节点。
msgflg: 标志选项,0(默认);MSG_NOERROR ,当数据长度超过指定长度,则截断数据
发送数据:
msgsnd: 把一条消息添加到消息队列中
#include <sys/ipc.h>
#include <sys/msg.h>
int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);
通过类型判断接受何种数据
如果是不正常退出(Ctrl+C),消息队列存在,如果发送一条消息,用ipcs查看消息队列:
用ipcrm -q msqid 删除msgqid的消息队列,然后用ipcs -q 查看没有消息队列。
消息队列控制函数:
msgctl:
#include <sys/ipc.h>
#include <sys/msg.h>
int msgctl(int msqid, int cmd, struct msqid_ds *buf);
msgqid:由msgget函数返回的消息队列标识码;
cmd是将要采取的动作(有三个可取值):
IPC_STAT:把msqid_ds结构中的数据设置为消息队列的当前关联值;
IPC_SET: 在进程有足够权限的前提下,把消息队列的当前关联值设置为msqid_ds数据结构中给出的值;
IPC_RMID :删除消息队列
// 这是一个以System V 消息队列实现的聊天程序客户端
// 1.创建消息队列
// 2.从消息队列获取一个数据,并且打印出来
// 3.从标准输入获取一个数据,组织成消息队列节点发送
// 4.不玩了,删除消息队列
// 消息队列接口: msgget msgrcv msgsnd msgctl
//
//
// #include <sys/ipc.h>
// key_t ftok(const char *pathname, int proj_id);
// ftok通过文件的inode结点号和一个proj_id计算得出一个key值
// 缺点:如果一个进程将文件删除或替换,那么另一个进程打开的不是同一个文件,所以在本程序中直接给出key值
msgqueue_s.c 先接收数据再发送数据
#include<stdio.h>
#include<sys/ipc.h>
#include<sys/msg.h>
#include<errno.h>
#include<string.h>
#define IPC_KEY 0X12345678
#define TYPE_C 1 //msgtype
#define TYPE_S 2
struct msgbuf {
long mtype; /* message type, must be > 0 */
char mtext[1024]; /* message data */
};
int main()
{
int msqid=-1;
//int msgget(key_t key, int msgflg);
//key :消息队列标识符,msgflg:IPC_CREAT :不存在消息队列则创建,存在则打开
//创建消息队列
msqid=msgget(IPC_KEY,IPC_CREAT |0664);
if(msqid<0)
{
perror("msgget error");
return -1;
}
while(1)
{
//接受数据
//ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtype,int msgflg);
struct msgbuf buff;
msgrcv(msqid,&buff,1024,TYPE_C,0);
//msqid :消息队列操作句柄;
//&buff 接受数据的结构体,需要自己定义
//1024 接受数据的最大数据长度,不包含mtype
//0 如果数据长度超过指定长度不截断,如果参数是MSG_NOERROR 将会截断数据
printf("[%s]\n",buff.mtext); //将接受的数据打印
//发送数据
memset(&buff,0,sizeof(struct msgbuf));//先将数据初始化为0
buff.mtype=TYPE_S;
scanf("%s",buff.mtext);
// int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);
msgsnd(msqid,&buff,1024,0);//并不是全部发送1024字节,会发生strlen(buff)
}
//一般用ctrl+c使进程停止,没有运行到这里,也就意味着没有删除消息队列
//删除消息队列:IPC_RMID 第三个参数是删除后获取消息队列属性信息,如果不想获取可以置NULL
//int msgctl(int msqid, int cmd, struct msqid_ds *buf);
msgctl(msqid,IPC_RMID,NULL);
return 0;
}
msgqueuq_c.c 先发送数据,再接收数据
#include<stdio.h>
#include<sys/ipc.h>
#include<sys/msg.h>
#include<errno.h>
#include<string.h>
#define IPC_KEY 0X12345678
#define TYPE_C 1
#define TYPE_S 2
struct msgbuf {
long mtype; /* message type, must be > 0 */
char mtext[1024]; /* message data */
};
int main()
{
int msqid=-1;
//int msgget(key_t key, int msgflg);
//key :消息队列标识符,msgflg:IPC_CREAT :不存在消息队列则创建,存在则打开
//创建消息队列
msqid=msgget(IPC_KEY,IPC_CREAT |0664);
if(msqid<0)
{
perror("msgget error");
return -1;
}
while(1)
{
//发送数据
struct msgbuf buff;
memset(&buff,0,sizeof(struct msgbuf));//先将数据初始化为0
buff.mtype=TYPE_C;
scanf("%s",buff.mtext);
// int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);
msgsnd(msqid,&buff,1024,0);
//接受数据
//ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp,int msgflg);
msgrcv(msqid,&buff,1024,TYPE_S,0);
//msqid :消息队列操作句柄;
//&buff 接受数据的结构体,需要自己定义
//1024 接受数据的最大数据长度,不包含mytype
//0 如果数据长度超过指定长度不截断,如果参数是MSG_NOERROR 将会截断数据
printf("[%s]\n",buff.mtext); //将接受的数据打印
}
//删除消息队列:IPC_RMID 第三个参数是删除后获取消息队列属性信息,如果不想获取可以置NULL
//int msgctl(int msqid, int cmd, struct msqid_ds *buf);
msgctl(msqid,IPC_RMID,NULL);
return 0;
}
操作系统中ipc相关命令:
ipcs : 查看ipc信息
ipcs -q :查看消息队列
ipcs -m :查看共享内存
ipcs -s :查看信号量
ipcrm : 删除ipc
ipcrm -q msqid 删除指定的消息队列
ipcrm -m msqid 删除指定的共享内存
ipcrm -s msqid 删除指定的信号量
共享内存(进程间最快通信)
一般数据操作过程把数据从用户态拷贝到内核态,用的时候,再将内核态拷贝到用户态,但共享内存不需要这两步,对虚拟地址空间的操作也就是操作了物理内存,那么另一个虚拟地址空间也可以有这个数据,即不需要拷贝。
因为共享内存直接申请一块物理内存通过页表映射到虚拟地址空间中,操作虚拟地址空间,其实是操作同一块物理内存区域,因此进行数据传输时相较于其他通信方 式,少了两步用户态与内核态数据拷贝的过程,因此共享内存是最快的进程间通信方式。
共享内存使用步骤:
1.创建共享内存
2.将共享内存映射到虚拟地址空间
3.内存的数据操作(数据拷贝)
4.解除映射,删除共享内存(如果有进程依然与共享内存保持映射连接关系,那么共享内存将不会被立即删 除,而是等最后一个映射断开后删除,在这期间,将拒绝其他进程映射)
1.创建:
shmget:
#include <sys/ipc.h>
#include <sys/shm.h>
int shmget(key_t key, size_t size, int shmflg);
key :操作系统上ipc标识,这个共享内存段名字
size : 要创建共享内存大小
shmflg:
IPC_CREAT | IPC _EXCL |0664
返回值:成功: 操作句柄 失败: -1
2.映射到虚拟地址空间
#include <sys/shm.h>
void *shmat(int shmid, const void *shmaddr, int shmflg);
shmid: 操作句柄
shmaddr: 映射起始地址,NULL(随机选择操作系统分配)
shmflg: SHM_RDONLY 只读,其他参数(0)为读写
返回值:映射的虚拟地址空间首地址
失败返回-1
3.解除映射:
int shmdt(const void shmddr);
shmddr; 由shmat返回的指针,共享内存的映射首地址
返回值: 成功: 0,失败-1
删除:
#include <sys/ipc.h>
#include <sys/shm.h>
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
shmid: 操作句柄
cmd: IPC_RMID 删除(共享内存映射链接数为0,才会删除)
buf: 用于接受共享内存描述信息,不关系可以置空
删除一个共享内存时,如果这个共享内存依然与其他进程有映射连接,这个时候共享内存不会被直接删除,而是等到所有进程都与这个共享内存解除映射关系才会删除
//这是一个基于共享内存的进程间聊天程序
// 共享内存操作步骤:
// 1.创建共享内存
// 2.将共享内存映射到虚拟地址空间
// 3.内存的数据操作(数据拷贝)
// 4.解除映射,删除共享内存(如果有进程依然与共享内存保持映射连接关系,那么共享内存将不会被立即删除,>而是等最后一个映射断开后删除,在这期间,将拒绝其他进程映射)
//写数据
#include<stdio.h>
#include<sys/ipc.h>
#include<sys/shm.h>
#include<errno.h>
#include<string.h>
#define SHM_KEY 0x12345678
int main()
{
int shmid=-1;
//创建共享内存
//int shmget(key_t key, size_t size, int shmflg);
shmid=shmget(SHM_KEY,32,IPC_CREAT | 0664);
if(shmid<0)
if(shmid<0)
{
perror("shmget errnor");
return -1;
}
//建立映射关系
// void *shmat(int shmid, const void *shmaddr, int shmflg);
void *shm_start=shmat(shmid,NULL,0); // 0是读写 返回的是虚拟地址空间首地址 ,失败返回-1
if(shm_start<0)
{
perror("shmat error");
return -1;
}
//向内存写内容
while(1)
{
printf("please input:");
fflush(stdout);
memset(shm_start,0,32);
scanf("%s",(char *)shm_start);
sleep(3);
}
//解除映射
//int shmdt(const void shmddr);//由shmat返回的指针
shmdt(shm_start);
//删除(共享内存映射连接数为0)
//int shmctl(int shmid, int cmd, struct shmid_ds *buf);
shmctl(shmid,IPC_RMID,NULL);
return 0;
}
//读数据
#include<stdio.h>
#include<sys/ipc.h>
#include<sys/shm.h>
#include<errno.h>
#include<string.h>
#define SHM_KEY 0x12345678
int main()
{
int shmid=-1;
//创建共享内存
//int shmget(key_t key, size_t size, int shmflg);
shmid=shmget(SHM_KEY,32,IPC_CREAT | 0664);
if(shmid<0)
{
perror("shmget errnor");
return -1;
}
//建立映射关系
// void *shmat(int shmid, const void *shmaddr, int shmflg);
void *shm_start=shmat(shmid,NULL,0); // 0是读写 返回的是虚拟地址空间首地址 ,失败返回-1
if(shm_start<0)
{
perror("shmat error");
return -1;
}
//向内存写内容
while(1)
{
printf("%s\n",(char *)shm_start);
sleep(3);
}
//解除映射
//int shmdt(const void shmddr);//由shmat返回的指针
shmdt(shm_start);
//删除(共享内存映射连接数为0)
//int shmctl(int shmid, int cmd, struct shmid_ds *buf);
shmctl(shmid,IPC_RMID,NULL);
return 0;
}
注意:scanf和printf循环语句里sleep()时最好保持一样
信号量:(本质:具有一个等待队列的计数器(现在是否有资源可以使用))
进程间通信方式之一,用于实现进程间的同步与互斥(进程与线程安全概念)
同步:保证对临界资源访问的时序可控性(保证有序);
互斥:对临界资源同一时间的唯一访问性(保证安全)。
多个进程同时,操作一个临界资源的时候就需要通过同步与互斥机制开实现对临界资源的安全访问;
当信号量没有资源可用(计数器为0)时,这时需要阻塞等待 ;
同步:只有信号量资源计数从0变为1的时候,会通知别人打断阻塞等待,去操作临界资源,也就是释放了资源(计数器+1)之后才能获取资源(计数器-1),然后进行操作;
互斥:信号量如果想要实现互斥,那么它的计数器只能是0/1(一元信号量),我获取的计数器的资源,那么别人就无法获取。
P操作:获取信号量资源及计数器-1操作,如果计数器为0,需要等待别人释放资源。
V操作:释放信号量资源及计数器+1操作;
进程在操作临界资源之前要先获取信号量,判断是否可以对临界资源进行操作,如果信号量没有资源(计数器为0),则需要等待,当别人释放信号量资源后信号量计数变为1,则会唤醒等待的进程去重新获取信号量资源。
信号量计数器大于0,代表信号量有资源,可以操作;
信号量计数器等于0,代表信号量没有资源,需要等待。
信号量作为进程间通信方式,意味着大家都能访问到信号量,信号量实际也是一个临界资源,当然信号量的这个临界资源的操作是不会出问题的,因为信号量的操作是一个原子操作。
信号量操作步骤:
创建信号量:
semget
#include <sys/ipc.h>
#include <sys/sem.h>
int semget(key_t key, int nsems, int semflg);
key: 信号集名字
nsems: 指定一次要创建多少个信号量
semflg: IPC_CREAT |0644 和mode使用方法一样
返回值:成功:信号集标识码 失败 :-1
system V标准的信号量可以一次创建一个集合(可以包含多个信号量的同时创建)
设置信号量初值:
semctl:
#include <sys/ipc.h>
#include <sys/sem.h>
int semctl(int semid, int semnum, int cmd, ...);
semid : 由semget返回的信号集标识码
semnum: 指定要操作第几个信号量(从0开始)
cmd : 具体的操作
SETVAL :设置单个信号量的初值
SETALL: 设置所有的信号量的初值(semnum将会被忽略)
IPC_RMID :删除信号集
...:第四个参数,是一个不定参数,比如要获取信号量的信息,那么第四个参数就是结构体;比如要设置信号量的值,那么第四个参数就是值的联合体
设置初值:(联合体)
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
(Linux-specific) */
};
在对临界资源操作之前要先获取信号量 (计数器-1)
对临界资源操作完毕后要释放信号量(计数器+1)
访问信号量
#include <sys/ipc.h>
#include <sys/sem.h>
int semop(int semid, struct sembuf *sops, unsigned nsops);
semid: semget返回值;
spos: 是一个指向结构体的指针;结构体如下:
struct sembuf{
unsigned short sem_num; /* semaphore number */
short sem_op; /* semaphore operation */
short sem_flg; /* operation flags */
}
sem_num:信号量编号
sem_op:信号量一次pv操作时加减的数值,-1 即p操作,获取资源,+1 即v操作,释放资源
sem_flag:IPC_NOWAIT:退出,没有释放资源;IPC_UNDO :退出,会释放资源
下面两个代码分别利用信号的互斥和同步实现代码
//这是一个基于信号量的互斥实现代:码
// 让一个进程打印A睡1000ms然后再打印一个A
// 另一个进程打印B睡1000ms然后再打印一个B
// 查看结果是否连续
// 如何让结果是我们预期的AA BB这种形式,关键在于两个进程的打印操作不能被打断,
// 这时需要一个一元信号量来完成互斥操作
#include<stdio.h>
#include<unistd.h>
#include<errno.h>
#include<string.h>
#include<sys/ipc.h>
#include<sys/sem.h>
#define SEM_KEY 0x12345678
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
(Linux-specific) */
};
//struct sembuf{
// unsigned short sem_num; /* semaphore number */
// short sem_op; /* semaphore operation */
// short sem_flg; /* operation flags */
// } //semop第二个参数
void sem_p(int semid)//获取资源
{
struct sembuf buf;
buf.sem_num=0;
buf.sem_op=-1;
buf.sem_flg=SEM_UNDO;
// int semop(int semid, struct sembuf *sops, unsigned nsops);
semop(semid,&buf,1);
}
void sem_v(int semid)//释放资源
{
struct sembuf buf;
buf.sem_num=0;
buf.sem_op=+1;
buf.sem_flg=SEM_UNDO;
// int semop(int semid, struct sembuf *sops, unsigned nsops);
semop(semid,&buf,1);
}
int main()
{
//创建信号量
int semid=-1;
// int semget(key_t key, int nsems, int semflg);
semid=semget(SEM_KEY,1,IPC_CREAT | 0664);
if(semid<0)
{
perror("semget error");
return -1;
}
//设置信号量初值:
//只能设置一次,不能重复设置,并且在创建子进程之前
union semun val;
val.val=1; //将信号量计数器设置为1
// int semctl(int semid, int semnum, int cmd, ...);
semctl(semid,0,SETVAL,val);//由于要设置信号量初值,semctl第四个参数为联合体,信号量从0开始
int pid=-1;
pid=fork();
if(pid<0)
{
perror("fork error");
return -1;
}
else if(pid==0)
else if(pid==0)
{
//子进程 打印A
while(1)
{
//获取信号量资源
sem_p(semid);
//对于一元信号量,当这个进程获取信号量之后,那么另一个进程将获取不到信号量,会等待即在释放信号量之前,此进程的临界操作不会被打断
printf("A");
fflush(stdout);
usleep(1000);//如果没有信号量,子进程睡了1000us,父进程可能会打印B
printf("A ");
fflush(stdout);
//释放信号量资源
sem_v(semid);
}
}
else
{
//父进程 打印B
while(1)
{
//获取信号量资源
sem_p(semid);
printf("B");
fflush(stdout);
usleep(1000);
printf("B ");
fflush(stdout);
//释放信号量资源
sem_v(semid);
}
}
return 0;
}
//这是一个基于信号量同步的实现代码
//只有生产了面,才有资源吃面
#include<stdio.h>
#include<unistd.h>
#include<errno.h>
#include<string.h>
#include<sys/ipc.h>
#include<sys/sem.h>
#define SEM_KEY 0x12345678
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
(Linux-specific) */
};
//struct sembuf{
// unsigned short sem_num; /* semaphore number */
// short sem_op; /* semaphore operation */
// short sem_flg; /* operation flags */
// } //semop第二个参数
void sem_p(int semid)//获取资源
{
struct sembuf buf;
buf.sem_num=0;
buf.sem_op=-1;
buf.sem_flg=SEM_UNDO;
// int semop(int semid, struct sembuf *sops, unsigned nsops);
semop(semid,&buf,1);
}
void sem_v(int semid)//释放资源
{
struct sembuf buf;
buf.sem_num=0;
buf.sem_op=+1;
buf.sem_flg=SEM_UNDO;
// int semop(int semid, struct sembuf *sops, unsigned nsops);
semop(semid,&buf,1);
}
int main()
{
//创建信号量
int semid=-1;
semid=semget(SEM_KEY,1,IPC_CREAT | 0664);
if(semid<0)
{
perror("semget error");
return -1;
}
//设置信号量初值:
union semun val;
val.val=0; //将信号量计数器设置为0,即没有资源可以使用
// int semctl(int semid, int semnum, int cmd, ...);
semctl(semid,0,SETVAL,val);//由于要设置信号量初值,semctl第四个参数为联合体,信号量从0开始
int pid=-1;
pid=fork();
if(pid<0)
{
perror("fork error");
return -1;
}
else if(pid==0)
{
while(1)
{
sem_p(semid); //吃面之前先获取资源,如果没有资源应该等待
printf("吃了一包面\n");
}
}
else
{
while(1)
{
sleep(1);
printf("生产一包面\n");
sem_v(semid); //生产完面释放资源,即此时有资源可以使用,唤醒所有等待的进程
}
}
return 0;
}