企业🤖AI智能体构建引擎,智能编排和调试,一键部署,支持私有化部署方案 广告
> 包括进程通信中信号的概念及信号处理、管道通信编程、内存共享编程、队列通信编程。 [TOC] ## 1 进程间通信 能够实现进程间通信的方法有: - 信号(signal):进程之间相互通信或操作的一种机制 - 管道(pipe):同一台机器的两个进程间双向通信 - 套接字(socket):允许在不同机器上的两个进程间进行通信 - System V IPC机制: - 消息队列(message queue):适用于信息传递频繁而内容较少 - 信号量(semaphore):用于实现进程之间通信的同步问题 - 共享内存(shared memory):信息内容较多 ## 2 信号 信号是在软件层次上对中断机制的一种模拟,是一种异步通信方式,可以在用户空间和内核之间直接交互。信号事件的发生有两个来源包括硬件来源(例如按下`Ctrl+C`)和软件来源(例如`kill` `raise` `alarm` `setitimer` `sigation` `sigqueue`,以及一些非法运算) 通过 `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 ``` 以上信号可分为三类: - [1,31] 都以"SIG"开头,属于非实时信号 - [34, 49] 都以"SIGREMIN" 开头,从Unix系统中继承下来的信号,称为不可靠信号(非实时信号) - [50, 64]的都以"SIGRTMAX" 开头,为了解决不可靠信号问题而进行更改和扩充的信号,称为可靠信号(实时信号) 一个完整的信号声明周期为: 1. 在内核进程产生信号 2. 在用户进程进行信号注册和信号注销 3. 信号处理 一旦有信号产生,用户进程对信号的响应有三种方式: - 执行默认操作(Linux对每种信号都规定了默认操作) - 捕捉信号(定义信号处理函数,当信号发生时,执行相应的处理函数) - 忽略信号(不作处理,但是SIGKILL和SEGSTOP无法忽略) > Linux 常见信号的含义及其默认操作 信号名 | 含义 | 默认操作 ---- | ---- | ---- SIGHUP | 在用户终端连接结束时发出 | 终止 SIGINT | 中断键 `Ctrl + C` | 终止 SIGQUIT | 退出键 `Ctrl + \` | 终止 SIGILL | 另一个进程企图执行一条非法指令时发出 | 终止 SIGFPE | 发生致命的算术运算错误时发出 | 终止 SIGKILL | 立即结束程序的运行,不能被阻塞、处理或忽略 | 终止 SIGALRM | 定时器完成时发出,可用alarm函数来设置 | 终止 SIGSTOP | 挂起键 `Ctrl + Z`,用于暂停一个进程,不能被阻塞、处理或忽略 | 暂停进程 SIGCHLD | 子进程结束时向父进程发出 | 忽略 > 以下程序演示了父进程向子进程发送信号,结束子进程 ```c int main() { pid_t pid_res; int kill_res; pid_res = fork(); if (pid_res < 0) { // 创建子进程失败 perror("创建子进程失败"); exit(1); } else if (pid_res == 0) { // 子进程 raise(SIGSTOP); // 调用rasise 函数,发送SIGSTOP使子进程暂停 exit(0); } else { printf("子进程号为:%d\n", pid_res); if ((waitpid(pid_res, NULL, WNOHANG)) == 0) { if ((kill_res = kill(pid_res, SIGKILL)) == 0) { printf("kill %d返回:%d\n", pid_res, kill_res); } else { perror("kill 结束子进程失败") } } } } /* int kill(pid_t, pid, int sig); - pid > 0 表示将信号传给识别码为 pid 的进程 - pid = 0 表示将信号传给和当前进程在相同进程组的所有进程 - pid = -1 表示将信号广播传送给系统内所有的进程 - pid < 0 表示将信号传给进程组识别码为 pid 绝对值的所有进程 */ ``` 以下是signal函数的示例,展示了按下两次`Ctrl + C` 后,终止进程的过程 ```c // Ctrl + C void fun_ctrl_c(); int main() { signal(SIGINT, fun_ctrl_c); // 令 SIGINT 信号调用函数 fun_ctrl_c while(1) { printf("无限循环,按<Ctrl + C>\n") sleep(3); } exit(0); } void fun_ctrl_c() { printf("按下Ctrl + C"); (void) signal(SIGINT, SIG_DFL); // 恢复 SIGINT 信号的默认操作 } /* #include <signal.h> void (*signal(int signum, void(* handler)(int))) (int); - SIG_IGN:忽略参数 signum 指定的信号 - SIG_DFL:重设参数 signum 指定的信号的默认操作 返回先前的处理函数指针,如有错误返回SIG_ERR,即-1 */ ``` ## 3 管道 管道的实质是一个内核缓冲区,进程以先进先出的方式从缓冲区中存取数据。 - pipe 为无名管道,用于相关进程之间的通信,如父进程和子进程,通过 `pipe()` 系统调用来创建,当最后一个使用它的进程关闭对它的引用时,pipe将自动撤销 - FIFO 为命名管道,在磁盘上有对应的节点,但没有数据块(即只拥有名字和相应的访问权限),通过mknod()系统调用和mkfifo()函数来建立,当不再被进程使用时,FIFO在内存中释放,但磁盘节点仍然存在。 > 以下例子展示了无名管道(pipe)的创建和读写 ```c #include <stdio.h> #include <stdlib.h> #include <sys/types.h> #include <sys/wait.h> #include <unistd.h> #include <string.h> // 管道的创建和读写 int main() { pid_t pid_res; int r_num; int pipe_fd[2]; // pipe_fd[0]为管道读取端,pipe_fd[1]为管道写入端 char buf_r[100], buf_w[100]; memset(buf_r, 0, sizeof(buf_r)); // 将buf_r的前sizeof(buf_r)元素用0填充 if(pipe(pipe_fd) < 0) { printf("创建管道失败"); return -1; } pid_res = fork(); if(pid_res < 0) { perror("创建子进程失败"); exit(0); } else if(pid_res == 0) { // 子进程代码块 close(pipe_fd[1]); if((r_num = read(pipe_fd[0], buf_r, 100)) > 0) { printf("读取到:%s\n", buf_r); close(pipe_fd[0]); exit(0); } } else { // 父进程代码块 close(pipe_fd[0]); printf("请从键盘输入写入管道的字符串:"); scanf("%s", buf_w); if((write(pipe_fd[1], buf_w, strlen(buf_w)))!= -1) { close(pipe_fd[1]); waitpid(pid_res, NULL, 0); // 阻塞父进程,等待子进程退出 exit(0); } } } ``` > 以下例子通过命名管道模拟了一个聊天功能,包括 `a.c` 与 b.c` 两个文件,其中 `b.c` 与 `a.c`的区别是交换了 FILO_NAME_1 和 FILO_NAME_2 ```c // a.c #include <stdio.h> #include <unistd.h> #include <fcntl.h> #include <string.h> #include <stdlib.h> #include <sys/select.h> #include <sys/types.h> #include <sys/stat.h> #include <errno.h> #define FILO_NAME_1 ".__fifo__1" #define FILO_NAME_2 ".__fifo__2" int main() { mkfifo(FILO_NAME_1, S_IWUSR|S_IRUSR|S_IRGRP|S_IROTH); mkfifo(FILO_NAME_2, S_IWUSR|S_IRUSR|S_IRGRP|S_IROTH); printf("创建管道成功\n"); int wfd = open(FILO_NAME_1, O_RDWR); int rfd = open(FILO_NAME_2, O_RDWR); if (wfd <=0 || rfd <= 0) { return 0; } printf("a的终端\n"); fd_set read_fd; struct timeval net_timer; char str[32]; int i; int len; while (1) { FD_ZERO(&read_fd); FD_SET(rfd, &read_fd); FD_SET(fileno(stdin), &read_fd); net_timer.tv_sec = 5; net_timer.tv_usec = 0; memset(str, 0, sizeof(str)); if (i = select(rfd+1, &read_fd, NULL, NULL, &net_timer) <= 0) { continue; } if (FD_ISSET(rfd, &read_fd)) { read(rfd, str, sizeof(str)); // 读取管道,将管道内容存入str printf("a读取到:%s\n", str); } fgets(str, sizeof(str), stdin); len = write(wfd, str, strlen(str)); // 写入管道 } close(rfd); close(wfd); } ``` > 高级管道可以参见 `popen` 函数 ## 4 消息队列 消息队列是一系列保存在内核中的消息的列表,用户进程可以向消息队列存取消息。 消息队列产生后,除非明确的删除,产生的队列会一致保留在系统中,队列的个数是有限的(注意不要泄露),使用已达到上限,msgget调动会失败,提示"no space left on device" 其常用函数为: 函数名称 | 函数功能 | 函数原型 | 函数返回值 ---- | ---- | ---- | ---- fotk | 由文件路径和工程ID生成标准key(通过 ftok 建立一个 用于 IPC 通信的 ID 值) | `key_t ftok(char *pathName, char projectId);` | 成功返回 key_t 的值,失败返回 -1,失败原因存于 errno 中 msgget | 创建或打开消息队列 | `int msgget(key_t key, int msgFlag` | 执行成功返回消息队列识别号,失败返回-1,失败原因存于 errno 中 msgsnd | 将消息送入消息队列 | `int msgsnd(int msgId, struct msgbuf *msgp, int msgSize, int msgFlag)` | 执行成功返回0,失败返回-1,失败原因存于 errno 中 msgrcv | 从消息队列读取消息 | `int msgrcv(int msgId, struct msgbuf *msgp, int msgSize, long msgType, int msgFlag)` | 执行成功返回实际读取的消息数据长度,否则返回-1,错误原因存于 errno 中 msgctl | 控制消息队列 | `msgctl (int msqId, int cmd, struct msqid_ds *buf)` | > 利用消息队列进行通信的示例 ```c /* 利用消息队列进行通信 */ #include <stdio.h> #include <string.h> #include <stdlib.h> #include <sys/types.h> #include <sys/msg.h> #include <sys/ipc.h> #include <unistd.h> struct msgbuf { long msg_type; char msg_text[512]; }; int main() { int qid; key_t key; if (key = ftok(".", 'a') == -1) { // 产生标准的 key perror("产生标准的key出错"); exit(1); } if ((qid=msgget(key, IPC_CREAT|0666))==-1) { perror("创建消息队列出错"); exit(1); } printf("打开消息队列:%d\n", qid); struct msgbuf msg; puts("输入要加入到消息队列的消息:"); if (fgets((&msg)->msg_text, 512, stdin) == NULL) { puts("没有消息"); exit(1); } msg.msg_type = getpid(); int len = strlen(msg.msg_text); if (msgsnd(qid, &msg, len, 0) < 0) { perror("添加消息出错"); exit(1); } if (msgrcv(qid, &msg, 512, 0, 0) < 0) { perror("读取消息出错"); exit(1); } printf("读取的消息是:%s\n", (&msg)->msg_text); if (msgctl(qid, IPC_RMID, NULL) < 0) { perror("删除消息队列出错"); exit(1); } exit(0); } ``` ## 5 共享内存 共享内存允许两个或多个进程共享一个给定的存储区,这一段存储区可以被两个或两个以上的进程映射至自身的地址空间中。 - 共享内存的好处是通信效率非常高。 - 可以通过内存映射机制实现,也可以通过Unix System V共享内存机制实现。 - 常用函数如下: - mmap:建立共享内存映射 - munmap:接触共享内存映射 - shmget:获取共享内存区域的ID - shmat:建立映射共享内存 - shmdt:接触共享内存映射 ------ 内存映射(memory map)机制使进程之间通过映射同一个普通文件实现共享内存,通过 mmap() 系统调用实现,普通文件被映射到进程地址空间后,进程可以像访问普通内存一样对文件进行访问,不必再调用 read 和 write 等文件操作函数 > 以下示例中,先调用 mmap 映射内存,然后调用 fork 函数创建进程;在调用 fork 函数之后,子进程继承父进程匿名映射的地址空间,同样也继承 mmap 函数的返回地址, ```c // 匿名内存映射 #include <sys/types.h> #include <unistd.h> #include <sys/mman.h> #include <fcntl.h> #include <stdio.h> #include <string.h> #include <stdlib.h> typedef struct { char name[4]; int age; } people; int main(int argc, char** argv) { int i; people *p_map; char tmp; p_map = (people *) mmap(NULL, sizeof(people) * 10, PROT_READ|PROT_WRITE, MAP_SHARED|MAP_ANONYMOUS, -1, 0); pid_t pid_res = fork(); if (pid_res < 0) { perror("创建子进程失败"); exit(0); } else if (pid_res == 0) { // 子进程代码块 sleep(2); for (i = 0; i < 5; i++) { printf("子进程读取 - 第 %d 个人的年龄是:%d\n", i+i, (*(p_map+i)).age); } (*p_map).age = 110; munmap(p_map, sizeof(people) * 10); // 解除内存映射关系 exit(0); } else { tmp = 'a'; for (i = 0; i < 5; i++) { tmp += 1; memcpy((*(p_map + i)).name, &tmp, 2); (*(p_map+i)).age = 20 + i; } sleep(5); printf("父进程读取 - 五个人的年龄和是:%d\n", (*p_map).age); munmap(p_map, sizeof(people) * 10); } } /* > gcc test.c -o test > /data/Code/C/$ ./test 子进程读取 - 第 0 个人的年龄是:20 子进程读取 - 第 2 个人的年龄是:21 子进程读取 - 第 4 个人的年龄是:22 子进程读取 - 第 6 个人的年龄是:23 子进程读取 - 第 8 个人的年龄是:24 父进程读取 - 五个人的年龄和是:110 */ ``` > 与 mmap 系统调用通过映射一个普通文件实现共享内存不同,UNIX System V 共享内存是通过映射特殊文件系统 shm 中的文件实现进程间的共享内存通信