进程和线程之间有很多种方法进行通信,如下是需要掌握的通信方式:
- 无名管道(pipe)
- 有名管道(fifo)
- 信号(signal)
- 共享内存(mmap)
本文章代码存放在GitHub中的UNIX_Coding中,需要自行查看
https://github.com/Scholar618/UNIX_Codings/tree/main/Linux
无名管道
定义:在内核中开辟一块内存作为通道连接两个进程之间的通信,它一般是单向的。
特点 :
- 只能在具有公共祖先的两个进程之间使用
- 单工(单向)的通信模式,具有固定的读写端
- 无名管道创建时会返回两个文件描述符,分别用于读写管道
无名管道创建
int pipe(int pfd[2]);
- 成功时返回0,失败时返回EOF
- pfd包含两个元素的整形数组,用来保存文件描述符
- pfd[0]用于读管道,pfd[1]用于写管道
示例程序:
#include <stdio.h> #include <unistd.h> #include <string.h> int main() { int pfd[2]; int re; char buf[20]; pid_t pid; re = pipe(pfd); if(re < 0) { perror("pipe"); return 0; } pid = fork(); if(pid < 0) { perror("fork"); return 0; } else if(pid == 0) { // 子进程 while(1) { strcpy(buf, "I love Linux!"); write(pfd[1], buf, strlen(buf)); sleep(1); } } else { // 父进程 while(1) { re = read(pfd[0], buf, 20); if(re > 0) { printf("read pipe = %s\n", buf); } } } return 0; }
管道可以由大于等于两个进程共享
例如一个父进程进行读数据,两个子进程进行写数据
示例程序:#include <stdio.h> #include <unistd.h> #include <string.h> int main(){ int pfd[2]; int i; int re; char buf[40]={0}; pid_t pid; re = pipe(pfd); if(re<0){ perror("pipe"); return 0; } printf("%d,%d\n",pfd[0],pfd[1]); for(i=0;i<2;i++){ pid = fork(); if(pid<0){ perror("fork"); return 0; }else if(pid>0){ }else{ break; } } if(i==2){ close(pfd[1]); while(1){ memset(buf,0,40); re=read(pfd[0],buf,40); if(re>0){ printf("%s\n",buf); } } return 0; } if(i==1){ close(pfd[0]); while(1){ strcpy(buf,"this is 2 process"); write(pfd[1],buf,strlen(buf)); usleep(930000); } return 0; } if(i==0){ close(pfd[0]); while(1){ strcpy(buf,"this is 1 process"); write(pfd[1],buf,strlen(buf)); sleep(1); } return 0; } }
无名管道的读写特性
读管道:
- 管道中有数据,read返回实际读到的字节数
- 管道中无数据:
- 管道写端被全部关闭,read返回0(假设读到文件末尾)
- 写段没有被完全关闭,read阻塞等待(以后可能会有数据传递,此时让出CPU)
写管道:
- 管道读端全部被关闭,进程异常终止(也可以捕捉SIGPIPE信号,使进程不终止)
- 管道读端没有全部关闭:
- 管道已满,write阻塞(管道大小64K)
- 管道未满,write将数据写入,并返回实际写入的字节数
有名管道
特点
- 有名管道可以使非亲缘关系的两个进程相互通信
- 通过路径名来操作,在文件系统中可见,但内容存放在内存中
- 文件IO来操作有名管道
- 遵循先进先出原则
- 不支持leek操作
- 单工读写
有名管道的创建
#include <fcntl.h> #include <unistd.h> int mkfifo(const char *path, mode_t mode);
- 成功时返回0,失败时返回EOF
- path创建的管道文件路径
- mode管道文件的权限,如0666
示例代码:fifor.c(读管道)
#include <sys/types.h> #include <sys/stat.h> #include <stdio.h> #include <unistd.h> #include <stdlib.h> #include <string.h> #include <fcntl.h> int main() { int re; int fd; char buf[32]; fd = open("/myfifo", O_RDONLY); if(fd < 0) { perror("open"); return 0; } while(1) { re = read(fd, buf, 32); if(re > 0) { printf("read fifo = %s",buf); } else if(re == 0) { exit(0); } } }
fifow.c(写管道)
#include <sys/types.h> #include <sys/stat.h> #include <stdio.h> #include <unistd.h> #include <stdlib.h> #include <string.h> #include <fcntl.h> int main() { int re; int fd; char buf[32]; re = mkfifo("/myfifo", 0666); if(re < 0) { perror("mkfifo"); // return 0; } fd = open("/myfifo", O_WRONLY|O_NONBLOCK); if(fd < 0) { perror("open"); return 0; } printf("after open!\n"); while(1) { fgets(buf, 32, stdin); write(fd, buf, strlen(buf)); } }
有名管道注意事项:
- 程序不能以O_RDWR(读写)模式打开FIFO文件进行读写操作
- 第二个参数中的选项O_NONBLOCK,选项O_NONBLOCK表示非阻塞,如果没有这个选项为阻塞的
共享内存
概念:
- 共享内存也叫内存映射
- 共享内存可以使用mmap()映射普通文件
- 使一个磁盘文件与内存中的一个缓冲区相映射,进程可以像普通内存那样对文件进行访问,不必再调用write,read
函数定义:
void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);
- 功能:创建共享内存映射
- 函数返回值:成功返回创建的映射区首地址,失败返回MAP_FAILED( ((void *) -1) ),设置errno值
- 参数:
- addr:指定要映射的内存地址,一般设置为 NULL 让操作系统自动选择合适的内存地址。
length:必须>0。映射地址空间的字节数,它从被映射文件开头 offset 个字节开始算起。
prot:指定共享内存的访问权限。可取如下几个值的可选:PROT_READ(可读), PROT_WRITE(可写), PROT_EXEC(可执行), PROT_NONE(不可访问)。
flags:由以下几个常值指定:MAP_SHARED(共享的) MAP_PRIVATE(私有的), MAP_FIXED(表示必须使用 start 参数作为开始地址,如果失败不进行修正),其中,MAP_SHARED , MAP_PRIVATE必选其一,而 MAP_FIXED 则不推荐使用。MAP_ANONYMOUS(匿名映射,用于血缘关系进程间通信)
fd:表示要映射的文件句柄。如果匿名映射写-1。
offset:表示映射文件的偏移量,一般设置为 0 表示从文件头部开始映射。
注意事项:
(1) 创建映射区的过程中,隐含着一次对映射文件的读操作,将文件内容读取到映射区。
(2) 当MAP_SHARED时,要求:映射区的权限应 <=文件打开的权限(出于对映射区的保护),如果不满足报非法参数(Invalid argument)错误。
当MAP_PRIVATE时候,mmap中的权限是对内存的限制,只需要文件有读权限即可,操作只在内存有效,不会写到物理磁盘,且不能在进程间共享。
(3) 映射区的释放与文件关闭无关,只要映射建立成功,文件可以立即关闭。
(4) 用于映射的文件大小必须>0,当映射文件大小为0时,指定非0大小创建映射区,访问映射地址会报总线错误,指定0大小创建映射区,报非法参数错误(Invalid argument)
(5) 文件偏移量必须为0或者4K的整数倍(不是会报非法参数Invalid argument错误).
(6)映射大小可以大于文件大小,但只能访问文件page的内存地址,否则报总线错误 ,超出映射的内存大小报段错误
信号
信号是软件中断,是一种处理异步事件的方法,例如终端用户输入中断键,会通过信号机制停止程序。例如kill -9 程序号,等等都是信号机制。
概念:
使用kill -l命令查看所有信号编号,注意哦,不存在编号为0的信号。编号为0的信号有新的用途。
一些常用信号解释:
产生信号的方式:
- 用户按下某些终端键时,引发终端产生的信号,例如(Ctrl + C)产生中断信号(SIGNALINT)
- 硬件异常产生信号:除数为0或者无效的内存引用等。会有硬件检测出来,并通知内核,然后内核为该进程产生信号。例如:对执行一个无效内存引用的进程产生(SIGSEGV)信号
- 进程调用kill函数将任意信号发送给另一个进程或进程组,但是,接收信号进程必须和发送信号进程的所有者完全相同。或者发送信号进程是root用户
- 用户使用kill命令
- 当检测到某种软件条件已经发生,并通知有关进程时也会产生信号。
处理信号的方式:
- 忽略信号:两种信号不能被忽略,SIGKILL(9)和SIGSTOP(19)
- 捕捉信号:在用户函数中,执行用户希望对这种事件进行的处理。
- 执行系统默认动作:如上图,执行默认动作,注意,对大多数信号的系统默认动作是终止该进程。
函数定义
#include <signal.h>
// 1.
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);
// 2.
void (*signal(int signo, void (*func) (int))) (int);
signo是信号名, func值常量是SIG_IGN、常量SIG_DFL或当接到此信号后要调用的函数地址。
- SIG_IGN:向内核表示忽略此信号(SIGKILL和SIGSTOP)不能被忽略
- SIG_DFL:表示接到此信号后的动作是系统默认动作。
- 函数地址:信号发生时,调用该函数,称为捕捉信号,称函数为信号处理程序、信号捕捉函数。
#include <signal.h>头文件中,有如下定义:
#define SIG_ERR (void(*)())-1 // 指定一个无效的处理程序
#define SIG_DFL (void(*)())0 // 使用默认的处理方式
#define SIG_IGN (void(*)())1 // 忽略信号
共享内存
允许两个或多个进程共享一个给定的存储区,在多个进程之间同步访问一个给定的存储区
函数定义
#include <sys/shm.h>
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
注意:共享内存实现进程间通信是进程间通信最快的。