最近因为项目原因耽误了一些时间,导致进程间的通讯一直没有来的及学习及整理。在我们之前学过进程相关的概念和操作,还学习了轻量级的进程的线程,在我们之后的开发和面试中,多线程,多进程开发都是非常重要的,那么进程间通讯的机制是非常重要的,那么linux中进程间有哪些通讯方式呢?接下来我们详细的开始学习一下。
目前的linux中包含很多种的通信机制,现在的进程通讯可谓是集百家之长,从各种机制中继承而来。详细的发展历程就不说了,感兴趣的可以理解一下。可以通过下面的框图大概的理解一下关系。
看到这么些通讯方式,回想到了之前的线程间通讯中也有信号量,那里面学习的信号量属于POSIX信号量中的无名信号量。
今天学习的属于system v IPC的信号量,其实这两种信号量原理上是一样的,都是用来进行同步,他们只是在实现的机制上有所不同,我们不必纠结,直接学习就可以了。
下面我们先开始UNIX 系统IPC最古老的形式的无名管道和有名管道,然后再学习system v iPC的消息队列,信号量及共享内存。
一.管道
管道有无名管道(主要用在父子进程间)和有名管道(无亲缘关系也可以)。
无名管道呢速度慢,容量有限。有名管道进程通讯都可以,只是速度也比较慢。
无名管道
1.无名管道特点:
1>它是半双工的(即数据只能在一个方向上流动),具有固定的读端和写端。
2>它只能用于具有亲缘关系的进程之间的通信(也是父子进程或者兄弟进程之间)。
3>它可以看成是一种特殊的文件,对于它的读写也可以使用普通的read、write 等函数。但是它不是普通的文件,并不属于其他任何文件系统,并且只存在于内存中.
2.无名管道的读写特性:
1>读无名管道
写端存在,有数据读取,无数据阻塞
写端不存在,无数据立即返回
2>写无名管道
写无名管道,读端存在有空间写,无空间阻塞
读端不存在,管道破裂。
3.如何获取无名管道的大小
int main(){
int pfd[2];
pid_t pid;
int re;
int i;
re = pipe(pfd);
if(re == -1){
perror("pipe");
return -1;
}
for(i=0;i<1000000;i++){
write(pfd[1],"a",1);
printf("i=%d\n",i);
}
}
一直到阻塞,就可以看出管道有多大。
4.无名管道的读写
int main(){
int pfd[2];
pid_t pid;
int re;
char buf[NUM];
if(pipe(pfd)<0){
perror("pipe");
return 0;
}
pid = fork();
if(pid<0){
perror("fork");
return 0;
}else if(pid ==0){
while(1){
stpcpy(buf,"pipe test from child");
write(pfd[1],buf,strlen(buf));
sleep(1);
}
}else{
while(1){
memset(buf,0,NUM);
re = read(pfd[0],buf,NUM);
if(re>0){
printf("buf=%s\n",buf);
}
}
}
}
有名管道
1.有名管道读写操作:
写端
int main(){
int re;
int fd;
char buf[32];
unlink("/myfifo");
re = mkfifo("/myfifo",0666);
if(re==-1){
perror("mkfifo");
return -1;
}
fd = open("/myfifo",O_WRONLY);
if(fd<0){
perror("open");
return -1;
}
strcpy(buf,"fifo write test");
while(1){
write(fd,buf,strlen(buf));
sleep(1);
}
}
读端
int main(){
int re;
int fd;
char buf[32];
fd = open("/myfifo",O_RDONLY);
if(fd<0){
perror("open");
return -1;
}
while(1){
memset(buf,0,32);
read(fd,buf,32);
printf("%s\n",buf);
sleep(1);
}
}
二.信号
这里说的信号和我们的信号量不是一回事,信号是在软件层次上对中断机制的一种模拟,是一种异步通信方式。linux内核通过信号通知用户进程,不同的信号类型代表不同的事件,比如我们使用ctrl+C 来结束终端的运行 ,kill杀死一个进程都是信号的应用。
1.进程对信号有不同的响应方式
缺省方式(给你信号一个默认的处理程序)
忽略信号(忽略你给的信号)
捕捉信号(来了信号之后,执行自己定义的代码)
2.常用的信号:
终端下,执行一个应用程序,当关闭终端的时候,终端就会发送sighup信号给应用程序
,让进程终止。
如我们使用ctrl+C 来结束终端的运行,就是sigint
kill杀死一个进程,如果杀不死可以kill -9 ,-9就是用了sigkill信号,这个信号不能被捕捉和忽略。(留了个后手,防止是病毒杀不死)可以通过kill -l查看各个数字代表的信号值。
进程的前后台切换,bg,fg等都是用信号来实现的。
3.信号的发送:
1> int kill(pid_t pid, int sig);
pid 接收进程的进程号:
0代表同组进程; -1代表所有进程
sig 信号类型
2>int raise(int sig); //给自己发信号
3>int alarm(unsigned int seconds); //常用于超时检测
成功时返回上个定时器的剩余时间,失败时返回EOF
seconds 定时器的时间
一个进程中只能设定一个定时器,时间到时产生SIGALRM
4>int pause(void);
进程一直阻塞,直到被信号中断
被信号中断后返回-1,errno为EINTR
5>void (*signal(int signo, void (*handler)(int)))(int); typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);
成功时返回原先的信号处理函数,失败时返回SIG_ERR
signo 要设置的信号类型
handler 指定的信号处理函数: SIG_DFL代表缺省方式;
可以通过返回值恢复之前的信号的行为。
例1,下面的例子,当我们此时在使用ctrl+c并不会退出程序,而是执行handle程序
void handle(int sig){
if(sig == SIGINT){
printf("I got a ctrl+C signal\n");
}else if(sig == SIGQUIT){
printf("I got a quit signal\n");
}else{
printf("other siganl\n");
}
}
int main(){
signal(SIGINT,handle);
signal(SIGQUIT,handle);
signal(SIGHUP,handle);
while(1){
sleep(1);
}
}
可以通过返回值恢复之前的信号的行为。
typedef void (*sight)(int);
sight oldhandle;
int count =0;
void functh(int sig){
count++;
printf("I catch ctrl+c!!!\n");
if(count==2){
signal(SIGINT,oldhandle);
}
}
int main(){
oldhandle = signal(SIGINT,functh);
while(1) sleep(1);
}
signal(SIGINT,SIG_DFL);也可以用SIG_DFL代表缺省值,同样可以恢复。
例2,子进程结束会发送SIGCHLD这个信号,当时使用wait函数进行回收,因为wait是阻塞的,但是会导致父进程无法进行自己的工作,通过signal接收子进程的信号,来处理僵尸进程,这个时候就能够解放父进程。
void asasa(int sig){
wait(NULL);
}
int main(){
pid_t pid;
pid = fork();
signal(SIGCHLD,asasa);
if(pid<0){
perror("fork");
}else if(pid==0){
sleep(10);
exit(0);
}
while(1){
printf("hahahah,I am free!!!!!\n");
sleep(1);
}
}
三.共享内存
从上面我们也知道system v IPC,然后我们就开始学习IPC对象
- IPC 对象包含: 共享内存、消息队列和信号灯集
- 每个IPC对象有唯一的ID
- IPC对象创建后一直存在,直到被显式地删除
- 每个IPC对象有一个关联的KEY
通过ipcs 查看系统中的IPC对象。
要想使用IPC对象,我们肯定是要先创建他们
1.创建一个IPC对象
key_t ftok(const char *path, int proj_id);
- 成功时返回合法的key值,失败时返回EOF
- path 存在且可访问的文件的路径
- proj_id 用于生成key的数字,范围1-255。
例:
key_t key;
if ((key = ftok(“.”, ‘a’)) == -1) {
perror(“key”);
exit(-1);
}
2.共享内存特点
- 共享内存是一种最为高效的进程间通信方式,进程可以直接读写内存,而不需要任何数据的拷贝
- 共享内存在内核空间创建,可被进程映射到用户空间访问,使用灵活
- 由于多个进程可同时访问共享内存,因此需要同步和互斥机制配合使用
3.共享内存使用步骤:
- 创建/打开共享内存
- 映射共享内存,即把指定的共享内存映射到进程的地址空间用于访问
- 读写共享内存
- 撤销共享内存映射
- 删除共享内存对象
3.1共享内存的创建
int shmget(key_t key, int size, int shmflg);
- 成功时返回共享内存的id,失败时返回EOF
- key 和共享内存关联的key,IPC_PRIVATE 或 ftok生成
- shmflg 共享内存标志位 IPC_CREAT|0666
- 创建/打开一个和key关联的共享内存,大小为1024
- 字节,权限为0666
例:
key_t key;
int shmid;
if ((key = ftok(“.”, ‘m’)) == -1) {
perror(“ftok”);
exit(-1);
}
if ((shmid = shmget(key, 1024, IPC_CREAT|0666)) < 0) {
perror(“shmget”);
exit(-1);
}
3.2共享内存的映射
include <sys/ipc.h>
#include <sys/shm.h>
void *shmat(int shmid, const void *shmaddr, int shmflg);
- 成功时返回映射后的地址,失败时返回(void *)-1
- shmid 要映射的共享内存id
- shmaddr 映射后的地址, NULL表示由系统自动映射
- shmflg 标志位 0表示可读写;SHM_RDONLY表示只读
3.3 共享内存读写
例如:在共享内存中存放键盘输入的字符串
char *addr;
int shmid;
……
if ((addr = (char *)shmat(shmid, NULL, 0)) == (char *)-1) {
perror(“shmat”);
exit(-1);
}
fgets(addr, N, stdin);
3.4 撤销共享内存
int shmdt(void *shmaddr);
- 成功时返回0,失败时返回EOF
- 不使用共享内存时应撤销映射
- 进程结束时自动撤销
3.4 删除共享内存
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
- 成功时返回0,失败时返回EOF
- shmid 要操作的共享内存的id
- cmd 要执行的操作 IPC_STAT IPC_SET IPC_RMID
- buf 保存或设置共享内存属性的地址
3.5举例,创建,共享,撤销。
记得通过ipcs查看是否创建成功。
int main(){
key_t key;
int shmid;
char *addr;
key = ftok(".",23);
if(key==-1){
perror("ftok");
return -1;
}
shmid = shmget(key,1024,IPC_CREAT|0666);
if(shmid==-1){
perror("shmget");
return -1;
}
addr = shmat(shmid,NULL,0);
//strcpy(addr,"this my share memeory");
printf("get share mem=%s\n",addr);
shmdt(addr);
shmctl(shmid,IPC_RMID,NULL);
}
四.消息队列
接下来学习IPC的第二大将,消息队列,属于IPC对象,肯定ipc对象的创建等是必不可少的了。
- 消息队列是System V IPC对象的一种
- 消息队列由消息队列ID来唯一标识
- 消息队列就是一个消息的列表。用户可以在消息队列中添加消息、读取消息等
- 消息队列可以按照类型来发送/接收消息
消息队列的使用:
1.打开/创建消息队列
int msgget(key_t key, int msgflg);
- 成功时返回消息队列的id,失败时返回EOF
- key 和消息队列关联的key IPC_PRIVATE 或 ftok
- msgflg 标志位 IPC_CREAT|0666
2.向消息队列发送消息
int msgsnd(int msgid, const void *msgp, size_t size,int msgflg);
- 成功时返回0,失败时返回-1
- msgid 消息队列id
- msgp 消息缓冲区地址
- size 消息正文长度
- msgflg 标志位 0 或 IPC_NOWAIT
3.从消息队列接收消息 msgrcv
int msgrcv(int msgid, void *msgp, size_t size, long msgtype, int msgflg);
- 成功时返回收到的消息长度,失败时返回-1
- msgid 消息队列id
- msgp 消息缓冲区地址
- size 指定接收的消息长度
- msgtype 指定接收的消息类型
- msgflg 标志位 0 或 IPC_NOWAIT
4.控制消息队列
int msgctl(int msgid, int cmd, struct msqid_ds *buf);
- 成功时返回0,失败时返回-1
- msgid 消息队列id
- cmd 要执行的操作 IPC_STAT / IPC_SET / IPC_RMID
- buf 存放消息队列属性的地址
5.消息格式
- 通信双方首先定义好统一的消息格式
- 用户根据应用需求定义结构体类型
- 首成员类型必须为long,代表消息类型(正整数)
- 其他成员都属于消息正文
- 消息长度不包括首类型 long
例:
消息队列接收
#include <linux/msg.h>
#include <string.h>
#include <stdlib.h>
typedef struct{
long type;
char txt[64];
}MSG;
int msgid;
#define LEN sizeof(MSG)-sizeof(long)
void rmmsg(int sig){
msgctl(msgid,IPC_RMID,NULL);
exit(0);
}
int main(){
key_t ipkey;
int re;
MSG msg_t;
ipkey = ftok(".",23);
if(ipkey==-1){
perror("ftok");
return -1;
}
msgid = msgget(ipkey,IPC_CREAT|0666);
if(msgid ==-1){
perror("msgget");
return -1;
}
signal(SIGINT,rmmsg);
while(1){
re = msgrcv(msgid,&msg_t,LEN,3,MSG_EXCEPT);
printf("receive msg:type=%d,txt=%s\n",(int)msg_t.type,msg_t.txt);
if(re<=0){
break;
}
}
}
消息队列发送
#include <stdio.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <string.h>
typedef struct{
long type;
char txt[64];
}MSG;
#define LEN sizeof(MSG)-sizeof(long)
int main(){
key_t ipkey;
int msgid;
MSG msg_t;
ipkey = ftok(".",23);
if(ipkey==-1){
perror("ftok");
return -1;
}
msgid = msgget(ipkey,IPC_CREAT|0666);
if(msgid ==-1){
perror("msgget");
return -1;
}
msg_t.type = 1;
strcpy(msg_t.txt,"msg type one");
msgsnd(msgid,&msg_t,LEN,0);
msg_t.type = 2;
strcpy(msg_t.txt,"msg type two");
msgsnd(msgid,&msg_t,LEN,0);
}
进行编译查看下效果:gcc -o msg_recv recv gcc -o msc_send send 然后执行./send 此时再执行./recv就可以看到打印信息了
五.信号量
信号灯也叫信号量,用于进程/线程同步或互斥的机制,之前我们也说过与之前学的信号灯原理是相同的,只是实现方式不同。
1.信号灯的类型
- Posix 无名信号灯(前面线程学过)
- Posix有名信号灯
- System V 信号灯
2.信号灯的含义
计数信号灯
3.特点:
- System V 信号灯是一个或多个计数信号灯的集合
- 可同时操作集合中的多个信号灯
- 申请多个资源时避免死锁
4.信号灯使用步骤
4.1打开/创建信号灯
int semget(key_t key, int nsems, int semflg);
- 成功时返回信号灯的id,失败时返回-1
- key 和消息队列关联的key IPC_PRIVATE 或 ftok
- nsems 集合中包含的计数信号灯个数
- semflg 标志位 IPC_CREAT|0666 IPC_EXCL
4.2信号灯初始化
int semctl(int semid, int semnum, int cmd, …);
- 成功时返回0,失败时返回EOF
- semid 要操作的信号灯集id
- semnum 要操作的集合中的信号灯编号
- cmd 执行的操作 SETVAL IPC_RMID
- union semun 取决于cmd
4.3 P/V操作 semop
int semop(int semid, struct sembuf *sops, unsigned nsops);
- 成功时返回0,失败时返回-1
- semid 要操作的信号灯集id
- sops 描述对信号灯操作的结构体(数组)
- nsops 要操作的信号灯的个数
struct sembuf
{
short sem_num;
short sem_op;
short sem_flg;
};
- semnum 信号灯编号
- sem_op -1:P操作 1:V操作
- sem_flg 0 / IPC_NOWAIT
删除信号灯 semctl
例:
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <errno.h>
#include <sys/sem.h>
#include <string.h>
union semun{
int val;
};
#define SEM_READ 0
#define SEM_WRITE 0
poperation(int index,int semid){
struct sembuf sop;
sop.sem_num = index;
sop.sem_op = -1;
sop.sem_flg = 0;
semop(semid,&sop,1);
}
voperation(int index,int semid){
struct sembuf sop;
sop.sem_num = index;
sop.sem_op = 1;
sop.sem_flg = 0;
semop(semid,&sop,1);
}
int main(){
key_t key;
int semid;
int shmid;
pid_t pid;
char *shmaddr;
key = ftok(".",123);
semid = semget(key,2,IPC_CREAT|0777);
if(semid<0){
perror("semget");
return -1;
}
shmid = shmget(key,256,IPC_CREAT|0777);
if(shmid<0){
perror("shmget");
return -1;
}
union semun myun;
myun.val = 0;
semctl(semid,SEM_READ,SETVAL,myun);
myun.val = 1;
semctl(semid,SEM_WRITE,SETVAL,myun);
pid = fork();
if(pid<0){
perror("fork");
return -1;
}else if(pid == 0){
shmaddr = (char*)shmat(shmid,NULL,0);
while(1){
poperation(SEM_READ,semid);
printf("getshm:%s",shmaddr);
voperation(SEM_WRITE,semid);
}
}else{
shmaddr = (char*)shmat(shmid,NULL,0);
while(1){
poperation(SEM_WRITE,semid);
printf("please input char to shm:");
fgets(shmaddr,32,stdin);
voperation(SEM_READ,semid);
}
}