进程间通信(IPC),就是在不同进程之间传播或交换信息,Linux进程间通信的方法有:管道,消息队列,信号量,共享内存,套接字等等。
目录
一,管道
1,匿名管道
匿名管道在系统中没有实名的,并不刻意在文件系统中以任何方式看到该管道,它只是进程的一种资源,会随着进程的结束而被系统清理掉。管道通信是半双工的,需要双向通信时,需要建立起两个管道,只能由于父子或者兄弟进程之间,管道的缓冲区是有限的,定义大小为PIPE_BUF(usr/include/linux/limits.h)4096字节。
#include <unistd.h>
Int pipe(int pipefd[2]) ;
成功返回0,出错返回-1,fd[2]是返回的两个文件描述符。fd[0]表示读出端的文件描述符。fd[1]表示写入端的文件描述符。
例如pipe.c:
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <stdlib.h>
#include <sys/stat.h>
int main()
{
pid_t pid;
int fd[2];
pipe(fd); //在fork()之前
printf("fd[0]=%d\n",fd[0]);
printf("fd[1]=%d\n",fd[1]);
if((pid=fork())==-1)
{
printf("error\n");
exit(1);
}
else if(pid==0)
{
}
else
;
return 0;
}
管道的读写:
可以使用read和write函数进行读写操作读取规则为:管道写端不存在时,认为读到了末尾,读出字节数为0。
如下: 先创建管道,父进程写入数据,子进程读取数据:
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <stdlib.h>
#include <sys/stat.h>
#include <sys/wait.h>
int main()
{
pid_t pid;
int fd[2];
char buf[32]={0};
pipe(fd);
printf("fd[0]=%d\n",fd[0]);
printf("fd[1]=%d\n",fd[1]);
if((pid=fork())==-1)
{
printf("error\n");
exit(1);
}
//子进程
else if(pid==0)
{
close(fd[1]);
read(fd[0],buf,32);
printf("buf is %s\n",buf);
close(fd[0]);
exit(0);
}
else //父进程
{
close(fd[0]);
int status;
write(fd[1],"hello Linux",11);
close(fd[1]);
wait(&status);
exit(0);
}
return 0;
}
如下:在兄弟进程间创建管道实现通信:
在父进程fork两个子进程,fork()出子进程后,子进程继承存活的故拿到,故第二个fork()才关闭父进程中关闭管道输出:
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <string.h>
#include <limits.h>
#define BUFSIZE PIPE_BUF
void err(char *msg)
{
printf("%s\n",msg);
exit(1);
}
int main()
{
int len=0;
int fd[2];
pid_t pid1,pid2;
char buf[BUFSIZE]="hello my brother\n";
if(pipe(fd)<0)
err("pipe failed");
if((pid1=fork())<0)
err("fork_1 failed");
if(pid1==0) //子进程1
{
close(fd[0]);
write(fd[1],buf,strlen(buf));
close(fd[1]);
exit(0);
}
if((pid2=fork())==-1)
err("fork_2 failed");
if(pid2==0) //子进程2
{
close(fd[1]);
len= read(fd[0],buf,BUFSIZE);
write(STDOUT_FILENO,buf,len); //标准输出
close(fd[0]);
exit(0);
}
else
{
close(fd[0]);
close(fd[1]);
exit(0);
}
return 0;
}
输出结果如下:
2,命名管道
命名管道也成为FIFO,,不同于匿名管道,它是一种文件类型,在文件系统中是可见的,命名管道可以在无亲缘关系的进程之间通信,FIFO严格遵守先进先出规则,不支持lseek文件定位操作。
在shell中可以使用mkfifo命令创建一个命名管道:
mkfifo [options] name
options: -m mode 设置文件权限,受umask修正
c程序创建:
#include <sys/types.h>
#include <sys/stat.h>
int mkfifo(const char* pathname,mode_t mode);
pathname文件路径,mode权限,成功返回0,出错返回-1。
如下所示: 创建fifo_write.c和wifi_read.c。执行是fifo_write.c进程创建命名管道并写入内容,fifo_read.c进程读取内容,并输出:
fifo_write.c:
1.#include <stdio.h>
2.#include <unistd.h>
3.#include <sys/types.h>
4.#include <stdlib.h>
5.#include <sys/stat.h>
6.#include <sys/wait.h>
7.#include <fcntl.h>
8.#include <string.h>
9.
10.int main(int argc,char *argv[])
11.{
12.int fp;
13.int ret;
14.//以规定格式打开
15.if(argc!=2)
16.{
17. printf("Format: %s <fifo name>\n",argv[0]);
18. return -1;
19.}
20.//如果该管道文件没有被创建,则创建
21.//access 参数二为F_OK时,返回-1表示参数一指定的文件不存在
22.if(access(argv[1],F_OK==-1))
23.{
24. ret=mkfifo(argv[1],0666);
25. if(ret==-1)
26. {
27. printf("make fifo error\n");
28. return -2;
29. }
30. printf("mkfifo is ok\n");
31.}
32.//以只写的方式打开该文件
33.fp=open(argv[1],O_WRONLY);
34.while(1)
35.{
36. sleep(1);
37. write(fp,"Linux haihaihai",15);
38.}
39.close(fp);
40.return 0;
41.}
fifo_read.c:
1.#include <stdio.h>
2.#include <unistd.h>
3.#include <sys/types.h>
4.#include <stdlib.h>
5.#include <sys/stat.h>
6.#include <sys/wait.h>
7.#include <fcntl.h>
8.#include <string.h>
9.
10.int main(int argc,char *argv[])
11.{
12.
13.char buf[32]={0};
14.int ret;
15.int fp;
16. //按规定格式输入
17. if(argc!=2)
18. {
19. printf("Format: %s <fifo name>\n",argv[0]);
20. return -1;
21. }
22.//以只读的方式打开管道文件
23.fp=open(argv[1],O_RDONLY);
24.while(1)
25.{
26. sleep(1);
27. read(fp,buf,32);
28. printf("buf is %s\n",buf);
29. memset(buf,0,sizeof(buf));
30.}
31.close(fp);
32.return 0;
33.}
用两个终端分别运行程序:fifo_write进程每隔1s把"Linux haihaihai"写进管道,fifo_read进程每隔1s从管道中读取并标准输出到屏幕:
二,消息队列
1,消息队列概念
消息队列是一种以链表式结构组织的数据,存放在内核中,是由各进程通过消息队列标识符来引用的一种数据传送方式,消息队列是随内核持续的,即只有在内核重启或者显示删除一个消息队列时,该消息队列才会被真正删除,系统中有记录消息队列的数据结构(struct ipc_ids msg_ids)位于内核中,系统中的所有消息队列都可以在结构msg_ids中找到访问入口。
消息队列就是一个消息的链表,每个消息队列都有一个队列头,用结构struct msg_quene来描述,该队列头包含了消息的大量信息,包括消息的队列键值,用户ID,组ID,消息数目等等,用户可以访问也可以设置这些信息,该结构体定义如下:
struct msg_quene
{
struct ipc_perm q_perm;
item_t q_stime; //上次发送时间
item_t r_stime; //上次接收时间
item_t c_stime;
unsigned long q_cbytes; //队列中当前字节
unsigned long q_cnum; //消息数量
unsigned long q_qbytes; //最大字节数
pid_t lspid; //pid of last msgsnd
pid_t lrpid; //last receive pid
struct list_head q_messages;
struct list_head q_receivers;
struct list_hesd q_senders;
}
结构msqid_ds用来设置或者返回消息队列的信息:
struct msqid_ds
{
struct ipc_perm q_perm;
struct msg* msg_first;
struct msg* msg_last;
item_t q_stime;
item_t r_stime;
item_t c_stime;
unsigned long msg_lcbytes;
unsigned long msg_lqbytes;
unsigned short msg_cbytes;
unsigned short msg_qnum;
unsigned short msg_qbytes;
pid_t msg_lspid;
pid_t msg_lrpid;
}
struct ipc_perm定义如下:
struct ipc_perm
{
key_t key; //该键值唯一对应一个消息队列
uid_t uid;
gid_t gid;
uid_t cuid;
gid_t cgid;
mode_t mode;
unsigned long seq;
};
2,消息队列的创建与打开
每个消息队列都有一个标识符,而要获取标识符,首先要提供该消息队列的键值。ftok函数用于获取特定文件名的键值。
#include <sys/types.h>
#include <sys/ipc.h>
key_t ftok(char * pathname,char proj);
成功返回键值,出错返回-1,pathname是一个路径于该键值对应。
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
int msgget(key_t key,int msgflg);
msgget函数用来创建或者打开与键值对应的消息队列,成功返回标识符,出错返回-1,key是键值,msgflg参数是标志位,可取以下值:IPC_CREAT,IPC_EXCL,IPC_NOWAIT或三者逻辑或的结果。msgget创建时,它相应的msqid_ds被初始化,ipc_perm结构各成员被设置成特定值。以下情况时,创建新的消息队列:
无消息队列与key对应,且设置了IPC_CREAT标志位;
key设置为IPC_PRIVATE。
3,消息队列的读写
消息队列传递的消息由两部分组成,消息的类型和所传递的数据,一般用数据结构struct msgbuf来表示,通常类型是一个长整数,数据根据需要进行设定,如:
struct msgbuf
{
long msgtype; //传递数据的类型
char msgtext[1024];
}
向消息队列发送数据系统调用函数原型如下:
#include <sys/types>
#include <sys/ipc.h>
#include <sys/msg.h>
int msgsnd (int msqid,const void *ptr,size_t nbytes,int flags);
成功返回0,出错返回-1。msgsnd函数向消息队列发送一个消息,该消息被添加到队列的末尾,msqid代表队列标识符,ptr指向要发送的消息,nbytes以字节数表示消息数据的长度,flags用于指定队列满时的处理方法,当flags设置为IPC_NOWAIT时,表示队列满时不等待直接返回一个错误。
从消息队列中接收一个数据函数如下:
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
int msgrcv (int msqid,const void *ptr,size_t nbytes,long type,int flags);
成功返回消息的数据长度,出错返回-1,该函数用来读取一个消息的数据。其参数类似于msgsnd。type取0时,接收队列中第一条消息,取值大于0时,接收队列中类型值等于type的第一条消息,小于0时,取类型值小于等于type的绝对值的所有消息中类型值最小的那一条消息。
4,获得或设置消息队列的属性
消息队列的属性基本上都保存在队列头上,可以分配一个类似于队列头的结构体来返回队列的属性。
int msgctl(int msqid,int cmd,struct msqid_ds *buf);
成功返回0,失败返回-1,cmd取值有:
IPC_STAT:获取队列信息,返回的信息存储在buf中;
IPC_SET:设置队列信息;
IPC_RMID:删除msqid标识的消息队列。
例子程序如下:msg.c:
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <stdio.h>
#include <unistd.h>
void msg_stat(int msgid,struct msqid_ds msg_info);
int main()
{
int gflags,sflags,rflags;
key_t key;
int msgid;
int reval;
//发送消息缓冲区数据结构
struct msgsbuf
{
int mtype;
char mtext[1];
}msg_sbuf;
//接收消息缓冲区数据结构
struct msgmbuf
{
int mtype;
char mtext[10];
}msg_rbuf;
struct msqid_ds msg_ginfo,msg_sinfo;
char* msgpath="./msgqueue";
key=ftok(msgpath,'a');
gflags=IPC_CREAT|IPC_EXCL;
msgid=msgget(key,gflags|00666);
if(msgid==-1)
{
printf("msg creat error!\n");
return -1;
}
msg_stat(msgid,msg_ginfo);
sflags=IPC_NOWAIT;
msg_sbuf.mtype=10;
msg_sbuf.mtext[0]='a';
reval=msgsnd(msgid,&msg_sbuf,sizeof(msg_sbuf.mtext),sflags);
if(reval==-1)
{
printf("msg send error!\n");
}
msg_stat(msgid,msg_ginfo);
rflags=IPC_NOWAIT|MSG_NOERROR;
reval=msgrcv(msgid,&msg_rbuf,4,10,rflags);
if(reval==-1)
{
printf("resd msg error!\n");
}
else
{
printf("read from msg queue %d bytes\n ",reval);
}
msg_stat(msgid,msg_ginfo);
msg_sinfo.msg_perm.uid=8;
msg_sinfo.msg_perm.gid=8;
msg_sinfo.msg_qbytes=16388;
reval=msgctl(msgid,IPC_SET,&msg_sinfo);
if(reval==-1)
{
printf(" msg set info error!\n");
return -2;
}
msg_stat(msgid,msg_ginfo);
reval=msgctl(msgid,IPC_RMID,NULL);
if(reval==-1)
{
printf(" unlink msg queue error!\n");
return -3;
}
return 0;
}
void msg_stat(int msgid,struct msqid_ds msg_info)
{
int reval;
sleep(1);
reval=msgctl(msgid,IPC_STAT,&msg_info);
if(reval==-1)
{
printf("get msg info error\n");
return;
}
printf("\n");
printf("current number of bytes on queue is %ld\n",msg_info.msg_cbytes);
printf("number of messages in queue is %ld\n",msg_info.msg_qnum);
printf("max number of bytes on queue is %ld\n",msg_info.msg_qbytes);
printf("pid of last msgsnd is %d\n",msg_info.msg_lspid);
printf("pid of last msgrcv is %d\n",msg_info.msg_lrpid);
printf("last msgsnd time is %s",ctime(&(msg_info.msg_stime)));
printf("last msgrcv time is %s",ctime(&(msg_info.msg_rtime)));
printf("last change time is %s",ctime(&(msg_info.msg_ctime)));
printf("msg uid is %d\n",msg_info.msg_perm.uid);
printf("msg gid is %d\n",msg_info.msg_perm.gid);
}
如下,用root权限运行,但是发现读取时发生了错误,目前不知道什么原因。
三,共享内存
两个不同的进程共享内存的意思是,同一块物理内存被映射到两个进程各自的进程地址空间,在shell可以使用ipcs查看当前系统IPC的状态。
对每一个共享存储段,内核会为其维护一个shmid_ds的数据类型的结构体(shmid_ds结构体定义在头文件<sys/shm.h>中),其定义如下:
struct shmid_ds
{
struct ipc_perm q_perm;
size_t shm_segzs; //字节表示共享内存的大小
pid_t shm_lpid; //最近一次调用shmop函数的进程ID
pid_t shm_cpid; //创建该共享内存的进程ID
unsigned short shm_lkcnt; //共享内存区域被锁定的时间数
unsigned long shm_nattch; //当期使用该共享内存的进程数
time_t shm_atime; //最近一次附加操作时间
time_t shm_dtime; //最近一次分离操作时间
time_t shm_ctime; //最近一次修改时间
};
共享内存相关操作:
对于System V共享内存,主要有几个API,shmget,shmat,shmdt,shmctl。
1,创建或者打开共享内存:
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
int shmget(key_t key,int size,int flag)
成功返回共享内存ID,出错返回-1。key表示所要创建或者打开共享内存的键值,size表示共享内存区域的大小,flag表示调用函数的操作类型,也可用于设置共享内存的访问权限,两者通过逻辑或来实现。
当key指定为IPC_PRIVATE时,创建一个新的共享内存,此时flag无效;
当key不为PC_PRIVATE且flag设置了IPC_CREAT位而没有IPC_EXCL位时,若key存在,则打开其对应的共享内存,否则则创建共享内存。
当key不为PC_PRIVATE且flag设置了IPC_CREAT位和IPC_EXCL位时,只执行创建共享内存的操作,key与内核中已经存在的共享内存的键值都不同时,才创建成功,否则返回EEXIST错误。
如下,创建共享内存:creat_shm.c:
#include <sys/ipc.h>
#include <stdio.h>
#include <sys/types.h>
#include <stdlib.h>
#include <sys/shm.h>
int main()
{
int shmid;
shmid=shmget(IPC_PRIVATE,1024,0666);
if(shmid<0)
{
printf("creat shm err!");
return -1;
}
printf("creat successfully and shmid is %d\n",shmid);
//使用shell命令查看
system("ipcs -m");
return 0;
}
执行结果如下:
使用宏IPC_CREAT创建得到键值为0,而使用ftok得到的键值传入该参数,键值不为0。
2,附加:
当一个共享内存被创建或打开后,某个进程如果要使用共享内存,必须将此内存区域附加到他的地址空间,附加的系统调用如下:
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
void * shmat(int shmid,const void* addr,int flag);
成功返回指向共享内存段的指针,出错返回-1。
shmid指定要引入的共享内存ID,addr和flag说明要引入的地址值,addr为NULL时,表示有内核决自动完成地址得映射。 执行成功后,shmid_ds结构体的shm_nattch值加一。
3,分离
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
int shmdt(void* addr);
成功返回0,否则返回-1。
将进程地址映射解除,即用于共享内存的区域与进程的地址空间相分离,addr是调用函数的返回值,并不删除共享内存本身。
4,共享内存的控制
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
int shmctl(int shmid,int cmd,struct shmid_ds *buf)
成功返回0,否则返回-1。
shmid表示共享内存ID,buf作用与cmd相关,cmd取值如下:
IPC_STAT : 获取共享内存shmid_ds结构,返回给buf;
IPC_SET : buf赋值给共享内存的shmid_ds结构;
IPC_RMID:删除shmid指向的共享内存段;
IPC_LOCK,IPC_UNLOCK:...
实例:使用共享内存完成父子进程之间的通信
creat_shm_2.c如下:
#include <sys/ipc.h>
#include <stdio.h>
#include <sys/types.h>
#include <stdlib.h>
#include <sys/shm.h>
#include <string.h>
#include <sys/wait.h>
#include <unistd.h>
int main()
{
char * addr_w,*addr_r;
int shmid;
int key;
pid_t pid;
key=ftok("./shm_space",25); //获取键值
shmid=shmget(key,1024,IPC_CREAT|0777);
if(shmid<0)
{
printf("creat shm err!");
return -1;
}
printf("creat successfully and shmid is %d\n",shmid);
pid=fork();
if(pid==0) //子进程读取共享内存数据
{
sleep(2);
printf("child process\n");
addr_r=shmat(shmid,NULL,0);
printf("receive massage is %s\n",addr_r);
}
else if(pid>0) //父进程向共享内存中写入数据
{
addr_w=(char*)shmat(shmid,NULL,0);
strncpy(addr_w,"This is a test",14);
wait(NULL);
exit(0);
}
else
{
printf("fork error!");
exit(1);
}
return 0;
}
执行结果如下:
四,信号量
信号量的原理是一种数据操作锁的概念,它本身不具备数据交换的功能,而是通过其他通信资源来实现进程之间的通信。在信号量上定义两种操作,Wait和Release,当一个进程调用Wait等待时,它要吗得到的信号量得值减一,要吗一直等待下去,直到信号量大于1时,Release实际上在信号量上执行加一操作,释放信号量守护的资源。
在信号量的实际应用中,不能单独定义一个信号量,而是定义一个信号量集,其中包含一组信号量,同一信号量只使用同一个引用ID,每个信号量集都有一个与之对应的结构体:
struct semid_ds
{
struct ipc_perm sem_perm;
struct sem* sem_base;
unsigned short sem_nsems;
time_t sem_otime;
time_t sem_ctime;
};
其中sem结构体如下,记录了一个信号的信息:
struct sem
{
unsigned short semval;
pid_t sempid;
unsigned short semncent;
unsigned short semncent;
};
信号量的相关操作
1,创建或者打开信号量集
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
int semget(key_t key,int nsems,int flag);
成功返回信号量集ID,否则返回-1。
该函数用于创建或者打开信号量集,key表示对应的键值,nsems表示信号量集中包含信号的个数,此参数只在创建信号量集时有效,flag表示调用函数的操作类型和权限,用逻辑或实现,约定与shmget类似。若使用该函数创建一个新的信号量集时,相应的semid_ds被初始化。
2,对信号量集的操作
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
int semop(int semid,struct sembuf semoparray[],size_t nops);
成功返回0,失败返回-1。
semid表示信号量集的ID,nops表明semoparray数组的个数,sembuf结构体定义如下(原子操作):
struct sembuf
{
unsigned short sem_num;
short sem_op;
short sem_flg;
}
sem_num表示信号量中某一个资源(要指定的信号量),取值范围为0-ipc_perm.sem_nsems,
sem_op指向执行的操作,其取值为整数。sem_flg说明函数semop的行为:
sem_op>0时,进程对该资源使用完毕,释放相应的资源数,并将sem_op的值加到信号量上。
sem_op=0时,进程阻塞直到相应值为0,当信号量为0时,函数立即返回,如果信号量值不为0,依据sem_flag的IPC_NOWAIT位决定函数的动作。
sem_o<0时,请求sem_op绝对值得资源,如果相应资源数可以满足。则信号量的值减去sem_op的绝对值,函数成功返回,否则操作将与sem_flg有关。
3,对信号量集的控制
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
int semctl(int semid,int semnum,int cmd,union semun arg);
函数成功返回值大于等于0,否则返回-1。
semnum指定semid的信号集中某一个信号量,cmd表示对应函数进行的操作,其取值与意义与arg有关,arg如下:
union
{
int val;
struct semid_ds* buf;
unsigned short array;
};
cmd取值:
IPC_SET :按参数arg.buf的值设置信号两属性;
IPC_STAT: 获取信号量属性
IPC_RMID:删除信号量集
...
例程,使用信号量完成进程间的同步:
sem.c如下:
#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
#include <stdlib.h>
#include <unistd.h>
union semun //定义结构体
{
int val;
struct semid_ds* buf;
unsigned short array;
};
int main()
{
int semid;
int key;
pid_t pid;
struct sembuf sem;
union semun sem_un;
key=ftok("./sem_space",0666); //获取键值
semid=semget(key,1,0666|IPC_CREAT); //创建信号集
sem_un.val=0;
semctl(semid,0,SETVAL,sem_un); //信号量设置为0
pid=fork(); //创建子进程
/*
子进程睡眠2s,父进程执行时,由于信号量为0,会被阻塞,子进程睡眠醒来之后
,释放信号量(V操作),此时父进程可以继续执行。
*/
if(pid>0)
{
sem.sem_num=0;
sem.sem_op=-1;
sem.sem_flg=0;
semop(semid,&sem,1);
printf("parents process\n");
sem.sem_num=0;
sem.sem_op=1;
sem.sem_flg=0;
semop(semid,&sem,1);
}
else if(pid==0)
{
sleep(2);
printf("child process\n");
sem.sem_num=0;
sem.sem_op=1;
sem.sem_flg=0;
semop(semid,&sem,1);
}
return 0;
}
如下,通过信号量,实现量进程之间的同步操作: