进程间通信(IPC(InterProcess Communication))
在上一节进程中我们提到了,等待进程的机制是老张隔一段时间去厨房看一下水有没有烧开(非阻塞同步)。
跟站在旁边等水开(阻塞同步)相比,这样做有优势,但是仍然浪费了很多时间和资源。最好是买个能发出信号的水壶。
水开时通知老张,也就是需要进程间的通信。
进程间有几种经典通讯方式:管道、信号、消息队列、信号量、共享内存。
无名管道(匿名管道)
在linux系统中,一切皆是文件。管道、也是一种文件。因为是无名管道,也就是没有名字,没有文件描述符。
和普通文件不同的是,无名管道不占用内存。
管道是半双工的通讯方式,类似于stm32中的串口。我们这里复习一下半双工的概念:
单工:只支持单向的数据传输。比如电视 广播
半双工:同一时间,只能有一个方向的数据传输。比如对讲机
全双工:同一时间,可以互相发送数据,实现同时收发的功能。比如电话
这里介绍的管道是半双工通讯。同一时间,只能由一个进程写,另一个进程读。
(那么有没有全双工的管道呢?有。我们叫他流管道,后面有机会再说)
使用管道之前我们要创建一个管道,前面说过管道也是一个文件。那么创建管道就和创建文件类似
int pipe( int fd[2] );
fd[2] 其实就是前面文件iO中的文件描述符,这里的表达形式表示:
能且只能返回2个参数,分别是 fd[0] 和 fd[1],这两个文件描述符分别用来读、写。
成功返回0,失败返回 -1 .(所以可以用perror()了)
除了创建之外,读写关闭都可以用文件IO中的函数操作。
我们使用这个函数创建一个管道:
#include <unistd.h> #include <errno.h> #include <stdio.h> #include <stdlib.h> int main() { int pipe_fd[2]; /*创建一无名管道*/ if(pipe(pipe_fd)<0) { perror("pipe create error"); return -1; } else { printf("pipe create success\n"); } /*关闭管道描述符*/ close(pipe_fd[0]); close(pipe_fd[1]); }
那么如何使用这个管道?
创建一个管道的目的,自然是让父子进程都可以使用。也就是说让父子进程共用一个管道
这样才能保证他们之间的通信,那么就需要 pipe() 在 fork() 之前。否则子进程创建自己的管道他们之间信息传递不了
然后关掉父进程的读端fd[0] 子进程的写端fd[1]
变成了这个样子:
这样就达到了一个父进程写,子进程读的目的。
看下面的例子:
#include <unistd.h> #include <sys/types.h> #include <errno.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <sys/wait.h> int main() { int pipe_fd[2]; pid_t pid; char buf_r[100]; char* p_wbuf; int r_num; memset(buf_r,0,sizeof(buf_r)); /*创建管道*/ if(pipe(pipe_fd)<0) { perror("pipe create error"); return -1; } /*创建一子进程*/ if((pid=fork())==0) { printf("\n"); /*关闭子进程写描述符,并通过使父进程暂停 2 秒确保父进程已关闭相应的读描述符*/ close(pipe_fd[1]); sleep(2); /*子进程读取管道内容*/ if((r_num=read(pipe_fd[0],buf_r,100))>0) { printf("%d numbers read from the pipe is %s\n",r_num,buf_r); } /*关闭子进程读描述符*/ close(pipe_fd[0]); exit(0); } else if(pid>0) { /*/关闭父进程读描述符,并分两次向管道中写入 Hello Pipe*/ close(pipe_fd[0]); if(write(pipe_fd[1],"Hello",5)!= -1) printf("parent write1 success!\n"); if(write(pipe_fd[1]," Pipe",5)!= -1) printf("parent write2 success!\n"); /*关闭父进程写描述符*/ close(pipe_fd[1]); sleep(3); /*收集子进程退出信息*/ waitpid(pid,NULL,0); exit(0); } }
在fork()之后,父子进程分别关闭对应的读写端,使父进程只写 子进程只读
然后父进程写入 Hello Pipe,子进程读并打印,接下来分别关闭管道,父进程收尸退出。
因为无名管道的创建方式决定了,他只能用于有亲缘关系之间的进程通信。
向管道中写入数据时,linux 将不保证写入的原子性,管道缓冲区一有空闲区域,写进程就会试图向管道写入数据。如果读进程不读取管道缓冲区中的数据,那么写操作将会一直阻塞。
标准流管道
有这么一个函数,同时包含创建 打开 并启动另外一个进程。
FILE *popen(const char *command, const char *type)
command 一个shell命令
type r/w 分别表示输出/输入
成功返回0,失败返回-1
由popen创建的流管道必须由 pclose() 关闭。
int pclose(FILE *stream)
先用popen创建一个进程"ls -l",把内容写进新建的文件shell.c 中。
#include<stdio.h> #include<unistd.h> #include<string.h> int main() { FILE *fp=NULL; FILE *fh=NULL; char buff[1024]={0}; memset(buff,0,sizeof(buff)); fp=popen("ls -l","r");//将命令ls-l 同过管道读到fp fh=fopen("shell.c","w+");// 创建一个可写的文件 fread(buff,1,1024,fp);//将fp的数据流读到buff中 fwrite(buff,1,1024,fh);//将buff的数据写入fh指向的文件中 pclose(fp); fclose(fh); return 0; }
执行结果如下:
命名管道(有名管道)
上面提到的无名管道只能进行有亲缘关系的通信,那么命名管道就是解决无亲缘关系的进程之间通信的问题
也就是说 在同一个电脑上的进程,都可以通过命名管道通信。
和无名管道类似,命名管道也是文件,所以开关读写的文件IO操作同样可作用于此文件。
创建方法如下:
#include <sys/types.h> #include <sys/stat.h> int mkfifo(const char *pathname, mode_t mode);
int mknod(const char *pathname, mode_t mode, dev_t dev);
mkfifo是POSIX.1首先提出的。SVR3用mknod(2)系统调用创建FIFO。
而在SVR4中,mkfifo调用mknod创建FIFO。
POSIX.2已经建议了一个mkfifo(1)命令。SVR4和4.3+BSD现在支持此命令。
于是,用一条shell命令就可以创建一个FIFO,然后用一般的shell I/O重新定向对其进行存取。
以上摘自《UNIX环境高级编程》。下文中我们使用mkfifo()函数。
很类似我们的open函数,第一个参数是文件名,第二个参数mode是创建模式。dev是设备值,只有创建设备文件时会用到,一般填0。
mode参数可选值如下:
O_RDONLY:读管道
O_WRONLY:写管道
O_RDWR:读写管道
O_NONBLOCK:非阻塞
O_CREAT:如果该文件不存在,那么就创建一个新的文件,并用 第三的参数为其设置权限
O_EXCL:如果使用 O_CREAT 时文件存在,那么可返回错误消息。这一参数可测试文件是否存在
错误信息:
EACCESS 参数 filename 所指定的目录路径无可执行的权限
EEXIST 参数 filename 所指定的文件已存在
ENAMETOOLONG 参数 filename 的路径名称太长
ENOENT 参数 filename 包含的目录不存在
ENOSPC 文件系统的剩余空间不足
ENOTDIR 参数 filename 路径中的目录存在但却非真正的目录
EROFS 参数 filename 指定的文件存在于只读文件系统内
也可以从命令行直接创建管道
$ mkfifo myfifo
这样就创建了一个叫myfifo的有名管道
普通的文件操作:开关读写。
其中开open()和读read(),会阻塞运行函数。
当一个进程使用open()以只写的方式打开管道文件,程序会阻塞运行。等待另一个进程以只读方式打开该管道。
这个例子中我遇到了一个坑:
#include <stdio.h> #include <unistd.h> #include <strings.h> #include <string.h> #include <stdlib.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #define FIFO_NAME "/mnt/test/testfifo" int main(int argc, char* argv[]) { int fd; char buf[5] = "hello"; int ret ; ret = mkfifo(FIFO_NAME, 0777); if(ret == -1) { perror("mkfifo faild"); } fd = open(FIFO_NAME, O_WRONLY); write(fd, buf, strlen(buf)+1); close(fd); unlink(FIFO_NAME);//删除管道文件 sleep(1); return 0; }
#include <stdio.h> #include <unistd.h> #include <strings.h> #include <string.h> #include <stdlib.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #define FIFO_NAME "/mnt/test/testfifo" #define BUF_SIZE 1024 int main(int argc, char* argv[]) { int fd; char buf[BUF_SIZE]; memset(buf,0x31,5); fd = open(FIFO_NAME, O_RDONLY); if(fd < 0) { perror("open fifofile faild"); } read(fd, buf, BUF_SIZE); puts(buf); close(fd); return 0; }
其实很简单的程序,就是一个进程创建管道并且写入hello,然后另一个进程读hell并打印。
但是,我执行 ./fifoa 之后
我不信邪
所以又用上面提到的shell命令: mkfifo myfifo 试了一下
对,他就是把刚才perror返回给我的错误信息翻译了一遍。
这是文件权限的问题,所以我查看了我share的权限:
三个rwx,root权限也没有毛病,那么问题出在哪里呢?
其实是虚拟机和windows共享文件夹这里,
共享文件夹没有写权限,所以导致无法创建fifo文件。我在mnt/下面mkdir了一个新文件夹
这样再回去执行测试代码就可以正常通过了
这张图可以看出来右边终端,当我执行 ./fifo 之后因为open打开方式是只写,所以进入阻塞状态,等待另一个只读方式的open
此时我在左边终端,去刚才新建的test文件查看,就找到了刚才创建的testfifo 管道文件。
接下来再执行 ./fifob,只读方式打开了管道,所以管道接通,hello 字符串发送过来成功打印。
信号
老张的水壶在水开的时候会响,就是发送了一个信号给老张,老张接收了这个信号然后去执行下一步操作。
信号就类似于单片机中的中断,更类似于我们前面学习QT中的信号。
Linux内核中提供了一些可供我们使用的信号 kill -l 查看,这些定义好的信号都是>0正整数
kill -l
1) SIGHUP 2) SIGINT 3) SIGQUIT 4) SIGILL 5) SIGTRAP 6) SIGABRT 7) SIGBUS 8) SIGFPE 9) SIGKILL 10) SIGUSR1 11) SIGSEGV 12) SIGUSR2 13) SIGPIPE 14) SIGALRM 15) SIGTERM 16) SIGSTKFLT 17) SIGCHLD 18) SIGCONT 19) SIGSTOP 20) SIGTSTP 21) SIGTTIN 22) SIGTTOU 23) SIGURG 24) SIGXCPU 25) SIGXFSZ 26) SIGVTALRM 27) SIGPROF 28) SIGWINCH 29) SIGIO 30) SIGPWR 31) SIGSYS 34) SIGRTMIN 35) SIGRTMIN+1 36) SIGRTMIN+2 37) SIGRTMIN+3 38) SIGRTMIN+4 39) SIGRTMIN+5 40) SIGRTMIN+6 41) SIGRTMIN+7 42) SIGRTMIN+8 43) SIGRTMIN+9 44) SIGRTMIN+10 45) SIGRTMIN+11 46) SIGRTMIN+12 47) SIGRTMIN+13 48) SIGRTMIN+14 49) SIGRTMIN+15 50) SIGRTMAX-14 51) SIGRTMAX-13 52) SIGRTMAX-12 53) SIGRTMAX-11 54) SIGRTMAX-10 55) SIGRTMAX-9 56) SIGRTMAX-8 57) SIGRTMAX-7 58) SIGRTMAX-6 59) SIGRTMAX-5 60) SIGRTMAX-4 61) SIGRTMAX-3 62) SIGRTMAX-2 63) SIGRTMAX-1 64) SIGRTMAX
就像水开了的警报一样,老张可以接收这个信号,也可以忽略这个信号,上表中的信号也是同理
但是有2个信号除外,就是9号和19号信号,SIGSTOP SIGKILL,这两个信号不可被忽略。
发送信号
发送信号函数kill,和刚才的那个shell命令相同,发送的就是上表中的信号之一
#include <sys/types.h> #include <signal.h> int kill(pid_t pid, int sig);
既然是发送信号,那么我们肯定要想好 发给谁? 发什么?
第一个参数 pid,就是接收信号的进程
第二个参数 sig,就是要发送的信号
成功返回0,失败返回-1
看下面的例子,当迭代的值为20时 自杀。
#include <sys/types.h> #include <signal.h> #include <unistd.h> #include <stdio.h> int main(int argc, char const *argv[]) { int i = 0; while(1) { i++; sleep(1); if (i == 20) { kill(getpid(),SIGKILL); } printf("propess num is : %d\n",i); } return 0; }
运行结果如下:
系统调用的定时函数,闹钟函数
#include <unistd.h> unsigned int alarm(unsigned int seconds);
alarm只有一个参数,秒。
当到达这个时间时,就会向系统发送一个SIGALRM信号。
alarm和sleep不同,alarm会立即返回一个值。
要注意:一个进程只能使用一次alarm,如果多次使用后面的会覆盖前面的alarm
pause()将进程挂起直到捕捉到信号为止
#include <unistd.h> int pause(void);
例子:(在第五秒的时候发送时钟信号并打印hello)
#include <unistd.h> #include <signal.h> #include <stdio.h> void handler() { printf("hello\n"); } int main() { int i; signal(SIGALRM, handler); alarm(5); for(i = 1; i < 7; i++) { printf("sleep %d ...\n", i); sleep(1); } }
运行结果如下:
信号的处理
#include <signal.h> void (*signal(int signum, void (*handler)(int)))(int)
该函数原型比较难懂,但是参数很好理解,既然是处理信号,那么参数无非就是处理什么信号,怎么处理信号
signum 就是要处理的信号
handler 处理方式 可以自己定义函数,也可以使用默认的定义
SIG_IGN 忽略该信号
SIG_DFL 系统默认方式处理
直接看一个例子
#include <signal.h> #include <stdio.h> #include <stdlib.h> #include <unistd.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() { printf("my pid :%d\n", getpid()); printf("Waiting for signal SIGINT or SIGQUIT \n "); /*发出相应的信号,并跳转到信号处理函数处*/ signal(SIGINT, my_func); signal(SIGQUIT, my_func); pause(); exit(0); }
当我在终端输入ctrl+c时,触发信号打印句子。
再看一个示例:
#include <signal.h> #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <string.h> #include <stdlib.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> /*自定义信号处理函数*/ void my_func(int sign_no) { if(sign_no==SIGUSR1) { printf("I have get SIGUSR1\n"); kill(getpid(),9); } } int main() { int fd; int i; fd = fork(); if(fd == -1) { perror("creat fork error"); exit(-1); } else if(fd == 0) { //child while(1) { i +=2; printf("child : %d\n", i); signal(SIGUSR1,my_func); sleep(1); } } else { //father while(1) { i++; printf("father : %d\n", i); if(i == 4) { kill(fd,SIGUSR1); } sleep(1); } } exit(0); }
父子进程同时开始计数,在父进程计第四个数得时候发给子进程一个信号,子进程接到信号后自杀
观测结果如下:
还有一个更高级的信号处理函数,用来替代signal
#include <signal.h> int sigaction(int signum, const struct sigaction *act,struct sigaction *oldact);
参数:
signum 信号,除了SIGKIILL SIGSTOP
后两个参数都是结构体sigaction:
struct sigaction { void (*sa_handler)(int); void (*sa_sigaction)(int, siginfo_t *, void *); sigset_t sa_mask; int sa_flags; void (*sa_restorer)(void); };
画一个图解释一下这些成员
那么我们使用一下
用这个函数完成刚才的效果
#include <signal.h> #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <string.h> #include <stdlib.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> /*自定义信号处理函数*/ void my_func(int sign_no) { if(sign_no==SIGUSR1) { printf("I have get SIGUSR1\n"); kill(getpid(),9); } } int main() { int fd; int i = 0; fd = fork(); if(fd == -1) { perror("creat fork error"); exit(-1); } else if(fd == 0) { //child struct sigaction act; //结构体sigaction act.sa_handler = my_func; //信号处理函数 act.sa_flags = SA_RESETHAND; //信号处理之后恢复默认的处理方式 sigemptyset(&act.sa_mask); //清空屏蔽信号集 sigaction(SIGUSR1,&act,NULL); //信号接收 while(1) { i +=2; printf("child : %d\n", i); sleep(1); } } else { //father while(1) { i++; printf("father : %d\n", i); if(i == 4) { kill(fd,SIGUSR1); } sleep(1); } } exit(0); }
在 Linux 中,当一个信号的信号处理函数执行时,如果进程又接收到了该信号,该信号会自动被储存而不会中断信号处理函数的执行,直到信号处理函数执行完毕再重新调用相应的处理函数。
如果两个信号同时产生,系统并不能保证他们的调用次序。