C语言-Linux进程间通信方式
1、进程间通信方式概述
进程间通信方式有:
管道(Pipo)和有名管道(FIFO):用于具有亲缘关系进行间通信,有名管道,允许无亲缘关系进程间的通信
信号(Signal):比较复杂的通信方式,用于通知接收进程有某种事件发生
消息队列:消息的连接表,包括Posix消息队列和System V消息队列
信号量(Signal)/信号灯:主要被用作进程间或同一进程不同线程之间的同步手段
共享内存:最有用的进程间通信方式,需要某种同步机制,互斥锁和信号量都可以
套接字:用于不同机器之间的进程间通信进程间通信的目的
数据传输、共享数据、通知事件、资源共享、进程控制
2、管道通信
特点:
半双工,数据只能一个方向流动,进行双方通信的时候要建立两个管道,用于父子间或兄弟进程间通信
一个进程向管道中写的数据内容被管道另一端的进程读出
写入的数据每次都添加在管道缓冲区的末尾,并且每次都是从缓冲区的头部读出数据
一般的I/O函数都可以用于管道,如close、read、write等读数据步骤:
若写端不存在,则认为以及读到了数据的末尾,读函数返回读出的字节数为0
若管道的写端存在,请求的字节数目大于PIPE_BUE(定义在include/linux/limits中),返回现有字节数,不大于的话,返回现有字节数或请求字节数
在管道写端关闭后,写入的数据将一直存在,直到被读出为止写端对读端的依赖性:
在向进程中写入数据时,至少应该存在某一个进程,其中管道读端没有被关闭。否则就会出现管道断裂,进程收到SIGPIPE信号,默认动作为进程终止
2.1 创建无名管道
函数详解
表头文件#include <unistd.h>定义函数int pipe(int filedes[2]);函数说明用于建立管道将文件描述符由参数filedes数组返回filedes[0] 为管道的读取端filedes[1] 为管道的写入端返回值成功返回零,否则返回-1
综合案例
#include <stdio.h>
#include <unistd.h>int main(int argc,char *argv[])
{int filedes[2];char buffer[80];pipe(filedes);if(fork() > 0){char s[] = "hello!\n";write(filedes[1],s,sizeof(s));}else{read(filedes[0],buffer,80);printf("father %s",buffer);}return 0;
}
运行结果
linux@ubuntu:~/test$ gcc pipe_dome.c -o pipe_dome
linux@ubuntu:~/test$ ./pipe_dome
father hello!
2.2 创建有名管道
函数详解
表头文件#include <sys/types.h>#include <sys/stat.h>定义函数int mkfifo(const char* pathname,mode_t mode);函数说明pathname: 建立特殊的FIFO文件,该文件必须不存在mode: 描述pahtname文件的权限当使用open()打开文件时,O_NONBLOCK旗标会有下列影响:当使用O_NONBLOCK旗标时,打开文件来读取操作会立刻返回;若没有其他进程打开文件来读取,则写入操作会返回ENXIO错误没有使用O_NONBLOCK旗标时,打开FIFO文件读取操作会等到其他进程打开FIFO文件进行写入才能正常返回。同样,打开FIFO文件进行写入操作会等其他进程打开FIFO文件进程读取后才能正常返回
综合案例
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#define FIFO "/home/linux/test/2"int main(int argc,char *argv[])
{char buffer[80];int fd;unlink(FIFO); //unlink删除指定的文件,若文件存在则删除它,确保后边mkfifo建立的特殊文件不存在mkfifo(FIFO,0666);if(fork() > 0){char s[] = "hello!\n";fd = open(FIFO,O_WRONLY);write(fd,s,sizeof(s));close(fd);}else{fd = open(FIFO,O_RDONLY);read(fd,buffer,80);printf("%s",buffer);close(fd);}return 0;
}
运行结果
linux@ubuntu:~/test$ gcc mkfifo.c -o mkfifo
linux@ubuntu:~/test$ ./mkfifo
hello!
3、信号
信号的本质:
在软件层次上对中断机制的一种模拟。异步的信号来源:
硬件来源:按下了键盘或者其他硬件故障
软件来源:发送信号的系统函数:kill、raise、alarm、setitimer、非法运算操作分类:
性能:
可靠:克服了信号可能丢失问题,这些信号支持排队
不可靠:从UNIX继承过来的信号,进程每次处理信号后,就将对信号的相应设置为默认动作,有时候会错误处理,或信号丢失
时间:
实时:支持排队,都是可靠信号
非实时:不支持排队,是不可靠信号信号的处理方式:
忽略信号:即对信号不做任何处理,SIGKILL和SIGSTOP不能忽略
捕捉信号:定义信号处理函数,当信号发生时,执行相应的处理函数
执行默认操作:进程对实时信号的默认操作反应是进程终止信号发送的主要函数有:
kill、raise、sigqueue、alarm、setitimer、abort
3.1 信号发送函数kill和alarm
函数详解
表头文件#include <sys/types.h>#include <signal.h>定义函数int kill(pid_t pid,int sig);函数说明用于发送信号给指定进程pid: 指定的进程pid>0:将信号传递给指定的pid进程pid=0:将信号传给目前进程相同的进程组的所有进程pid=-1:将信号广播传送给系统内所有的进程pid<0:将信号传给进程组识别码为pid绝对值的所有进程sig:指定要传送的信号返回值执行成功将返回0,如果有错误则返回-1
综合案例
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <signal.h>int main(int argc,char *argv[])
{pid_t pid;int status;pid = fork();if(pid == 0){printf("I an child process!\n");sleep(10);}else{printf("send signal to child process (%d)\n",pid);sleep(1);kill(pid,SIGABRT); //向子进程中发送信号6wait(&status); //等待子进程中断或结束,将结束信号返回if(WIFSIGNALED(status)) //判断子进程是否因信号而结束printf("child process receive signal %d\n",WTERMSIG(status)); //获取子进程因信号而中止的信号代码}return 0;
}
运行结果
linux@ubuntu:~/test$ gcc kill_dome.c -o kill_dome
linux@ubuntu:~/test$ ./kill_dome
send signal to child process (25564)
I an child process!
child process receive signal 6
函数详解
表头文件#include <unistd.h>定义函数unsigned int alarm(unsigned int seconds);函数说明用来设置信号传送闹钟设置信号SIGALRM,在经过seconds指定的秒数后传送给目前的进程若参数为0,则之前设置的闹钟会被取消,剩下的时间返回返回值返回之前闹钟剩余的秒数,如果之前未设置闹钟则返回0
综合案例
#include <stdio.h>
#include <unistd.h>
#include <signal.h>void handler()
{printf("hello\n");
}int main(int argc,char *argv[])
{int i;signal(SIGALRM,handler);alarm(5);for(i = 1;i < 7;i++){printf("sleep %d ..\n",i);sleep(1);}return 0;
}
运行结果
linux@ubuntu:~/test$ gcc ararm_dome.c -o alarm_dome
linux@ubuntu:~/test$ ./alarm_dome
sleep 1 ..
sleep 2 ..
sleep 3 ..
sleep 4 ..
sleep 5 ..
hello
sleep 6 ..
3.2 自定义信号处理方式
3.2.1 signal函数
函数详解
表头文件#include <signal.h>
定义函数void (*signal(int signum,void(*handler)(int)))(int);
函数说明用于传送信号给指定的进程signum: 指定信号编号来设置该信号的处理函数void(*handler)(int): 信号处理函数如果参数handler不是函数指针,则必是下列两个常数之一SIG_IGN:忽略参数signum指定信号SIG_DFL:将参数signum指定的信号重设为核心预设的信号处理方式
返回值返回先去的信号处理函数指针,若错误则返回-1注意:信号跳转到自定的handler处理函数后,系统会自动将此函数换回原来系统预设的处理方式,若要改变此操作,采用sigaction
综合案例
#include <stdio.h>
#include <signal.h>
#include <stdlib.h>void my_func(int sign_no)
{if(sign_no == SIGINT)printf("I have get SIGINT\n");else if(sign_no == SIGQUIT)printf("I have get SIGQUIT\n");
}int main(int argc,char *argv[])
{printf("Waiting for signal SIGINT or SIGQUIT\n");/*注册信号处理函数*/signal(SIGINT,my_func);signal(SIGQUIT,my_func);pause(); //暂停主进程,直到收到信号exit(0);return 0;
}
运行结果
linux@ubuntu:~/test$ ./signal_dome
Waiting for signal SIGINT or SIGQUIT
I have get SIGQUIT//新开窗口
linux@ubuntu:~$ ps -aux|grep signal_dome
Warning: bad ps syntax, perhaps a bogus '-'? See http://procps.sf.net/faq.html
linux 25920 0.1 0.5 15872 5560 pts/2 S+ 19:02 0:01 vi signal_dome.c
linux 25992 0.0 0.0 1988 280 pts/3 T 19:15 0:00 ./signal_dome
linux 26000 0.0 0.0 1988 284 pts/3 S+ 19:16 0:00 ./signal_dome
linux 26064 0.0 0.0 6044 836 pts/5 T 19:16 0:00 grep --color=auto -l signal_dome
linux 26071 0.0 0.0 6044 836 pts/5 S+ 19:17 0:00 grep --color=auto signal_dome
linux@ubuntu:~$ kill -SIGQUIT 26000 //向暂停的进程发送信号
3.2.2 sigaction函数
表头文件#include <signal>定义函数int sigaction(int signum,const struct sigaction *act,struct sigaction * oldact);函数说明用于查询或设置信号处理方式signum:指定信号编号来处理函数sigaction定义如下:struct sigaction{void (*sa_handler)(int); //新处理函数,同signal函数的参数handlersigset_t sa_mask; //处理该信号时暂时将sa_mask信号搁置int sa_flags; //用来处理信号的其他相关操作//A_NOCLDSTOP:如果参数signum为SIGCHLD,则当子进程暂停时并不会通知父进程//SA_ONESHOT/SA_RESETHAND:在调用新的信号处理函数前,将此信号处理方式改为系统预设的方式//SA_RESTART:被信号中断的系统调用会自行重启//SA_NOMASK/SA_NODEFER:在处理此信号未结束前不理会此信号的再次到来void (*sa_restorer)(void); //已过时,POSIX不支持它}若参数oldact不是NULL指针,则原来的信号处理方式会由结构sigaction返回返回值成功返回0,错误返回-1
3.3 信号集操作
信号集被定义为一种数据类型: typedef struct {unsigned long sig[_NSIG_WORDS]; }sigset_t
3.3.1 sigemptyset函数
表头文件#include <signal.h>定义函数int sigemptyset(sigset_t *set);函数说明用于初始化信号集将参数set初始化,并清空返回值执行成功返回0,错误返回-1
3.3.2 sigfillset函数
表头文件#include <signal.h>
定义函数int sigfillset(sigset_t *set);
函数说明用于将所有信号加入信号集将参数set初始化,然后把所有信号加入此信号集里返回值成功返回0.错误返回-1
3.3.3 sigaddset()函数
表头文件#include <signal.h>
定义函数int sigaddset(sigset_t *set,int signum);
函数说明增加一个信号到信号集用来将参数signum代表的信号加入至set中
返回值成功返回0.错误返回-1
3.3.4 sigdelset函数
表头文件#include <signal.h>
定义函数int sigdelset(sigset_t *set,int signum);
函数说明用于从信号集中删除一个信号用来将参数signum代表的信号从参数set中删除
返回值成功返回0.错误返回-1
3.3.5 sigismember函数
表头文件#include <signal.h>
定义函数int sigismember(const sigset_t *set,int signum);
函数说明测试某个信号是否已加入信号集内用来测试参数signum代表的信号是否加入参数set中
返回值成功返回0.错误返回-1
4、消息队列
概念
消息队列就是一个消息的链表。可以把消息看作一个记录,具有特定的格式和特定的优先级。对消息队列有写权限的进程可以按照一定的规则添加新消息,对于消息队列有读权限的进程则可以从消息队列中读走消息,消息队列是随内核持续的
类型
POSIX消息队列和系统V消息队列,考虑程序的移植性,尽量用POSIX消息队列基础理论
系统V消息队列是随内核持续的,只有在内核重启或者显示删除一个消息队列时,该消息队列才会真正被删除
系统中记录消息队列的数据结构(struct ipc_ids msg_ids)位于内核中 消息队列可以在msg_ids中找到访问入口
每个消息都有一个队列头,用struct msg_queue来描述对消息队列的操作
打开或创建消息队列:要获得消息队列描述词,需提供消息队列的键值
读写消息队列:每个消息都有类似如下的数据结构
struct msgbuf
{
long mtype; //消息类型
char mtext[1]; //消息内容,长度不一定为1
}
故对于发送消息来说,需要预置一个msgbuf缓冲区并写入消息内容,读同样
获取或设置消息队列属性
4.1 ftok函数
表头文件#include <sys/types.h>#include <sys/ipc.h>定义函数key_t ftok(char* pathname,char proj);
函数说明用于将文件名转化成键值pathname:相应的一个文件,必须存在,且在程序的访问范围内char:任意一个字符
返回值返回与文件对应的键值
4.2 msgget函数
表头文件#include <sys/types.h>#include <sys/ipc.h>#include <sys/msg.h>
定义函数int msgget(key_t key,int msgflg);
函数说明用于创建一个消息队列key:一个键值msgflg:标志位IPC_CREAT、IPC_EXCL、IPC_NOWAIT若没有消息队列与键值key对应,且msgflg包含IPC_CREAT标志位或参数key位IPC_PRIVATE则将创建一个新的消息队列
返回值成功返回队列描述词,否则返回-1
4.3 msgrcv函数
表头文件#include <sys/types.h>#include <sys/ipc.h>#include <sys/msg.h>
定义函数int msgrcv(int msqid,struct msgbuf *msgp,int msgsz,long msgtyp,int msgflg);
函数说明用于读出消息队列的数据msqid:消息队列描述词*msgp:将消息存在msgp所指向的msgbuf结构中msgsz:指定msgbuf的mtext的长度msgtyp:请求读取的消息类型msgflg:控制消息队列中没有相应类型的消息可以接收时将发生的事情IPC_NOWAIT:没有返回条件的消息,调用立即返回ENOMSG
返回值成功则返回读出消息的实际字节数,否则返回-1
4.4 msgsnd函数
表头文件#include <sys/types.h>#include <sys/ipc.h>#include <sys/msg.h>
定义函数int msgsnd(int msqid,struct msgbuf *msgp,int msgsz,int msgflg);
函数说明用于向消息队列中写入数据msqid:消息队列描述词*msgp:将存在msgp所指向的msgbuf结构中的消息进行发送msgsz:消息的大小msgflg:有意义的msgflg标志为IPC_NOWAIT,表明消息队列没有足够的空间容纳要发送的消息时,msgsnd是否等待
返回值成功返回0,否则返回-1
4.5 msgctl函数
表头文件#include <sys/types.h>#include <sys/ipc.h>#include <sys/msg.h>
定义函数int msgctl(int msqid,int cmd,struct msqid_ds *buf);
函数说明用于控制消息队列msqid:消息队列描述词cmd:IPC_STAT:获取消息队列信息,返回的信息存储在buf指向的msqid结构中IPC_SET:设置消息队列的属性,设置的属性存储在buf指向的msqid结构中IPC_RMID:删除msqid标识的消息队列
返回值成功返回0,否则返回-1
5、信号灯(了解)
作用:
提供对进程间共享资源的访问控制机制,相当于内存中的标志,进程可以根据它判定是否能够访问某些共享资源类型:
二值信号灯:最简单的信号灯形式,信号灯的值只能取0或1,类似于互斥锁
计算信号灯:信号灯的值可以取任意非负值内核实现原理:
系统V信号灯也是随内核持续的。在内核中由数据结构struct ipc_ids sem_ids记录,所有信号灯都可以在结构sem_ids中找到访问入口信号灯的使用
打开或创建信号灯:与消息队列的创建及打开相同
信号灯值操作:linux可以加减信号灯的值,相当于对共享资源的释放和占有
获得或设置信号灯属性:系统中的每一个信号灯集都对应一个struct sem_array结构,该结构记录了信号灯集的各种信息,存在于系统空间。为了设置、获得该信号灯集的各种信息和属性,在用户空间有一个重要的联合结构与之对应,即union semun
5.1 semget函数
表头文件#include <sys/types.h>#include <sys/ipc.h>#include <sys/msg.h>
定义函数int semget(key_t key,int nsems,int semflg);
函数说明配置信号灯key:是一个键值,由ftok获得,标识唯一一个信号灯集,用法与msgget中的key相同nsems:指定打开或者新创建的信号灯集中包含的信号灯的数目semflg:标志位,与msgget相同返回值成功返回信号灯集描述词,否则返回-1
5.2 semop函数
表头文件#include <sys/types.h>#include <sys/ipc.h>#include <sys/msg.h>
定义函数int semop(int semid,struct sembuf *sops,unsigned nsops);
函数说明用于信号灯处理semid:信号集IDsops:指向数组的每一个sembuf结构都描述一个特定信号灯上的操作nsops:sops数组的大小sembuf的结构为:struct sembuf{unsigned short sem_num; //对应集中的信号灯,0对应第一个信号灯short sem_op; //大于0,等于0,小于0确定了对sem_num指定的信号灯进行的3种操作short sem_flg; //IPC_NOWAIT和SEM_UNDO两个标志}
返回值成功返回0,否则返回-1
5.3 semctl函数
表头文件#include <sys/types.h>#include <sys/ipc.h>#include <sys/msg.h>
定义函数int semctl(int semid,int semnum,int cmd,union semun arg);
函数说明用于控制信号灯semid:指定信号灯集cmd:具体的操作类型IPC_STAT:获取信号灯信息,由arg.buf返回IPC_SET:设置信号灯信息,设置信息保存在arg.buf中CETALL:返回所有信号灯信息,结果保存在arg.array中CETNCNT:返回等待semnum所代表信号灯的值增加的进程数GETPID:返回最后一个对semnum所代表信号灯执行semop操作的进程IDGETVAL:返回semnum所代表信号灯的值GETZCNT:返回等待semnum所代表信号灯的值变成0的进程数SETALL:设置semnum所代表信号灯的值为arg.val
6、共享内存方式
概念
共享内存是最有用的进程间通信方式,也是最快的IPC形式。两个不同进程A,B共享内存的意思是,同一块物理内存被映射到进程A、B各自的进程地址空间,A和B都能看到对方内存中数据的更新,由于共用一块内存,必然需要某种同步机制,互斥锁和信号量都可以