一、Linux下进程间通信方式
- 管道:无名管道 (pipe)和有名管道 (fifo)
- 信号(signal)
- 共享内存(shared memory)
- 消息队列(message queue)
- 信号量(semaphore)
- 套接字(socket)
二、管道
管道是进程间通信的主要手段之一。一个管道实际上就是个只存在于内存中的文件,对这个文件的操作要通过两个已经打开文件进行,它们分别代表管道的两端。管道是一种特殊的文件,它不属于某一种文件系统,而是一种独立的文件系统,有其自己的数据结构。根据管道的适用范围将其分为:无名管道和命名管道(有名)。
1.无名管道(pipe)
- 在内核里面开辟的一段空间,并且一端作为读,另一端作为写。
- 只能用于具有亲缘关系的进程之间通信(也就是父子进程或兄弟进程之间)
- 是一个单工通信模式,只支持数据的单向流动,当需要双向通信时需要建立两个管道。
- 数据的读写操作:一个进程向管道中写数据,所写的数据添加在管道缓冲区的尾部;另一个进程在管道中缓冲区的头部读数据。
实现函数
#include <unistd.h>
int pipe(fildes[2])
//filedes[0]为管道里的读取端
// filedes[1]为管道里的写入端
/* 读写 */
read(fd, buf, sizeof(buf));
write(fd, buf, sizeof(buf));
例程:
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
#define BUFF_SZ 256
int main()
{
printf("app start...\n");
pid_t pid;
int pipe_fd[2];
char buf[BUFF_SZ];
const char data[] = "hi, this is the test data";
int bytes_read;
int bytes_write;
//clear buffer, all bytes as 0
memset(buf, 0, sizeof(buf));
//creat pipe
if(pipe(pipe_fd) < 0)
{
printf("[ERROR] can not create pipe\n");
exit(1);
}
//fork an new process
if(0 == (pid=fork()))
{
//close the write-point of pipe in child process
close(pipe_fd[1]);
//read bytes from read-point of pipe in child process
if((bytes_read = read(pipe_fd[0], buf, BUFF_SZ)) > 0)
{
printf("%d bytes read from pipe : '%s'\n", bytes_read, buf);
}
//close read-point of pipe in child process
close(pipe_fd[0]);
exit(0);
}
//close read-point of pipe in parent process
close(pipe_fd[0]);
//write bytes to write-point of pipe in parent process
if((bytes_write = write(pipe_fd[1], data, strlen(data))))
{
printf("%d bytes wrote to pipe : '%s'\n", bytes_write, data);
}
//close write-point of pipe in parent process
close(pipe_fd[1]);
//wait child process exit
waitpid(pid, NULL, 0);
printf("app end\n");
return 0;
}
/*
运行输出为:
app start...
25 bytes wrote to pipe : 'hi, this is the test data'
25 bytes read from pipe : 'hi, this is the test data'
app end
*/
2.有名管道(FIFO)
有名管道是为了解决无名管道只能用于近亲进程之间通信的缺陷而设计的。命名管道是建立在实际的磁盘介质或文件系统(而不是只存在于内存中)上有自己名字的文件,任何进程可以在任何时间通过文件名或路径名与该文件建立联系。为了实现命名管道,引入了一种新的文件类型——FIFO文件(遵循先进先出的原则)。实现一个命名管道实际上就是实现一个FIFO文件。命名管道一旦建立,之后它的读、写以及关闭操作都与普通管道完全相同。虽然FIFO文件的inode节点在磁盘上,但是仅是一个节点而已,文件的数据还是存在于内存缓冲页面中,和普通管道相同。
实现函数:
/* 命名管道 */
#include <sys/types.h>
#include <sys/stat.h>
/* 建立管道 */
int mkfifo(const char * pathname, mode_t mode);
/*
pathname 路径,创建管道的位置
mode 打开函数open中的Mode
*/
/* 打开管道 */
int open(const char *pathname,int oflag,... /* mode_t mode */);
/*
mode 可选:O_RDONLY | O_WRONLY | O_NONBLOC
*/
/* 读写 */
int read(int fd, void *buf, int nbyte)
int write(int fd, void *buf, int nbyte)
/* 关闭 */
close(int handle)
例程:
/* fifo_write.c 写进程*/
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdlib.h>
#include <errno.h>
int main(int argc, const char *argv[])
{
int fd;
if(mkfifo("./fifo",0664) == -1)
{
if(errno == EEXIST) //哪个进程先运行,谁就创建
{
fd = open("./fifo",O_WRONLY);
}
else
{
perror("mkfifo");
exit(1);
}
}
else
{
fd = open("./fifo",O_WRONLY);
}
write(fd,"hello",5);
close(fd);
return 0;
}
/*fifo_read.c 读进程*/#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdlib.h>
#include <errno.h>
int main(int argc, const char *argv[])
{
int fd;
char buf[64] = {0};
if(mkfifo("./fifo",0664) == -1) //哪个进程先运行,谁就创建
{
if(errno == EEXIST)
{
fd = open("./fifo",O_RDONLY);
}
else
{
perror("mkfifo");
exit(1);
}
}
else
{
fd = open("./fifo",O_RDONLY);
}
read(fd,buf,sizeof(buf));
printf("%s\n",buf);
close(fd);
return 0;
}
三、信号通信
kill -l 查看信号
- 信号是软件层次上对中断机制的一种模拟,是一种异步通信方式,进程间唯一的异步通信机制。
- 信号可以直接进行用户空间进程和内核空间的交互
- 每个信号都有一个名字,这些名字都以SIG开头。在
#include <signal.h>
void(*signal(int signo,void(*func)(int)))(int)
// void(*)(int) signal(int signo,void(*func)(int))
// 返回值 参数1 参数2
signo是信号名,func的值是常量SIG_IGN、SIG_DFL或者当接到此信号要调用的函数的地址。
- 如需要忽略信号sig,调用方式: signal(sig,SIG_IGN);
- 如需要捕捉某个信号进行特殊处理,则 signal(sig,fun); fun为信号处理的函数名,定义相应功能即可
(2)kill()
#include <signal.h>
int kill(pid_t pid, int signo)
功能:发送信号signo给pid 指定的进程。
- pid > 0,pid是信号欲送往的进程的标识。
- pid == 0,信号将送往所有与调用kill()的那个进程属同一个使用组的进程。
- pid <0,信号将送往所有调用进程有权给其发送信号的进程,除了进程1(init)。
- pid == -1,信号将送往以-pid为组标识的进程。
注:int raise(int signo)只可给自身发送信号,等价于kill(getpid(),signo)
(3) pause()
#include <unistd.h>
int pause(void);
功能,使调用进程挂起直至捕捉到一个信号。
(4)alarm()
#include<unistd.h>
unsigned int alarm(unisigned int seconds);
- 用alarm可以设置一个计时器,当计时器超时时,产生SIGALRM信号,如果不忽略不捕捉该信号,默认操作时终止调用alarm的进程。
- 每个进程智能设置一个闹钟时钟,如果在调用alarm时,以前已为该进程设置过时钟:所设置时钟未超时,余值作为本次alarm调用的返回值,闹钟时钟被新值替代,若本次调用seconds为0,则取消以前的闹钟时钟。
例程:
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
/****************************************************************************
* 司机售票员问题(必做题)
* 创建子进程代表售票员,父进程代表司机 ,同步过程如下:
*
* 售票员捕捉SIGINT(代表开车),发SIGUSR1给司机,司机打印(“let’s gogogo”)
* 售票员捕捉SIGQUIT(代表停车),发SIGUSR2给司机,司机打印(“stop the bus”)
* 司机捕捉SIGTSTP(代表车到总站),发SIGUSR1给售票员,售票员打印(“please get off
* the bus”)
*
**************************************************************************** */
pid_t pid_son;
void funson(int seg)
{
if(seg == SIGINT)
kill(getppid(),SIGUSR1);
if(seg == SIGQUIT)
kill(getppid(),SIGUSR2);
if(seg == SIGUSR1)
printf("please get off the bus\n");
}
void funfath(int seg)
{
if(seg == SIGUSR1)
printf("let's gogogo\n");
if(seg == SIGUSR2)
printf("stop the bus\n");
if(seg == SIGTSTP)
kill(pid_son,SIGUSR1);
}
int main(int argc, const char *argv[])
{
pid_t pid;
signal(SIGINT,SIG_IGN);
signal(SIGQUIT,SIG_IGN);
pid = fork();
if(pid == -1)
;
else if(pid == 0)
{
signal(SIGINT,funson);
signal(SIGQUIT,funson);
signal(SIGUSR1,funson);
signal(SIGTSTP,SIG_IGN);
}
else
{
pid_son = pid;
signal(SIGUSR1,funfath);
signal(SIGUSR2,funfath);
signal(SIGTSTP,funfath);
}
pause();
pause();
pause();
return 0;
}
四、共享内存
1.IPC对象
IPC对象(inter-process communication)是活动在内核级别的一种进程间通信的工具,源自 system V,由一系列System V IPC函数实现。
IPC对象通过它的标识符来引用和访问,这个标识符是一个非负整数,它唯一的标识了一个IPC对象,这个IPC对象可以是消息队列或信号量或共享内存中的任意一种类型。在Linux系统中标识符被声明成整数,所以可能存在的最大标识符为65535。这里标识符与文件描述符有所不同,使用open函数打开一个文件时,返回的文件描述符的值为当前进程最小可用的文件描述符数组的下标。IPC对象删除或创建时相应的标识符的值会不断增加到最大的值,归零循环分配使用。
IPC键值与IPC标识符
(1)key值选择方式
对于key值,应用程序有如下三种选择:
- 调用ftok,给它传递pathname和proj_id,操作系统根据两者合成key值。
- 指定key为IPC_PRIVATE,内核保证创建一个新的、唯一的IPC对象,IPC标识符与内存中的标识符不会冲突。IPC_PRIVATE为宏定义,其值等于0。
- 指定key为大于0的常数,这需要用户自行保证生成的IPC key值不与系统中存在的冲突,而前两种是操作系统保证的。
(2)IPC标识符
给semget、msgget、shmget传入key值,它们返回的都是相应的IPC对象标识符。注意IPC键值和IPC标识符是两个概念,后者是建立在前者之上。ipc_id为IPC标识符,由semget、msgget、shmget函数生成。ipc_id在三种通信方式中各自IPC对象标识符:
- 在信号量函数中称为semid
- 在消息队列函数中称为msgid
- 在共享内存函数中称为shmid
终端查看共享内存:ipcs
删除共享内存命令:ipcrm -m (shmid)
2.共享内存
共享内存从字面意义解释就是多个进程可以把一段内存映射到自己的进程空间,以此来实现数据的共享以及传输,这也是所有进程间通信方式中最快的一种。共享内存是存在于内核级别的一种资源,在shell中可以使用ipcs命令来查看当前系统IPC中的状态。
- 共享内存是覆盖的方式操作,而非清空
- 本身不具备同步功能
实现函数
shmget():创建共享内存
int shmget(key_t key,int size,int shmflag)
// key:IPC_PRIVATE 或 ftok() 的返回值
// size:共享内存区大小
// shmflg:同open函数的权限位,也可以用8进制表示法
shmat():映射共享内存
void *shmat(int shmid,const void *shmaddr,int shmflag)
// shmid:要映射的共享内存区标识符
// shmaddr:将共享内存映射到指定地址(若为NULL,则表示由系统自动完成映射)
// shmflag:SHM_RDONLY 共享内存只读
// 默认0:共享内存可读写
shmdt():取消映射
int shmdt(const void *shmaddr)
// 调用shmat函数的返回值
shmctl():可以对共享内存段进行多种操作,常用为删除共享内存
int shmctl(int shmid, int cmd, struct shmid_ds *buf)
// shmid为所要操作的共享内存段的标识符
//s truct shmid_ds型指针参数buf的作用与参数cmd的值相关
// cmd指明了所要进行的操作,如IPC_RMID为删除
例程
/* read.c*/
#include<stdio.h>
#include<sys/types.h>
#include<sys/ipc.h>
#include<sys/shm.h>
#include<errno.h>
#include<stdlib.h>
#include<signal.h>
#include<string.h>
typedef struct
{
pid_t pid;
char buf[64];
}MSG;
void func(int sig)
{
}
int main(int argc, const char *argv[])
{
key_t key;
int shmid;
MSG *p;
key = ftok(".",100);
pid_t peerpid;
signal(SIGUSR1,func);
if((shmid = shmget(key,sizeof(MSG),0664 | IPC_CREAT | IPC_EXCL)) == -1)
{
if(errno == EEXIST)//共享内存已经存在,只需要打开共享内存
{
shmid = shmget(key,sizeof(MSG),0664);
p = (MSG *)shmat(shmid,NULL,0);
peerpid = p->pid;
p->pid = getpid();
kill(peerpid,SIGUSR1);
}
else
{
perror("shmget");
exit(1);
}
}
else //当前进程创建并且打开
{
p = (MSG *)shmat(shmid,NULL,0);
//写自己的进程号到共享内存
//挂起
//读取对方的pid
p->pid = getpid();
pause();
peerpid = p->pid;
}
while(1)
{
//挂起
//读取数据
//发信号给对方
pause();
if(strncmp(p->buf,"quit",4) == 0)
break;
printf("%s\n",p->buf);
kill(peerpid,SIGUSR1);
}
shmdt(p);
shmctl(shmid,IPC_RMID,NULL);
return 0;
}
/* write.c*/
#include<stdio.h>
#include<sys/types.h>
#include<sys/ipc.h>
#include<sys/shm.h>
#include<errno.h>
#include<stdlib.h>
#include<signal.h>
#include<string.h>
typedef struct
{
pid_t pid;
char buf[64];
}MSG;
void func(int sig)
{
}
int main(int argc, const char *argv[])
{
key_t key;
int shmid;
pid_t peerpid;
MSG *p;
key = ftok(".",100);
signal(SIGUSR1,func);
if((shmid = shmget(key,sizeof(MSG),0664 | IPC_CREAT | IPC_EXCL)) == -1)
{
if(errno == EEXIST)//共享内存已经存在,只需要打开共享内存
{
shmid = shmget(key,sizeof(MSG),0664);
p = (MSG *)shmat(shmid,NULL,0);
//读取对方的pid
//写自己的pid
//发信号告诉对方
peerpid = p->pid;
p->pid = getpid();
kill(peerpid,SIGUSR1);
}
else
{
perror("shmget");
exit(1);
}
}
else //当前进程创建并且打开
{
p = (MSG *)shmat(shmid,NULL,0);
p->pid = getpid();
pause();
peerpid = p->pid;
}
while(1)
{
//写数据
//告诉对方写过了数据,发信号给对方
//挂起
fgets(p->buf,sizeof(p->buf),stdin);
kill(peerpid,SIGUSR1);
if(strncmp(p->buf,"quit",4) == 0)
break;
pause();
}
shmdt(p);
return 0;
}
上述为测试,实际使用中,共享内存之间数据通过共享内存的拷贝函数,memcpy();
五、消息队列
消息队列就是一个消息的链表。可以把消息看作一个记录,具有特定的格式以及特定的优先级。对消息队列有写权限的进程可以向中按照一定的规则添加新消息;对消息队列有读权限的进程则可以从消息队列中读走消息。消息队列是随内核持续的。目前主要有两种类型的消息队列:POSIX消息队列以及系统V消息队列,系统V消息队列目前被大量使用。考虑到程序的可移植性,新开发的应用程序应尽量使用POSIX消息队列。
对消息队列的操作:
- 打开或创建消息队列
消息队列的内核持续性要求每个消息队列都在系统范围内对应唯一的键值,所以,要获得一个消息队列的描述字,只需提供该消息队列的键值即可;
注:消息队列描述字是由在系统范围内唯一的键值生成的,而键值可以看作对应系统内的一条路经。
- 读写操作
消息读写操作非常简单,对开发人员来说,每个消息都类似如下的数据结构:
struct msgbuf{
long mtype;
char mtext[1];
};
mtype成员代表消息类型,从消息队列中读取消息的一个重要依据就是消息的类型;mtext是消息内容,当然长度不一定为1。因此,对于发送消息来说,首先预置一个msgbuf缓冲区并写入消息类型和内容,调用相应的发送函数即可;对读取消息来说,首先分配这样一个msgbuf缓冲区,然后把消息读入该缓冲区即可。
注意:封装消息结构体的时候,将消息类型作为第一个成员。
- 获得或设置消息队列属性:
消息队列的信息基本上都保存在消息队列头中,因此,可以分配一个类似于消息队列头的结构(struct msqid_ds),来返回消息队列的属性;同样可以设置该数据结构。
实现函数
msgget()
int msgget(key_t key, int msgflg)
// 参数key是一个键值,由ftok获得;
// msgflg参数是一些标志位。该调用返回与健值key相对应的消息队列描述字。msgflg可以为以下:IPC_CREAT、IPC_EXCL、IPC_NOWAIT或三者的或结果。
//调用返回:成功返回消息队列描述字,否则返回-1。
在以下两种情况下,该调用将创建一个新的消息队列:
- 如果没有消息队列与健值key相对应,并且msgflg中包含了IPC_CREAT标志位;
- key参数为IPC_PRIVATE;
注:参数key设置成常数IPC_PRIVATE并不意味着其他进程不能访问该消息队列,只意味着即将创建新的消息队列。
msgrcv()
int msgrcv(int msqid, struct msgbuf *msgp, int msgsz, long msgtyp, int msgflg);
//msqid为消息队列描述字
//消息返回后存储在msgp指向的地址
//msgsz指定msgbuf的mtext成员的长度(即**消息内容的长度**)
//msgtyp为请求读取的消息类型;读消息标志msgflg可以为以下几个常值的或:
- IPC_NOWAIT 如果没有满足条件的消息,调用立即返回,此时,errno=ENOMSG
- IPC_EXCEPT 与msgtyp>0配合使用,返回队列中第一个类型不为msgtyp的消息
- IPC_NOERROR 如果队列中满足条件的消息内容大于所请求的msgsz字节,则把该消息截断,截断部分将丢失。
该系统调用从msgid代表的消息队列中读取一个消息,并把消息存储在msgp指向的msgbuf结构中。
msgrcv()解除阻塞的条件有三个:
- 消息队列中有了满足条件的消息;
- msqid代表的消息队列被删除;
- 调用msgrcv()的进程被信号中断;
msgsnd()
int msgsnd(int msqid, struct msgbuf *msgp, int msgsz, int msgflg);
向msgid代表的消息队列发送一个消息,即将发送的消息存储在msgp指向的msgbuf结构中,消息的大小由msgze指定。
对发送消息来说,有意义的msgflg标志为IPC_NOWAIT,指明在消息队列没有足够空间容纳要发送的消息时,msgsnd是否等待。造成msgsnd()等待的条件有两种:
- 当前消息的大小与当前消息队列中的字节数之和超过了消息队列的总容量;
- 当前消息队列的消息数(单位”个”)不小于消息队列的总容量(单位”字节数”),此时,虽然消息队列中的消息数目很多,但基本上都只有一个字节。
msgsnd()解除阻塞的条件有三个:
- 不满足上述两个条件,即消息队列中有容纳该消息的空间;
- msqid代表的消息队列被删除;
- 调用msgsnd()的进程被信号中断;
msgctl()
int msgctl(int msqid, int cmd, struct msqid_ds *buf);
// 该系统调用对由msqid标识的消息队列执行cmd操作
共三种cmd操作:IPC_STAT、IPC_SET 、IPC_RMID。
- IPC_STAT:该命令用来获取消息队列信息,返回的信息存贮在buf指向的msqid结构中;
- IPC_SET:该命令用来设置消息队列的属性,要设置的属性存储在buf指向的msqid结构中;可设置属性包括:msg_perm.uid、msg_perm.gid、msg_perm.mode以及msg_qbytes,同时,也影响msg_ctime成员。
- IPC_RMID:删除msqid标识的消息队列;
例程
/*send.c*/
#include<stdio.h>
#include<sys/types.h>
#include<sys/ipc.h>
#include<sys/msg.h>
#include<stdlib.h>
#define TYPE 100
typedef struct
{
long type;
char buf[64];
}MSG;
int len = sizeof(MSG) - sizeof(long); // *求消息正文大小*
int main(int argc, const char *argv[])
{
key_t key;
int msgid;
MSG info;
key = ftok(".",'a');
if((msgid = msgget(key,0664 | IPC_CREAT)) == -1)
{
perror("msgget");
exit(1);
}
while(1)
{
fgets(info.buf,sizeof(info.buf),stdin);
info.type = TYPE;
msgsnd(msgid,&info,len,0);
}
return 0;
}
/*recv.c*/
#include<stdio.h>
#include<sys/types.h>
#include<sys/ipc.h>
#include<sys/msg.h>
#include<stdlib.h>
#define TYPE 100
typedef struct
{
long type;
char buf[64];
}MSG;
int len = sizeof(MSG) - sizeof(long);
int main(int argc, const char *argv[])
{
key_t key;
int msgid;
MSG info;
key = ftok(".",'a');
if((msgid = msgget(key,0664 | IPC_CREAT)) == -1)
{
perror("msgget");
exit(1);
}
while(1)
{
msgrcv(msgid,&info,len,TYPE,0);
printf("%s\n",info.buf);
}
return 0;
}
进程间通信常用:涉及数据吞吐量大用共享内存,发送信息用消息队列