进程间通信(IPC)
基于早期UNIX进程间通信,基于System V进程间通信,基于Socket进程间通信和POSIX的进程间通讯
1. 进程间通信的概述
1.1 进程间通信的目的
数据传输,共享数据,通知事件,资源共享,进程控制。
1.2 有哪些进程间通讯方式
- 管道pipe,命名管道FIFO
- 信号siganl
- 消息队列
- 共享内存
- 信号量
- 套接字(socket)
- 文件锁(系统IO中介绍)
2. 管道通信
- 本地计算机的两个进程之间的通讯而设计的通讯方式。
- 创建管道,获得两个文件描述符,一个用于读取数据,一个用于写入数据。
- 管道是单工的,只有一个方向。
- 管道实际上是内存中的一块缓存。每次写入添加的数据都是添加在尾部。读取的进程从头部读取。
2.1 管道的分类
-
匿名管道 pipe
两个通讯的进程必须有关系,一般来说 ,管道由父进程创建。 -
命名管道 FIFO
两个没有任何关系的进程之间可以通过FIFO通讯。
2.2 pipe
2.2.1 管道pipe的创建
相关函数
#include <unistd.h>
int pipe(int pipefd[2]);
#define _GNU_SOURCE /* See feature_test_macros(7) */
#include <fcntl.h> /* Obtain O_* constant definitions */
#include <unistd.h>
int pipe2(int pipefd[2], int flags);
参考实例
#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>
int main(void)
{
int pipe_fd[2];
if(pipe(pipe_fd)<0)
{
printf("pipe create error\n");
return -1;
}
else
{
printf("pipe create success\n");
}
close(pipe_fd[0]);
close(pipe_fd[1]);
}
2.2.2 管道pipe的通讯实例
管道主要用于不同进程通信。实际上,通常先创建一个管道,再通过fork函数创建一个子进程。
子进程会复制管道的fd[0],fd[1]。两个进程必须各自关掉一个管道方向。
如下图所示,父进程关闭fd[0],子进程关闭fd[1]。
参考示例:父进程通过管道传输两个数据给子进程。
#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>
#include <string.h>
int main(void)
{
int fd[2];
//创建管道
if(pipe(fd)<0)
{
printf("pipe create error\n");
return -1;
}
pid_t pid;
if((pid = fork())<0)
{
perror("pipe error");
exit(1);
}
else if(pid>0){ //父进程 用来写入数据
close(fd[0]);
int start = 1,end =100;
if(write(fd[1],&start,sizeof(int)) != sizeof(int))
{
perror("write error\n");
exit(1);
}
if(write(fd[1],&end,sizeof(int)) != sizeof(int))
{
perror("write error\n");
exit(1);
}
close(fd[1]);
wait(0);//等待回收子进程
}
else if(pid == 0){ //子进程 用来读取数据
close(fd[1]);
int start,end;
if(read(fd[0],&start,sizeof(int))<0 ){
perror("read error\n");
exit(1);
}
if(read(fd[0],&end,sizeof(int))<0 ){
perror("read error\n");
exit(1);
}
printf("start %d end %d \n",start,end);
close(fd[0]);
}
exit(0);
}
运行结果:
kshine@kshine-virtual-machine:~/桌面/Kshine/lsd$ gcc test.c -o ttt
kshine@kshine-virtual-machine:~/桌面/Kshine/lsd$ ./ttt
start 1 end 100
kshine@kshine-virtual-machine:~/桌面/Kshine/lsd$
2.2.3 管道pipe结合execute命令执行
上图中,cat命令和grep命令相当于shell的两个子进程。
#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <wait.h>
char *cmd1[3] = {"/bin/cat","./passwd",NULL};
char *cmd2[3] = {"/bin/grep","kshine",NULL};
int main(void)
{
int fd[2];
//创建管道
if(pipe(fd)<0)
{
printf("pipe create error\n");
return -1;
}
int i=0;
pid_t pid;
for(;i<2;i++)
{
pid = fork();
if(pid<0)
{
exit(1);
}
else if(pid>0)
{
if(i==1)
{
//父进程等到子进程全部创建完毕才去回收子进程
close(fd[0]);
close(fd[1]);
wait(0);
wait(0);
}
}
else if(pid==0)
{
if(i==0){//第一个子进程负责写入数据
close(fd[0]);//关闭读端
//重定向 到写端
if(dup2(fd[1],STDOUT_FILENO) != STDOUT_FILENO)
{
perror("dup2 error");
}
close(fd[1]);//关闭写端(重定向已经做了复制)
//执行命令
if(execvp(cmd1[0],cmd1)<0){perror("execvp error");exit(1);}
break;
}
if(i==1){//第二个子进程负责读取
close(fd[1]);//关闭写端
//grep 命令默认从标准输入读取内容,再过滤。
//重定向 管道读端到标准输入
if(dup2(fd[0],STDIN_FILENO) != STDIN_FILENO)
{
perror("dup2 error");
}
close(fd[0]);//关闭读端(重定向已经做了复制)
if(execvp(cmd2[0],cmd2)<0){perror("execvp error");exit(1);}
break;
}
}
}
exit(0);
}
本地目标文件passwd的内容
运行结果:
kshine@kshine-virtual-machine:~/桌面/Kshine/lsd$ gcc test.c -o ttt
kshine@kshine-virtual-machine:~/桌面/Kshine/lsd$ ./ttt
kshine 123456
999 kshine
kshine@kshine-virtual-machine:~/桌面/Kshine/lsd$
2.3 管道的读写特性
- 使用两个管道可以实现双向的读写。
- 从管道读取数据,没有数据时,读取方会被阻塞。
- 向管道写入数据时,若管道已经满了,会报错。
- 当管道的写端被关闭,所有数据被读取后,read会返回0,表示已经读完。
- 当管道的读端被关闭,向里面写数据,会产生SIGPIPE信号。write返回-1,同时errno 为EPIPE。
2.3.1 不完整管道读写
读一个写端被关闭的管道,参考例子:
#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <wait.h>
int main(void)
{
int fd[2];
//创建管道
if(pipe(fd)<0)
{
printf("pipe create error\n");
return -1;
}
//通过父子进程,完成不完整管道的测试
pid_t pid;
pid = fork();
if(pid<0)
{
exit(1);
}
else if(pid>0)
{
//父进程 从不完整管道中(写端关闭)读取数据
sleep(1);//等待子进程写端关闭
close(fd[1]);
while(1)
{
char c;
if(read(fd[0],&c,1)==0)
{
printf("\nwrite-end of pipe closed\n");
break;
}
else
{
printf("%c",c);
}
}
close(fd[0]);
wait(0);
}
else if(pid==0)
{
//子进程负责吧数据写入管道
close(fd[0]);
char *s = "1234";
write(fd[1],s,sizeof(s));
//写入数据后,关闭管道的写端
close(fd[1]);
}
exit(0);
}
运行结果
kshine@kshine-virtual-machine:~/桌面/Kshine/lsd$ gcc test.c -o ttt
kshine@kshine-virtual-machine:~/桌面/Kshine/lsd$ ./ttt
1234
write-end of pipe closed //读取完成后,发现写端已经关闭
kshine@kshine-virtual-machine:~/桌面/Kshine/lsd$
写一个读端被关闭的管道
#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <wait.h>
#include <errno.h>
void sig_handler(int signo)
{
if(signo == SIGPIPE){
printf("SIGPIPE occured\n");
}
}
int main(void)
{
int fd[2];
//创建管道
if(pipe(fd)<0)
{
printf("pipe create error\n");
return -1;
}
//通过父子进程,完成不完整管道的测试
pid_t pid;
pid = fork();
if(pid<0)
{
exit(1);
}
else if(pid>0)
{
//父进程 向不完整管道中(读端关闭)写入数据
sleep(1);//等待子进程读端关闭
close(fd[0]);
if(signal(SIGPIPE,sig_handler) == SIG_ERR)
{
perror("signal sigpipe error");
close(fd[1]);
exit(1);
}
char *s ="1234";
if(write(fd[1],s,sizeof(s)) != sizeof(s) )
{
fprintf(stderr,"%s, %s\n",strerror(errno),(errno == EPIPE)?"EPIPE":"unknow");
}
close(fd[1]);
wait(0);
}
else if(pid==0)
{
//子进程 关闭全部管道
close(fd[0]);
close(fd[1]);
}
exit(0);
}
运行结果
kshine@kshine-virtual-machine:~/桌面/Kshine/lsd$ gcc test.c -o ttt
kshine@kshine-virtual-machine:~/桌面/Kshine/lsd$ ./ttt
SIGPIPE occured
Broken pipe, EPIPE
kshine@kshine-virtual-machine:~/桌面/Kshine/lsd$
2.4 标准库中的管道操作函数
前文的管道操作太麻烦,使用标准库函数,可以更容易的使用管道。
- 使用popen()创建的管道,必须使用pclose()关闭。
#include <stdio.h>
FILE *popen(const char *command, const char *type); //返回文件描述符,错误返回NULL
int pclose(FILE *stream);//返回终止状态,错误返回-1
参考例子
#include <stdlib.h>
#include <stdio.h>
#include <memory.h>
int main(void)
{
FILE* fp;
fp = popen("cat ./passwd","r");
char buf[512];
memset(buf,0,sizeof(buf));
while(fgets(buf,sizeof(buf),fp) != NULL){
printf("%s",buf);
}
pclose(fp);
//---------------------------------//
printf("-------------------------\n");
fp = popen("wc -l","w");//wc命令
fprintf(fp,"1\n2\n3\n");//向fp结构体缓存中写入3行数据
pclose(fp);
exit(0);
}
运行结果
kshine@kshine-virtual-machine:~/桌面/Kshine/lsd$ gcc test.c -o ttt
kshine@kshine-virtual-machine:~/桌面/Kshine/lsd$ ./ttt
kshine 123456
handsome 1111
good job
999 kshine
-------------------------
3
kshine@kshine-virtual-machine:~/桌面/Kshine/lsd$
2.5 命名管道
相关函数:(man 3 mkfifo)
#include <sys/types.h>
#include <sys/stat.h>
int mkfifo(const char *pathname, mode_t mode); //文件路径,访问权限
#include <fcntl.h> /* Definition of AT_* constants */
#include <sys/stat.h>
int mkfifoat(int dirfd, const char *pathname, mode_t mode);
- 可以在无关系的进程之间进行通信。
- 本质是内核的缓存。
- 命名管道,读写必须都打开。否则单独读或者单独写都会导致堵塞。
- 在文件系统中,只有路径没有数据块,数据在内核中。
- mkfifo也是一条shell命令
- 对fifo的操作和操作普通文件是一样的。创建一个fifo,也可以叫做创建一个管道文件。
参考实例:
- 创建命名管道
两种方式,这里直接用命令的方式创建
mkfifo s.pipe
- 从命名管道读数据(独立的代码,运行成为独立的进程)
这里的代码,就是基本的文件操作。从文件中读取数据。
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <memory.h>
#include <unistd.h>
int main(int argc, char * argv[])
{
if(argc <2 ){
printf("usage:%s fifo\n",argv[0]);
exit(1);
}
printf("open fifo read...\n");
//打开命名管道fifo
int fd = open(argv[1],O_RDONLY);
if(fd<0){
perror("open error");
exit(1);
}else{
printf("open file success");
}
//从命名管道读取数据
char buf[512];
memset(buf,0,sizeof(buf));
while(read(fd,buf,sizeof(buf))<0){
printf("read error");
}
printf("%s\n",buf);
close(fd);
exit(0);
}
- 向命名管道写数据(独立的代码,运行成为独立的进程)
这里的代码,就是基本的文件操作。向文件写入数据。
#include <unistd.h>
#include <memory.h>
#include <stdlib.h>
#include <stdio.h>
#include <fcntl.h>
int main(int argc,char* argv[])
{
if(argc<2){
printf("usage:%s fifo\n",argv[0]);
exit(1);
}
printf("open fifo write...\n");
int fd = open(argv[1],O_WRONLY);
if(fd<0){
perror("open error");
exit(1);
}else{
printf("open fifo success:%d\n",fd);
}
char *s = "1234567890";
size_t size = strlen(s);
if(write(fd,s,size) != size){
perror("write error");
}
close(fd);
exit(0);
}
- 运行结果
当只运行读取命令管道或者只运行写入命名管道时,进程会被阻塞,直到另一端操作开始。
2.6 匿名管道和命名管道的异同区别
相同点:
- 适用于网路通信socket。
- 默认都是阻塞性读写。
- 阻塞型不完整管道
- 阻塞性完整管道
- 非阻塞型不完整管道(一端关闭)
- 非阻塞型完整管道(两端都开启)
不同点:
- 打开方式不一样。
- pipe通过fcntl系统调用,设置成非阻塞型O_NOBLOCK。
- FIFO通过fcntl系统调用或者open函数,设置成非阻塞型。
以FIFO为例,关于设置非阻塞的参考案例:
//...
printf("open fifo read...\n");
//打开命名管道fifo
//int fd = open(argv[1],O_RDONLY);
int fd = open(argv[1],O_RDONLY|O_NONBLOCK);//非阻塞型
//...
read(fd,buf,sizeof(buf);//这里不会阻塞,没有数据直接通过。
//...
3. System V的IPC对象
- IPC对象(消息队列,共享内存,信号量)。
- IPC对象 ,存在于内核中,必须由用户控制释放。不释放,则一直存在(除非关机重启)。可以通过以下命令查看:
ipcs 查看内核中的消息队列,共享内存,信号量
ipcs -q 查看消息队列
ipcs -m 查看共享内存
ipsc -s 查看信号量
- ipc对象在内核空间有唯一性标识ID,在用户空间有唯一性标识key。
- ipc对象是全局对象。
- ipc对象由get函数创建,msgget,shmget,semget。调用get函数时,必须指定关键字key。(内核自动分配ID)
3.1 IPC对象的权限和所有者
ipc_perm 结构定义于中,原型如下:
struct ipc_perm
{
uid_t uid; /* 所有者的用户ID*/
gid_t gid; /* 所有者所属组的有效组ID*/
uid_t cuid; /* 创建者的有效用户ID*/
gid_t cgid; /* 创建者所属组的有效组ID*/
unsigned short mode; /* 权限*/
unsignedshort seq; /* 序列号*/
};
4. IPC对象 - 消息队列
- 消息队列是一个链表。
- 消息:内核将用户数据,用户ID,组ID,读写进程ID,优先级等打包成的一个数据包。
- 允许多进程访问消息队列,读消息和写消息。
- 一个消息被读取后会被自动删除。
- 消息队列在内核中用唯一的IPC标识ID表示。
- 四种操作:创建打开,发送,读取,控制消息。
4.1 消息队列属性
struct msqid_ds
{
struct ipc_perm msg_perm; //放置权限和ID
struct msg *msg_first; /* first message on queue,unused */
struct msg *msg_last; /* last message in queue,unused */
__kernel_time_t msg_stime; /* last msgsnd time */
__kernel_time_t msg_rtime; /* last msgrcv time */
__kernel_time_t msg_ctime; /* 最后一次消息改变的时间 */
unsigned long msg_lcbytes; /* Reuse junk fields for 32 bit */
unsigned long msg_lqbytes; /* ditto */
unsigned short msg_cbytes; /* current number of bytes on queue */
unsigned short msg_qnum; /*消息的数量 */
unsigned short msg_qbytes; /* 消息总最大的字节数 */
__kernel_ipc_pid_t msg_lspid; /* 最后一次发送消息的进程pid */
__kernel_ipc_pid_t msg_lrpid; /* 最后一次接收消息的进程pid */
};
4.2 打开创建消息队列
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
int msgget(key_t key, int msgflg);//用户指定的key键值,权限标志(IPC_CREAT,IPC_EXCL)
- key指定键值,或者设置为,IPC_PRIVATE。若进行查询,key不能设置为0,否则查询不到。
4.3 控制消息队列
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
int msgctl(int msqid, int cmd, struct msqid_ds *buf);//消息队列ID,命令,消息队列属性指针
- cmd命令,IPC_STAT获取消息队列的属性(总数,最大字节数,时间等等)。
- cmd命令,IPC_SET设置消息队列的属性。
- cmd命令,IPC_RMID删除消息队列。删除消息队列以及队列上所有数据。
4.4 发送数据到消息队列
#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 消息队列ID
- msgp 消息结构,如下:(自己定义)
struct msgbuf {
long mtype; /* message type, must be > 0 */
char mtext[1]; /* message data */
};
- msgsz 消息的大小(用来指定mtext[]数组的大小)。
- msgflg 消息队列标志,如可以设置为 IPC_NOWAIT(类似于非阻塞,访问的进程不等待)。设置为0,阻塞。
4.5 从消息队列接收数据
#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 消息队列ID
- msgp 消息结构 缓存,用来存放数据。
- msgsz 消息的大小(不包括mtype的大小)。
- msgtyp 消息类型。0 获取第一个消息;大于0 获取该类型的消息的第一个消息;小于0 获取消息队列中 小于等于该值绝对值的消息(类型最小的)。
- msgflg 消息队列标志,如可以设置为 IPC_NOWAIT(类似于非阻塞,访问的进程不等待)。设置为0,阻塞。
4.6 参考案例
程序1 向消息队列发送数据
#include <string.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/msg.h>
typedef struct{
long type;//消息类型
int data[2];
}MSG;
int main(int argc,char* argv[])
{
if(argc<2){
printf("usage:%s key\n",argv[0]);
exit(1);
}
key_t key = atoi(argv[1]);
//key_t key = IPC_PRIVATE;
//key_t key = ftok(argv[1],0);//特殊的算法 一般传入存在的文件路径
printf("key:%d\n",key);
int msq_id;
if((msq_id= msgget(key,IPC_CREAT|IPC_EXCL|0777))<0)
{
perror("msgget error");
}
printf("msq id:%d\n",msq_id);
MSG m1 = {7,17,27};
MSG m2 = {6,16,26};
MSG m3 = {5,15,25};
MSG m4 = {4,14,24};
MSG m5 = {4,34,44};
//发送信息到消息队列
if(msgsnd(msq_id,&m1,sizeof(MSG)-sizeof(long),IPC_NOWAIT)<0){
perror("msgsnd error");
}
if(msgsnd(msq_id,&m2,sizeof(MSG)-sizeof(long),IPC_NOWAIT)<0){
perror("msgsnd error");
}
if(msgsnd(msq_id,&m3,sizeof(MSG)-sizeof(long),IPC_NOWAIT)<0){
perror("msgsnd error");
}
if(msgsnd(msq_id,&m4,sizeof(MSG)-sizeof(long),IPC_NOWAIT)<0){
perror("msgsnd error");
}
if(msgsnd(msq_id,&m5,sizeof(MSG)-sizeof(long),IPC_NOWAIT)<0){
perror("msgsnd error");
}
//获取当前消息队列中 消息的总数
struct msqid_ds ds;
if(msgctl(msq_id,IPC_STAT,&ds)<0){
perror("msgctl error");
}
printf("msg total:%ld\n",ds.msg_qnum);
exit(0);
}
kshine@kshine-virtual-machine:~/桌面/Kshine/lsd$ gcc msqwrite.c -o mw
kshine@kshine-virtual-machine:~/桌面/Kshine/lsd$ ./mw
usage:./mw key
kshine@kshine-virtual-machine:~/桌面/Kshine/lsd$ ./mw 223
key:223
msq id:0
msg total:5
kshine@kshine-virtual-machine:~/桌面/Kshine/lsd$
kshine@kshine-virtual-machine:~/桌面/Kshine/lsd$ ipcs -q
--------- 消息队列 -----------
键 msqid 拥有者 权限 已用字节数 消息
0x000000df 0 kshine 777 40 5
程序2 从消息队列中读取消息
#include <string.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/msg.h>
typedef struct{
long type;//消息类型
int data[2];
}MSG;
//传入key和类型
int main(int argc,char* argv[])
{
if(argc<3){
printf("usage:%s key type\n",argv[0]);
exit(1);
}
key_t key = atoi(argv[1]);
long type = atoi(argv[2]);
//获取指定的消息队列
int msq_id = msgget(key,0777);
if(msq_id<0)perror("msgget error");
printf("msq_id:%d\n",msq_id);
//从消息队列中接收指定类型的消息
MSG m;
if(msgrcv(msq_id,&m,sizeof(MSG)-sizeof(long),type,IPC_NOWAIT)<0)
{
perror("msgrcv error");
}else{
printf("type:%ld data:%d %d\n",m.type,m.data[0],m.data[1]);
}
exit(1);
}
从消息队列中读取数据,测试完成后,需要用户删除改消息队列。
kshine@kshine-virtual-machine:~/桌面/Kshine/lsd$ gcc msqread.c -o mr
kshine@kshine-virtual-machine:~/桌面/Kshine/lsd$ ./mr 223
usage:./mr key type
kshine@kshine-virtual-machine:~/桌面/Kshine/lsd$ ./mr 223 7
msq_id:0
type:7 data:17 27
kshine@kshine-virtual-machine:~/桌面/Kshine/lsd$ ./mr 223 6
msq_id:0
type:6 data:16 26
kshine@kshine-virtual-machine:~/桌面/Kshine/lsd$ ./mr 223 5
msq_id:0
type:5 data:15 25
kshine@kshine-virtual-machine:~/桌面/Kshine/lsd$ ./mr 223 4
msq_id:0
type:4 data:14 24
kshine@kshine-virtual-machine:~/桌面/Kshine/lsd$ ./mr 223 4
msq_id:0
type:4 data:34 44
kshine@kshine-virtual-machine:~/桌面/Kshine/lsd$ ./mr 223 4
msq_id:0
msgrcv error: No message of desired type
kshine@kshine-virtual-machine:~/桌面/Kshine/lsd$
kshine@kshine-virtual-machine:~/桌面/Kshine/lsd$ ipcs -q
--------- 消息队列 -----------
键 msqid 拥有者 权限 已用字节数 消息
0x000000df 0 kshine 777 0 0
kshine@kshine-virtual-machine:~/桌面/Kshine/lsd$ ipcrm -q 0
kshine@kshine-virtual-machine:~/桌面/Kshine/lsd$ ipcs -q
--------- 消息队列 -----------
键 msqid 拥有者 权限 已用字节数 消息
5. IPC对象 - 共享内存
- 共享内存是被多个进程共享的一部分物理内存。
- 共享内存被映射到进程的虚拟内存空间。通过虚拟地址操作共享内存。
- 不提供同步机制。
- 效率最高的IPC机制。
5.1 共享内存的属性
struct shmid_ds{
struct ipc_perm shm_perm; /* 权限,访问模式,创建用户信息等等*/
int shm_segsz; /*共享内存段的大小(以字节为单位)*/
time_t shm_atime; /*最后一个映射成功的时间*/
time_t shm_dtime; /*最后一个解除映射的时间*/
time_t shm_ctime; /*最后一个改变的时间*/
unsigned short shm_cpid; /*创建共享内存的进程的id/
unsigned short shm_lpid; /*最后一次调用该共享内存的进程的pid*/
short shm_nattch; /*当前已经成功映射的进程的数量*/
/*下面是私有的*/
unsigned short shm_npages; /*段的大小(以页为单位)*/
unsigned long *shm_pages; /*指向frames->SHMMAX的指针数组*/
struct vm_area_struct *attaches; /*对共享段的描述*/
};
- 操作步骤:创建shmget,映射shmat。
5.2 创建共享内存
#include <sys/ipc.h>
#include <sys/shm.h>
int shmget(key_t key, size_t size, int shmflg);
- key 用户指定或者IPC_PRIVATE,或者ftok()
- size 共享内存的大小
- shmflag 权限组合,IPC_CREAT,IPC_EXCL
5.3 控制共享内存
#include <sys/ipc.h>
#include <sys/shm.h>
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
- shmid 共享内存Id
- cmd 控制命令。IPC_STAT 获取共享内存的属性。IPC_SET 设置属性,IPC_RMID 删除共享内存,SHM_LOCK 锁定,SHM_UNLOCK 解锁
- buf 共享内存的属性指针
5.4 映射和解除映射
#include <sys/types.h>
#include <sys/shm.h>
void *shmat(int shmid, const void *shmaddr, int shmflg);
int shmdt(const void *shmaddr);
- 返回虚拟内存地址,失败返回-1
- shmaddr 可以用户设置的虚拟地址,建议写0,由系统自动分配。
- shmflag 标志,一般设置为0。
5.5 参考案例
两个进程对共享内存进行操作。一个进程操作,另一个阻塞,交替工作。
1)用于管理匿名管道的文件
tell.h
#ifndef _TELL_H_
#define _TELL_H_
//管道初始化
extern void init();
//利用管道进行等待
extern void wait_pipe();
//利用管道进行通知
extern void notify_pipe();
//销毁管道
extern void destory_pipe();
#endif
tell.c
#include "tell.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
static int fd[2];
//管道初始化
void init()
{
if(pipe(fd)<0){
perror("pipe error");
}
}
//利用管道进行等待
void wait_pipe()
{
char c;
if(read(fd[0],&c,1)<0){
perror("wait pipe error");
}
}
//利用管道进行通知
void notify_pipe()
{
char c='a';
if(write(fd[1],&c,1)!=1){
perror("notify pipe error");
}
}
//销毁管道
void destory_pipe()
{
close(fd[0]);
close(fd[1]);
}
创建共享内存,并写入和读取数据
#include <sys/shm.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include "tell.h"
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
int main(void)
{
//创建共享内存
int shmid;
if((shmid = shmget(IPC_PRIVATE,1024,IPC_CREAT|IPC_EXCL|0777))<0)
{
perror("shmget error");
exit(1);
}
pid_t pid;
init();//初始化管道
if((pid = fork())<0)
{
perror("fork error");
exit(1);
}else if(pid>0){
int *pi = (int*)shmat(shmid,0,0);
if(pi == (int*)-1){
perror("shmat error");
exit(1);
}
//写入数据
*pi =100;
*(pi+1)=200;
//解除映射
shmdt(pi);
notify_pipe();//通知子进程
destory_pipe();
wait(0);
}else if(pid ==0){
wait_pipe();//子进程等待父进程的通知
int *pi = (int*)shmat(shmid,0,0);
if(pi == (int*)-1){
perror("shmat error");
exit(1);
}
printf("start:%d end:%d\n",*pi,*(pi+1));
shmdt(pi);//解除映射
shmctl(shmid,IPC_RMID,NULL);//在此,所有的映射都被解除,删除共享内存
destory_pipe();
}
exit(1);
}
运行结果
kshine@kshine-virtual-machine:~/桌面/Kshine/lsd$ gcc shm.c tell.c -I./ -o shm
kshine@kshine-virtual-machine:~/桌面/Kshine/lsd$ ./shm
start:100 end:200
kshine@kshine-virtual-machine:~/桌面/Kshine/lsd$
6 IPC对象 - 进程间的信号量
6.1 进程信号量的概念
- 用于进程间的互斥和同步。
- 一个共享资源需要一个信号量。引入信号量集 来管理多个共享资源(类似于去图书馆借书,借阅多本的场景)。
- 信号量集是多个信号量的集合。可以对集合内的信号量进行统一操作。
- 信号量集里的信号量操作,可以要求全部成功,可以要求部分成功。
- 二元信号量(0和1)。信号灯。
- 对信号量进行PV操作。
6.2 信号量集属性
struct semid_ds {
struct ipc_perm sem_perm; /* 权限和用户ID相关 */
time_t sem_otime; /* 最后一次操作时间 */
time_t sem_ctime; /* 最后一次改变时间 */
unsigned long sem_nsems; /* 信号量集中信号灯的数量 */
};
6.3 创建信号量集
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
int semget(key_t key, int nsems, int semflg);
- key 用户指定的键值
- 信号量集中的信号量个数
- samflg IPC_CREAT,IPC_EXCL等权限组合
6.4 控制信号量集
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
int semctl(int semid, int semnum, int cmd, ...);
- semid 信号量集ID
- semnum 0表示对所有的信号量进行操作,信号量编号从0开始
- cmd 操作指定,IPC_STAT,IPC_SET,IPC_RMID,GETVAL,SETVAL,GETALL,SETALL
- 信号量属性结构定义,根据操作不同使用联合体中的相应类型变量
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) */
};
6.5 信号量集的操作
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
int semop(int semid, struct sembuf *sops, size_t nsops);
int semtimedop(int semid, struct sembuf *sops, size_t nsops,const struct timespec *timeout);
- semid 信号量集ID
- sops sembuf结构体数组指针
- nsops 第二个参数sops中结构体数组的长度。
struct sembuf{
unsigned short sem_num; /*信号量ID*/
short sem_op; /* semaphore operation */
short sem_flg; /* operation flags */
};
- sem_num 信号量编号
- sep_op 正数为V操作,负数为P操作。0可用于测试 共享资源是否已用完的。
- sem_flg SEM_UNDO标志,表示在进程结束时,操作将被取消。如果进程没有释放共享资源,内核将代为释放。 IPC_NOWAIT 非阻塞标志。
6.6 参考实例
封装一个信号量集模块(方便于调用)
定义一个文件pv.h
#ifndef __PV_H_
#define __PV_H_
//初始化 semnums个信号灯
int I(int semnums,int value);
//对信号量集(semid)中的信号灯(semnum)做P(value)操作
void P(int semid,int semnum,int value);
//对信号量集(semid)中的信号灯(semnum)做V(value)操作
void V(int semid,int semnum,int value);
//销毁信号量集
extern void D(int semid);
#endif
创建pv.c文件
#include <sys/sem.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <assert.h>
#include <malloc.h>
#include "pv.h"
union semun{
int val;
struct semid_ds *buf;
unsigned short *array;
};
//初始化 semnums个信号灯
int I(int semnums,int value)
{
//创建信号量集
int semid = semget(IPC_PRIVATE,semnums,IPC_CREAT|IPC_EXCL|0777);
if(semid<0){
return -1;
}
union semun un;
unsigned short* array = (unsigned short* )calloc(semnums,sizeof(unsigned short));
int i;
for(i=0;i<semnums;i++){
array[i]=value;
}
un.array =array;
//初始化信号量集中所有信号灯的初值
if(semctl(semid,0,SETALL,un)<0)
{
perror("semctl error");
return -2;
}
free(array);
return semid;
}
//对信号量集(semid)中的信号灯(semnum)做P(value)操作
void P(int semid,int semnum,int value)
{
assert(value>=0);
//定义sembuf类型的结构体数组,放置若干结构体变量
struct sembuf ops[] = {
{semnum,-value,SEM_UNDO}//1个成员
//{},
//{}
}
if(semop(semid,ops,sizeof(ops)/sizeof(struct sembuf))<0){
perror("semop error");
}
}
//对信号量集(semid)中的信号灯(semnum)做V(value)操作
void V(int semid,int semnum,int value)
{
assert(value>=0);
//定义sembuf类型的结构体数组,放置若干结构体变量
struct sembuf ops[] = {
{semnum,value,SEM_UNDO}//1个成员
//{},
//{}
}
if(semop(semid,ops,sizeof(ops)/sizeof(struct sembuf))<0){
perror("semop error");
}
}
//销毁信号量集
void D(int semid)
{
if(semctl(semid,0,IPC_RMID,NULL)<0){
perror("semctl error");
}
}
关于调用:
I(semid,1);//初始化信号量集,初值为1
//...
//进程1
P(semid,0,1);//减操作,值变成0(如果此时值已经为0,则阻塞在此)
//... 对共享资源进行操作(如果异常提前跳出,记得需要做V操作)
V(semid,0,1);//(正常退出占用)加操作,值变成1
//进程2
P(semid,0,1);//减操作,值变成0(如果此时值已经为0,则阻塞在此)
//... 对共享资源进行操作(如果异常提前跳出,记得需要做V操作)
V(semid,0,1);//(正常退出占用)加操作,值变成1
6.结束
如有写错的地方欢迎指正,我将及时更正。