linux编程学习 04 进程间通信
进程间通信概述
- 数据传输:一个进程需要将它的数据发送给另一个进程,发送的数据量在一个字节到几兆字节之间
- 共享数据:多个进程想要操作共享数据,一个进程对共享数据的修改,别的进程应该立刻看到
- 通知事件:一个进程需要向另一个或一组进程发送消息,通知他们发生了某种事件(如进程终止时要通知父进程)
- 资源共享:多个进程之间共享同样的资源。为了做到这一点,需要内核提供锁和同步机制。
- 进程控制:有些进程希望完全控制另一个进程的执行(如debug进程),此时控制进程希望能够拦截另一个进程的所有陷入和异常,并能知道他的状态和改变
进程间通信的变革
linux进程间通信(IPC)由以下几个部分发展而来
- 早期unix进程间通信
- 基于System V进程间通信
- 基于socket进程间通信和POSIX进程间通信
- unix进程间通信的方式包括:管道,FIFO,信号
- System V进程间通信方式包括:System V消息队列,System V信号灯,System V共享内存
- POSIX进程间通信包括:POSIX消息队列,POSIX信号灯,POSIX共享内存
现代的进程间通信的方式
- 管道(pipe)和命名管道(FIFO)
- 信号(signal)
- 消息队列
- 共享内存
- 信号量
- 套接字(socket)
管道通信
- 管道是针对于本地计算机的两个进程之间的通信而设计的通信方法,管道建立后,实际获得两个文件描述符:一个用于读取而另一个用于写入
- 最常见的IPC通信机制,通过pipe系统调用
- 管道是单工的,数据只能向一个方向流动,需要双向通信时,需要建立起两个管道
- 数据的读出和写入:一个进程向管道中写的内容被管道另一端的进程读出。写入的内容每次都添加在管道缓冲区的末尾,并且每次都是从缓冲区头部读出数据
- 本质上是在内核中的一个缓存
管道分类
匿名管道
- 在关系进程中进行(父进程和子进程,兄弟进程之间)
- 由pipe系统调用,管道由父进程建立
- 管道位于内核空间,其实是一块缓存
命名管道
- 两个没有任何关系的进程之间通信可通过命名管道进行数据传输,本质是内核中的一块缓存,另在文件系统中以一个特殊的设备文件(管道文件)存在
- 通过系统调用mkfifo创建
匿名管道创建
int pipe(int fp[2]);
- 返回:成功返回0,出错返回-1
- 头文件:
#include <unistd.h>
两个文件描述符数组
- fd[0]:为pipe的读端
- fd[1]:为pipe的写端
- fd[0]用于读取管道,fd[1]用于写入管道
管道读写
管道主要用于不同进程间通信,实际上,通常先创建一个管道,再通过fork函数创建一个子进程
示例
基本使用
int main(void)
{
int fp[2];
if(pipe(fp) == -1)
{
perror("create pipe error");
return 1;
}
int pid = fork();
int i = 0;
if(pid == -1)
{
perror("fork error");
return 1;
}
else if(pid > 0)
{
// 父进程写入,先关闭管道读端
close(fp[0]);
i = 8;
if(sizeof(int) != write(fp[1],(void*)&i,sizeof(int)))
{
perror("first write error");
return 1;
}
i = 88;
if(sizeof(int) != write(fp[1],(void*)&i,sizeof(int)))
{
perror("second write error");
return 1;
}
wait(NULL);
close(fp[1]);
}
else if(pid == 0)
{
// 子进程读,先关闭管道写端
close(fp[1]);
int j = 0;
if(-1 == read(fp[0],(void*)&j,sizeof(int)))
{
perror("first read error");
return 1;
}
printf("first read;i = %d\n",j);
if(-1 == read(fp[0],(void*)&j,sizeof(int)))
{
perror("second read error");
return 1;
}
printf("second read;i = %d\n",j);
close(fp[0]);
}
return 0;
}
达到 cat /etc/passwd | grep root
的效果
#include <stdio.h>
#include <unistd.h>
char* command1[] = {"cat","/etc/passwd",NULL};
char* command2[] = {"grep","root",NULL};
int main(void)
{
int fp[2];
// 备份标准输出
int stdout_copy = dup(STDOUT_FILENO);
if(-1 == pipe(fp))
{
perror("pipe error");
return 1;
}
int pid = fork();
if(pid == -1)
{
perror("first fork error");
return 1;
}
else if(pid == 0)// 读取内容进程
{
// 关闭读端
close(fp[0]);
// cat命令默认从标准输出读取数据,使用dup2命令把标准输出转换成管道的输入端
dup2(fp[1],STDOUT_FILENO);
close(fp[1]);
if(execvp(command1[0],command1) == -1)
{
// 显示出错信息前先把标准输出还原
dup2(stdout_copy,STDOUT_FILENO);
printf("fail to execute %s",command1[0]);
perror("\n");
return 1;
}
}
else if(pid > 0)
{
pid = fork();
if(pid == -1)
{
perror("second fork error");
return 1;
}
else if(pid == 0)// 过滤内容进程
{
// 关闭写端
close(fp[1]);
// 和上面进程一样,把标准输入转换成管道出口
dup2(fp[0],STDIN_FILENO);
close(fp[0]);
if(execvp(command2[0],command2) == -1)
{
dup2(stdout_copy,STDOUT_FILENO);
printf("fail to execute %s",command2[0]);
perror("\n");
return 1;
}
}
else// 主进程
{
// 回收进程资源
printf("\n");
wait(NULL);
// 关闭
close(fp[0]);
close(fp[1]);
}
}
return 0;
}
管道的读写特性
- 通过打开两个管道来创建一个双向的管道
- 管道是阻塞性的,当进程从管道中读取数据,若没有数据进程会阻塞
- 当一个进程往管道中不断写入数据但是没有进程去读取数据,此时只要管道没有满是可以的,但若管道放满数据则会报错
不完整管道
- 当读一个写端已经被关闭的管道时,在所有数据被读取后,read返回0,以表示到达了文件末尾
- 如果写一个读端已被关闭的管道,则产生信号SIGPIPE,如果忽略该信号或捕捉该信号并从处理程序返回,则write返回-1,同时errno设置为EPIPE
- 示例
#include <stdio.h>
#include <unistd.h>
#include <signal.h>
void func1(void);
void func2(void);
int main(void)
{
func2();
return 0;
}
/* 读取写端被关闭的管道 */
void func1(void)
{
int fp[2];
if(-1 == pipe(fp))
{
return;
}
int pid = fork();
if(pid == -1)
{
perror("fork error");
return;
}
else if(pid == 0)
{
close(fp[0]);
write(fp[1],"I am child precess\n",19);
close(fp[1]);
}
else
{
sleep(3);
close(fp[1]);
char c = '\0';
while(1)
{
// 写端被关闭,读完管道中的数据后返回0,不会阻塞
if(0 == read(fp[0],&c,1))
{
break;
}
printf("%c",c);
}
close(fp[0]);
}
}
void sigpipe_handle(int sig)
{
printf("I am sigpipe_handle\n");
return;
}
/* 往读端被关闭的管道写入数据 */
void func2(void)
{
int fp[2];
pipe(fp);
close(fp[1]);
signal(SIGPIPE,sigpipe_handle);
if(-1 == write(fp[0],"8",1))
{
perror("write pipe error");
return;
}
close(fp[0]);
}
标准库中的管道操作
FILE* popen(const char* cmdstring,const char* type);
- 返回值:成功返回文件指针,出错返回NULL
int pclose(FILE *fp);
- 返回值:cmdstring的终止状态,出错返回-1
- 头文件
#include <stdio.h>
- 使用popen创建的管道必须使用pclose关闭。其实popen/pclose和标准文件输入/输出流中的fopen/fclose十分相似
- 封装管道的常用操作
- 示例
int main(void)
{
// 读
FILE *f = popen("cat /etc/passwd","r");
char str[1024] = {'\0'};
while(NULL != fgets(str,1024,f))
{
// printf("%s",str);
}
pclose(f);
// 写
f = popen("wc -l","w");
fprintf(f,"/etc/passwd\n/etc/shadow");
pclose(f);
return 0;
}
命名管道FIFO创建
int mkfifo(const char* pathname,mmode_t mode);
- 返回:成功返回0,出错返回-1
- 头文件
#include <sys/types.h>
#include <sys/stat.h>
- 只要对FIFO有适当访问权限,FIFO可用在任何两个没有任何关系的进程之间通信。
- 本质是内核的一块缓存,另外在文件系统中以一个特殊的设备文件(管道文件)存在
- 在文件系统中只有一个索引块存放文件的路径,没有数据块,所有数据存放在内核中。
- 命名管道必须读和写同时打开,否则单独读或者单独写会引发阻塞
- 命名mkfifo创建命名管道(命令内部调用mkfifo函数)
- 对FIFO的操作与操作普通文件一样
- 一旦已经用mkfifo创建了一个FIFO,就可以用open打开它,一般文件I/O函数(open,close,write,read,unlink等)都可用于FIFO
FIFO相关出错信息
- EACCES(无存取权限)
- EEXIST(指定文件不存在)
- ENAMETOOLONG(路径名太长)
- ENOENT(包含的目录不存在)
- ENOSPC(文件系统剩余空间不足)
- ENOTDIR(文件路径无效)
- EROFS(指定文件存在于只读文件系统中)
- 示例
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <string.h>
#include <fcntl.h>
#include <unistd.h>
int main(int argc,char* argv[])
{
if(argc < 2)
{
printf("incorret parameter\n");
return 1;
}
// 创建管道文件
char *path = "/tmp/fifo_test";
mkfifo("/tmp/fifo_test",0777);
int f = open(path,O_RDWR);
char str[8] = {'\0'};
// 可以启动两个,一个读取一个写入
if(strcmp("w",argv[1]) == 0)
{
scanf("%s",str);
write(f,str,8);
}
else if(strcmp("r",argv[1]) == 0)
{
read(f,str,8);
printf("str = %s\n",str);
}
close(f);
return 0;
}
System V IPC
概述
- unix系统存在信号,管道和命名通道等基本进程间通信机制
System V引入了三种高级进程间通信机制
- 消息队列,共享内存和信号量
- IPC对象(消息队列,共享内存和信号量)存在于内核中而不是文件系统中,由用户控制释放(用户管理IPC对象的生命周期),不像管道的释放由内核控制
- IPC对象通过其标识符来引用和访问,所有IPC对象在内核空间中有唯一性标识,在用户空间中的唯一性标识称为key
- linux IPC继承自System V IPC
System V IPC对象访问
IPC对象是全局对象
- 可用ipcs,ipcrm等命令查看或删除
每个IPC对象都由get函数创建
- msgget,shmget,semget
- 调用get函数时必须指定关键字key
IPC对象的权限和所有者结构体
struct ipc_perm
{
uid_t uid;// owner's effective user id
gid_t gid;// owner's effective group id
uid_t cuid;// creator's effective user id
gid_t cgid;// creator's effective group id
mode_t mode;// access mode
......
};
消息队列
- 消息队列是内核中的一个链表
- 用户进程将数据传输到内核后,内核重现添加一些如用户id,组id,读写进程的id和优先级等相关信息后并打包成一个数据包称为消息
- 允许一个或多个进程往消息队列中写消息读消息,但一个消息只能被一个进程读取,读取完毕后就自动删除
- 消息队列具有一定的FIFO的特性,消息可以按照顺序发送到队列中,也可以以几种不同的方式从队列中读取。每一个消息队列在内核中用一个唯一的IPC标识id表示。
- 消息队列的实现包括创建和打开队列,发送消息,读取消息和控制消息队列四种操作
消息队列属性
struct msqid_ds{
struct ipc_perm msg_perm;
msgqnum_t msg_qnum;// of message on queue
msglen_t msg_qbytes;// max of bytes on queue 最大消息字节数
pid_t msg_lspid;// pid of last msgsnd() 最后一次发送消息的进程的pid
pid_t msg_lrpid;// pid of last msgrcv() 最后一次接受消息的进程的pid
time_t msg_stime;// last msgsnd time 最后发送消息的时间
time_t msg_ctime;// last change time 最后一次改变的时间
......
};
打开或创建消息队列
int msgget(key_t key,int flag);
- 返回:成功返回内核中消息队列的表示id,出错返回-1
- 头文件
#include <sys/msg.h>
参数
- key:用户指定的消息队列的键值
- flag:IPC_CREAT,IPC_EXCL等权限组合
- 若创建消息队列,key可指定键值,也可将之设置为IPC_PRIVATE。若打开进行查询,则key不能为0必须是一个非0的值否则是查询不到的
消息队列控制
int msgctl(int msgid,int cmd,struct msqid_ds *buf)
- 返回:成功返回0,出错返回-1
- 头文件
#include <sys/msg.h>
参数
- msgid:消息队列的id
- buf:消息队列属性指针
cmd:
- IPC_STAT:获取消息队列属性,取此队列的msqid_ds结构,并将其存放在buf指向的结构体中
- IPC_SET:设置属性,按由buf指向的结构体中的值,设置此队列相关的结构中的字段
- IPC_RMID:删除队列,从系统中删除该消息队列以及仍在该队列上的所有数据
发送消息
int msgsnd(int msgqid,const void* ptr,size_t nbytes,int flag);
- 返回:成功返回0,出错返回-1
- ptr 结构
struct mymesg{
long mytype; // positive message type
char mtext[512]; // message data of length nbytes
...
};
- nbytes:指定消息的大小,不包括mtype的大小
- mtype是指消息的类型,他由一个整数来代表,并且他只可能是大于0的整数
- mtext是消息数据本身
- 在linux中消息的最大长度是4056个字节,其中包括mtype,他占四个字节
- 结构体mymesg用户可自定义,但第一个成员必须是mtype
参数flag
- 0:阻塞
- IPC_NOWAIT:类似于文件I/O的非阻塞I/O标志
若消息队列已满(或是队列中消息总数等于系统限制值,或队列中的字节总数等于系统限制值),则指定IPC_NOWAIT使得msgsnd立即出错返回EAGAIN。如果指定0,则进程
- 阻塞直到有空间可以容纳要发送的消息
- 或从系统中删除了此队列
- 或捕捉到了一个信号,并从信号处理程序返回
接受消息
size_t msgrcv(int msgqid,void* ptr,size_t nbytes,long type,int flag);
- 返回:成功返回消息的数据部分长度,出错返回-1
- 头文件
#include <sys/msg.h>
参数
- msgqid:消息队列id
- ptr:指向存放消息的缓存
nbytes:消息缓存的大小,不包括mtype的大小。计算方式
- nbytes = sizeof(struct mymesg) - sizeof(long)
type:消息类型
- type == 0:获取队列消息中的第一个消息
- type > 0:获取消息队列中类型为type的第一个消息
- type < 0:获取消息队列中类型小于或等于type绝对值的消息(有多个就取类型最小的一个)
- flag:0或IPC_NOWAIT
消息示例
#include <sys/msg.h>
#include <stdio.h>
#include <string.h>
void send_message(int);
void receive_message(int);
/* 消息结构体,注意第一个元素必须是type */
typedef struct{
long type; // 必须大于1,否则发送消息失败
int id;
char name[8];
}my_message;
int main(int argc,char* argv[])
{
if(argc < 3)
{
printf("incorrect parameter\n");
return 1;
}
// 可以用ftok生成key,注意argv[2]必须是一个实际存在的路径,不然函数返回-1
// key_t k = ftok(argv[2],0);
// 直接使用数字
key_t k = atoi(argv[2]);
// 打开/创建消息队列
int msgqid = 0;
if((msgqid = msgget(k,IPC_CREAT | 0777)) == -1)
{
perror("msgget error");
return 1;
}
if(strcmp("send",argv[1]) == 0)
{
send_message(msgqid);
}
else if(strcmp("receive",argv[1]) == 0)
{
receive_message(msgqid);
}
else
{
printf("incorrect parameter\n");
return 1;
}
// 删除队列,程序结束后消息队列还是会存在,不用一定要接的删除
if(msgctl(msgqid,IPC_RMID,NULL) == -1)
{
perror("remove message queue error");
return 1;
}
return 0;
}
/* 发送消息方法 */
void send_message(int msgqid)
{
my_message m = {0,0,{'\0'}};
int type = 1;
// 消息的大小,注意要减去type的大小
size_t bytes = sizeof(my_message) - sizeof(long);
struct msqid_ds ds;
int c;
while(1)
{
printf("input id name\n");
scanf("%d %7s",&m.id,m.name);
m.type = type;
if(msgsnd(msgqid,(void*)&m,bytes,0) == -1)
{
perror("msgsnd error");
break;
}
type++;
if(-1 == msgctl(msgqid,IPC_STAT,&ds))
{
perror("msgctl error");
}
else
{
printf("msgqnum = %u\n",(unsigned int)ds.msg_qnum);
}
// 重置name字符数组
memset(m.name,'\0',8);
// 清空标准输入的缓冲区的内容
while ((c=getchar()) != '\n' && c!=EOF);
}
}
/* 接受消息方法 */
void receive_message(int msgqid)
{
my_message m;
// 消息的大小,注意要减去type的大小
size_t bytes = sizeof(my_message) - sizeof(long);
struct msqid_ds ds;
while(1)
{
if(msgrcv(msgqid,(void*)&m,bytes,0,0) == -1)
{
perror("msgrcv error");
break;
}
else
{
printf("type = %d;id = %d;name = %s\n",(int)m.type,m.id,m.name);
}
if(-1 == msgctl(msgqid,IPC_STAT,&ds))
{
perror("msgctl error");
}
else
{
printf("msgqnum = %u\n",(unsigned int)ds.msg_qnum);
}
}
}
共享内存
- 共享内存区域是被多个进程共享的一部分物理内存
- 多个进程都可把该共享内存映射到自己的虚拟内存空间。所有用户空间的进程若要操作共享内存,都要将其映射到自己的虚拟内存空间中,通过映射的虚拟内存空间地址去操作共享内存,从而达到进程间的数据通信
- 共享内存是进程间共享数据的一种最快的方法,一个进程向内存区域写入数据,共享这个区域的所有内存就可以立刻看到其中的内容
- 本身不提供同步机制,可通过信号量进行同步
- 提高数据处理效率,一种效率最高的IPC机制
共享内存属性
struct shmid_ds{
struct ipc_perm shm_perm;
size_t shm_segsz;// size of segment in bytes 共享内存区大小
pid_t shm_lpid;// pid of last shmop 最后一次操作共享内存的进程id
pid_t shm_cpid;// pid of creator 创建共享内存的进程的id
shmatt_t shm_nattch;// number of current attaches 和当前共享内存成功映射的内存数量
time_t shm_atime;// last attach time 最后成功映射的时间
time_t shm_dtime;// last detach time 最后解除映射的时间
time_t shm_ctime;// last change time 最后一次改变的时间
......
};
共享内存使用步骤
- 使用shmget函数创建共享内存
- 使用shmat函数映射共享内存,将这段创建的共享内存映射到具体的进程虚拟空间中
创建共享内存
int shmget(key_t key,size_t size,int shmflg);
- 返回:成功返回内核中共享内存的标识id,失败返回-1
- 头文件
#include <sys/shm.h>
参数
- key:用户指定的共享内存键值
- size:共享内存大小
- shmflg:IPC_CREAT,IPC_EXCL等权限组合
errno
- EINVAL(无效的内存段大小)
- EEXIST(内存段已经存在,无法创建)
- EIDRM(内存段已经被删除)
- ENOENT(内存段不存在)
- EACCES(权限不够)
- ENOMEM(没有足够的内存来创建内存段)
共享内存控制
int shmctl(int shmid,int cmd,struct shmid_ds* buf);
- 返回:成功返回0,出错返回-1
- 头文件
#include <sys/shm.h>
参数
- shmid:共享内存id
- buf:共享内存属性指针
cmd
- IPC_STAT 获取共享内存段属性
- IPC_SET 设置共享内存段属性
- IPC_RMID 删除共享内存段
- SHM_LOCK 锁定共享内存段页面(页面映射到的物理内存不和外存(swap)进行换入换出操作)
- SHM_UNLOCK 解除共享内存段页面的锁定
共享内存映射和接触映射
void* shmat(int shmid,char* shmaddr,int shmflg);
- 返回:成功返回共享内存映射到虚拟内存的空间的地址,失败返回-1
int shmdt(char* shmaddr);
- 返回:失败返回-1
参数
- shmid:共享内存id
- shmaddr:映射到进程虚拟内存空间的地址,建议设置0,有操作系统分配
shmflg:若shmaddr设置成0,则shmflg也设置成0
- SHM_RND
- SHMLBA 地址为2的乘方
- SHM_RDONLY 只读方式链接
errno
- EINVAL 无效的IPC ID值或无效的地址
- ENOMEM 没有足够的内存
- EACCES 存取权限不够
子进程不继承父进程创建的共享内存,大家的是共享的。子进程继承父进程映射的地址
示例
#include <stdio.h>
#include <sys/shm.h>
#include <unistd.h>
// 存放pipe,用来使进程互斥
int fp[2];
int init_pipe(void);
void close_pipe(void);
void sleep_pipe(void);
void wake_pipe(void);
int main(void)
{
// 创建虚拟内存块
key_t k = 888;
int shmid;
if((shmid = shmget(k,sizeof(int),IPC_CREAT | 0777)) == -1)
{
perror("shmget error");
return 1;
}
// 映射共享内存,可直接在fork映射也可以在fork后映射
/*
int* share_p = shmat(shmid,NULL,0);
if((int)share_p == -1)
{
perror("shmat error");
return 1;
}
*/
// 初始化pipe
init_pipe();
int pid = fork();
if(pid == -1)
{
perror("fork error");
return 1;
}
if(pid > 0)
{
// 映射共享内存
int* share_p = shmat(shmid,NULL,0);
if((int)share_p == -1)
{
perror("shmat error");
return 1;
}
*share_p = 8;
wake_pipe();
wait(NULL);
// 接触共享内存映射
shmdt(share_p);
// 删除共享内存
shmctl(shmid,IPC_RMID,NULL);
}
else if(pid == 0)
{
// 映射共享内存
int* share_p = shmat(shmid,NULL,0);
if((int)share_p == -1)
{
perror("shmat error");
return 1;
}
sleep_pipe();
printf("*share_p = %d\n",*share_p);
// 接触共享内存映射
shmdt(share_p);
}
// 关闭pipe
close_pipe();
return 0;
}
int init_pipe(void)
{
return pipe(fp);
}
void close_pipe(void)
{
close(fp[0]);
close(fp[1]);
}
void sleep_pipe(void)
{
char c;
read(fp[0],(void*)&c,sizeof(c));
}
void wake_pipe(void)
{
char c = 'D';
write(fp[1],(void*)&c,sizeof(c));
}
信号量
- 本质上就是共享资源的数目,用来控制对共享资源的访问
- 用于进程间的互斥和同步
- 每种共享资源对应一个信号量,为了便于大量共享资源的操作引入了信号集,可对所有信号量一次性操作。对信号量集中多有操作可以要求全部成功也可以部分成功
- 二元信号量(信号灯)值为0和1
- 对信号量作PV操作
信号量集属性
#include <sys/sem.h>
struct semid_ds{
struct ipc_perm sem_perm;
unsigned short sem_nsems;// of semaphores in set 信号量集中灯的数量
time_t sem_otime; // last semop time 最后一次对信号量操作的时间
time_t sem_ctime;// last change time 最后一次改变时间
};
创建信号量集
int semget(key_t key,int nsems,int flag);
- 返回:成功返回信号量集id,出错返回-1
参数
- key:用户指定的信号量集键值
- nsems:信号量集中信号量个数
- flag:IPC_CREAT,IPC_EXCL等权限组合
- 头文件
#include <sys/sem.h>
信号量集控制
int semctl(int semid,int semnum,int cmd,.../* union semun arg */);
- union semun结构
// 需要自己定义
union semun{
int val;
struct semid_ds* buf;
unsigned short* array;
};
参数
- semid:信号量集id
- semnum:0表示对所有信号量操作,信号量编号从0开始
- val:放置获取或设置信号量集中某个信号量的值
- buf:信号量集属性指针
- array:放置获取或设置信号量集中所有信号量的值
通过cmd参数设定对信号量集要执行的操作
- IPC_STAT 获取信号量集的熟悉 ==> buf
- IPC_SET 设置信号量集的熟悉 ==> buf
- IPC_RMID 删除信号量集 ==> buf
- GETVAL 返回信号量的值 ==> val
- SETVAL 设置semnum信号量的值 ==> val
- GETALL 获取所有信号量的值 ==> array
- SETALL 设置所有信号量的初始值 ==> array
- 头文件
#include <sys/sem.h>
信号量集操作
int semop(int semid,struct sembuf* sops,size_t nsops);
- 返回:成功返回0,出错返回-1
- struct sembuf 结构体
struct sembuf{
unsigned short sem_num;// member in set 对哪一个信号量进行操作
short sem_op;// operation(negative,0,positive) 做P操作还V操作
short sem_flg;// IPC_NOWAIT,SEM_UNDO
};
参数
- semid:信号量集id
- sops:sembuf结构体数组指针
- nsops:第二个参数中结构体数组的长度
- sem_num:信号量集中信号量的编号
- sep_op:正数为V操作,复数为P操作,0可用于对共享资源是否已完成的测试
- sem_flg:SEM_UNDO标志,表示在进程结束时,相应的操作将被取消,如果设置了该标志,那么在进程没有释放共享资源就退出时,内核将代为释放
- 用于信号量集中信号量的加和减操作(PV操作)
- 可用于进程间的互斥和同步
银行取款,PV操作
#include <stdio.h>
#include <sys/shm.h>
#include <sys/sem.h>
#include <sys/types.h>
#include <unistd.h>
typedef struct
{
int deposit;
int semid;
}Account;
typedef union
{
int val;
struct semid_ds* buf;
unsigned short* array;
}my_semun;
int I(void);
void P(int semid);
void V(int semid);
void D(int semid);
void withdraw(Account* account,int amount);
int main(void)
{
key_t k = 888;
int shmid = shmget(k,sizeof(Account),IPC_CREAT | 0777);
if(shmid == -1)
{
perror("shmget error");
return 1;
}
// 映射
Account* account = shmat(shmid,0,0);
account->deposit = 888888;
account->semid = I();
V(account->semid);
int pid = fork();
if(pid == -1)
{
perror("fork error");
// 解除映射
shmdt(account);
// 删除共享内存
shmctl(shmid,IPC_RMID,NULL);
// 删除信号量集
D(account->semid);
return 1;
}
else if(pid > 0)
{
withdraw(account,888888);
wait(NULL);
// 删除信号量集
D(account->semid);
// 解除映射
shmdt(account);
// 删除共享内存
shmctl(shmid,IPC_RMID,NULL);
}
else if(pid == 0)
{
withdraw(account,888888);
// 解除映射
shmdt(account);
}
return 0;
}
void withdraw(Account* account,int amount)
{
// 进行P操作,其他进程会阻塞
P(account->semid);
if(amount <= account->deposit)
{
account->deposit -= amount;
}
printf("process %d withdraw\n",(int)getpid());
sleep(3);
// 使其他进程继续运行
V(account->semid);
}
int I(void)
{
// 创建信号量集
int semid = semget(IPC_PRIVATE,1,IPC_CREAT | 0777);
if(semid != -1)
{
my_semun val;
val.val = 0;
// 初始化信号量集
semctl(semid,0,SETALL,&val);
}
return semid;
}
void P(int semid)
{
// 使用sembuf结构体进行P操作
struct sembuf buf[1] = {{0,-1,SEM_UNDO}};
semop(semid,buf,1);
}
void V(int semid)
{
// 使用sembuf结构体进行V操作
struct sembuf buf[1] = {{0,1,SEM_UNDO}};
semop(semid,buf,1);
}
void D(int semid)
{
// 删除信号量集
semctl(semid,0,IPC_RMID,NULL);
}
读者和写者问题
#include <stdio.h>
#include <sys/shm.h>
#include <sys/sem.h>
#include <sys/types.h>
#include <unistd.h>
typedef struct
{
int c_semid,g_semid;
int* data;
}Calculate;
typedef union
{
int val;
struct semid_ds* buf;
unsigned short* array;
}my_semun;
int I(void);
void get_v(Calculate*);
void calculate_v(Calculate*);
void P(int semid);
void V(int semid);
void D(int semid);
int main(void)
{
key_t k = 888;
int shmid = shmget(k,sizeof(int),IPC_CREAT | 0777);
if(shmid == -1)
{
perror("shmget error");
return 1;
}
// 映射
Calculate c;
c.data = shmat(shmid,0,0);
c.c_semid = I();
c.g_semid = I();
int pid = fork();
if(pid == -1)
{
perror("fork error");
// 解除映射
shmdt(c.data);
// 删除共享内存
shmctl(shmid,IPC_RMID,NULL);
// 删除信号量集
D(c.c_semid);
D(c.g_semid);
return 1;
}
else if(pid > 0)
{
get_v(&c);
wait(NULL);
// 删除信号量集
D(c.c_semid);
D(c.g_semid);
// 解除映射
shmdt(c.data);
// 删除共享内存
shmctl(shmid,IPC_RMID,NULL);
}
else if(pid == 0)
{
calculate_v(&c);
// 解除映射
shmdt(c.data);
}
return 0;
}
void get_v(Calculate* c)
{
for(int i = 0;i < 100;i++)
{
printf("get; i = %d\n",*c->data);
*c->data = i;
V(c->c_semid);
P(c->g_semid);
printf("print; i = %d\n--------\n\n",*c->data);
}
}
void calculate_v(Calculate* c)
{
for(int i = 0;i < 100;i++)
{
P(c->c_semid);
*c->data += 8;
printf("calculate; i = %d\n",*c->data);
V(c->g_semid);
}
}
int I(void)
{
// 创建信号量集
int semid = semget(IPC_PRIVATE,1,IPC_CREAT | 0777);
if(semid != -1)
{
my_semun val;
val.val = 0;
// 初始化信号量集
semctl(semid,0,SETALL,&val);
}
return semid;
}
void P(int semid)
{
// 使用sembuf结构体进行P操作
struct sembuf buf[1] = {{0,-1,SEM_UNDO}};
semop(semid,buf,1);
}
void V(int semid)
{
// 使用sembuf结构体进行V操作
struct sembuf buf[1] = {{0,1,SEM_UNDO}};
semop(semid,buf,1);
}
void D(int semid)
{
// 删除信号量集
semctl(semid,0,IPC_RMID,NULL);
}