进程间通信

tech2025-09-12  24

管道:

管道的概念: 管道是一种最基本的IPC机制,作用于有血缘关系的进程之间,完成数据传递。调用pipe系统函数即可创建一个管道。有如下特质:

其本质是一个伪文件(实为内核缓冲区)由两个文件描述符引用,一个表示读端,一个表示写端。规定数据从管道的写端流入管道,从读端流出。 管道的原理: 管道实为内核使用环形队列机制,借助内核缓冲区(4k)实现。 管道的局限性: ① 数据自己读不能自己写。 ② 数据一旦被读走,便不在管道中存在,不可反复读取。 ③ 由于管道采用半双工通信方式。因此,数据只能在一个方向上流动。 ④ 只能在有公共祖先的进程间使用管道。

常见的通信方式有,单工通信、半双工通信、全双工通信。

#include <stdio.h> #include <iostream> #include <unistd.h> #include <string> #include <cstring> #include <sys/wait.h> using namespace std; int main() { int fd[2]; if(pipe(fd) == -1) { cout << "error" << endl; return 0; } pid_t pid; char buf[1024]; const char *p = "hello for pipe"; pid = fork(); // 调用一次可以返回两次 if (pid < 0) { cout << "fork error" << endl; return 0; } else if (pid == 0) { close(fd[1]); // 关闭写端 int len = read(fd[0], buf, sizeof(buf)); cout << "Child: " << buf << endl; } else { close(fd[0]); cout << "Iam parent" << endl; write(fd[1], p, strlen(p)); wait(NULL); close(fd[1]); } return 0; } #include <stdio.h> #include <iostream> #include <unistd.h> #include <string> #include <cstring> #include <sys/wait.h> using namespace std; int main() { int fd[2]; if(pipe(fd) == -1) { cout << "error" << endl; return 0; } pid_t pid; char buf[1024]; const char *p = "hello for pipe"; pid = fork(); // 调用一次可以返回两次 if (pid < 0) { cout << "fork error" << endl; return 0; } else if (pid == 0) { close(fd[1]); // 关闭写端 dup2(fd[0], STDIN_FILENO); // dup2(int oldfd, int newfd) 让wc从管道读取数据 cout << "Child: " << buf << endl; execlp("wc", "-l", NULL); } else { close(fd[0]); cout << "Iam parent" << endl; dup2(fd[1], STDOUT_FILENO); execlp("ls", "ls", NULL); } return 0; }

有名管道FIFO

无名管道因为没有实体文件与之关联,只能应用在有共同祖先的各个进程之间. FIFO 有实体文件关联,所以可以用在没有亲缘关系之间的进程。文件系统中的路径名是全局的,各进程都可以访问,因此可以用文件系统中的路径名来标识一个IPC通道。

FIFO文件在磁盘上没有数据块,仅用来标识内核中的一条通道,如prw------- 1 root root 0 Sep 4 11:31 p1 ,文件类型标识为p表示FIFO,文件大小为0。各进程可以打开这个文件进行read/write,实际上是在读写内核通道(根本原因在于这个file结构体所指向的read、write函数和常规文件不一样),这样就实现了进程间通信。

创建

命名管道可以从命令行上创建,命令行方法是使用下面这个命令: $ mkfifo filename 命名管道也可以从程序里创建,相关函数有: int mkfifo(const char *filename,mode_t mode);

练习:进程1从一个文件读取内容到FIFO,进程2从FIFO读取,然后输出到另一个文件 进程1:

#include<sys/types.h> #include<sys/stat.h> #include<unistd.h> #include<fcntl.h> #include<stdio.h> #include<stdlib.h> #include<errno.h> #include<string.h> #include<signal.h> #define ERR_EXIT(m) \ do { \ perror(m); \ exit(EXIT_FAILURE); \ } while(0) int main(int argc, char *argv[]) { mkfifo("tp", 0644); int infd = open("hello.txt", O_RDONLY); if (infd == -1) ERR_EXIT("open error"); int outfd; outfd = open("tp", O_WRONLY); if (outfd == -1) ERR_EXIT("open error"); char buf[1024]; int n; while ((n = read(infd, buf, 1024)) > 0) write(outfd, buf, n); close(infd); close(outfd); return 0; }

进程2:

#include<sys/types.h> #include<sys/stat.h> #include<unistd.h> #include<fcntl.h> #include<stdio.h> #include<stdlib.h> #include<errno.h> #include<string.h> #include<signal.h> #define ERR_EXIT(m) \ do { \ perror(m); \ exit(EXIT_FAILURE); \ } while(0) int main(int argc, char *argv[]) { int outfd = open("./hello_out.txt", O_WRONLY | O_CREAT | O_TRUNC, 0644); if (outfd == -1) ERR_EXIT("open error"); int infd; infd = open("tp", O_RDONLY); if (infd == -1) ERR_EXIT("open error"); char buf[1024]; int n; while ((n = read(infd, buf, 1024)) > 0) write(outfd, buf, n); close(infd); close(outfd); unlink("tp"); // delete a name and possibly the file it refers to return 0; }

消息队列

相比于管道来讲,消息队列机制中,双方是通过消息来通信的,无需花费精力从字节流中解析出完整的消息。

System V消息队列 消息队列中每条消息都有type字段,消息的读取进程可以通过type字段来选择自己感兴趣的消息,也可以根据type字段来实现按消息的优先级进行读取,而不一定要按照消息生成的顺序来依次读取。 内核为每一个System V消息队列分配了一个msg_queue类型的结构体,它内部是维护一个优先级消息链表。 POSIX消息队列 POSIX消息队列与System V消息队列有一定的相似之处,信息交换的基本单位是消息,但也有显著的区别。 最大的区别当属在Linux实现里POSIX消息队列的句柄本质是文件描述符。这个性质给POSIX消息队列带来了巨大的优势。因为是文件描述符,所以可以使用I/O多路复用系统调用(select、poll或epoll等)来监控这个文件描述符。 其次,POSIX消息队列提供了通知功能,当消息队列中有消息可用时,就会通知到进程。而System V消息队列没有通知功能,所以消息队列上何时有消息进程无从得知,只能阻塞(msgrcv)或轮询(带IPC_NOWAIT标志位的msgrcv)。 最后,System V消息队列的消息提取要比POSIX消息队列灵活。POSIX消息队列本质是个优先级队列。而System V消息中存在类型字段,可以提取类型等于某值的消息,这点POSIX消息队列是做不到的。

信号量

消息队列的作用是进程之间传递消息。而信号量的作用是为了同步多个进程的操作。

信号量是和某种预先定义的资源相关联的。信号量元素的值,表示与之关联的资源的个数。内核会负责维护信号量的值,并确保其值不小于0。

创建或打开信号量的函数为semget,其接口如下:

#include <sys/types.h> #include <sys/ipc.h> #include <sys/sem.h> int semget(key_t key, int nsems, int semflg);//nsems表示信号量的个数

共享内存

共享内存是所有IPC手段中最快的一种。它之所以快是因为共享内存一旦映射到进程的地址空间,进程之间数据的传递就不须要涉及内核了, 数据直接从内存里存取、放入,数据不需要在两进程间复制。

前面已经讨论过的管道、FIFO和消息队列,任意两个进程之间想要交换信息,都必须通过内核,内核在其中发挥了中转站的作用: 发送信息的一方,通过系统调用(write或msgsnd)将信息从用户层拷贝到内核层,由内核暂存这部分信息。 提取信息的一方,通过系统调用(read或msgrcv)将信息从内核层提取到应用层。 一个通信周期内,上述过程至少牵扯到两次内存拷贝(从用户拷贝到内核空间和从内核空间拷贝到用户空间)和两次系统调用,这其中的开销不容小觑。用户层的体验固然不佳,所以产生了共享内存的通信方式,共享内存是在用户空间不经过内核: 内核负责构建出一片内存区域,两个或多个进程可以将这块内存区域映射到自己的虚拟地址空间。

注意 建立共享内存之后,内核完全不参与进程间的通信,这种说法严格来讲并不是正确的。因为当进程使用共享内存时,可能会发生缺页,引发缺页中断,这种情况下,内核还是会参与进来的。

缺陷 共享内存并未提供同步机制,需要使用信号量来实现对共享内存同步访问控制。

相关函数:

1.创建共享内存shmget

原型:int shmget(key_t key, size_t size, int shmflg) 返回值: 创建成功,则返回一个非负整数,即共享内存标识;       如果失败,则返回-1. 参数:   key: //程序需要提供一个参数key,它为共享内存段提供一个外部名。(每个IPC对象都与一个键   即key相关联,然后此键再由内核变换为标识符)。还有一个特殊的键值IPC_PRIVATE, 它用于   创建一个只属于该创建进程的新共享内存,通常不会用到;   size:   //以字节为单位指定需要共享的内存容量。   shmflag: //包含9个比特的权限标志,它们的作用与创建文件时使用的mode标志是一样。   由IPC_CREAT定义的一个特殊比特位,同时必须和权限标志按位或才能创建一个新的共享内存段。 (注意:若想创建的新IPC结构没有引用具有同一标识符的现有的IPC结构,就要同时指定IPC_CREAT 和 IPC_EXCL;共享内存属IPC中一种,它同样如此)

注:   权限标志对共享内存非常有用,因为它允许一个进程创建的共享内存可以被共享内存的创建者所拥有的进程写入,同时其它用户创建的进程只能读取共享内存。我们可以利用这个功能来提供一种有效的对数据进行只读访问的方法,通过将数据放共享内存并设置它的权限,就可以避免数据被其他用户修改。

2.将共享内存端挂载到自己地址空间shmat    第一次创建共享内存段时,它不能被任何进程访问。要想启动对该内存的访问,必须将其连接到一个进程的地址空间

该函数原型:void *shmat(int shmid, const void *shmaddr, int shmflg)    返回值:调用成功返回挂载的虚拟地址空间起始地址,失败返回NULL 参数:   int shmid //是由shmget函数返回的共享内存标识。   const void *shmaddr //指定共享内存连接到当前进程中的地址位置,通常为0,表示让系统来选择          共享内存的地址。   int shmflg     //是一组标志位,通常为0。它还可取:SHM_RND,用以决定是否将当前共享内存段   连接到指定的shmaddr上。该参数和shm_addr联合使用,用来控制共享内存连接的地址,除非只计划   在一种硬件上运行应用程序,否则不要这样指定。填0让操作系统自己选择是更好的方式。   SHM_RDONLY单独使用则是指让它使连接的内存段只读,否则以读写方式连接此内存段

3.与共享内存段分离 shmdt

原型:int shmdt(const void *shmaddr) 参数:   shm_addr: shmat返回的地址指针。   成功时,返回0,   失败时,返回-1.

NOTE:   仅仅是共享内存分离但并未删除它,其标识符及其相关数据结构都在;直到某个进程的IPC_RMID命令的调用shmctl特地删除它为止     只是使得该共享内存对当前进程不再可用。

shmctl 共享内存控制函数 #include <sys/ipc.h> #include <sys/shm.h> 原型: int shmctl(int shmid, int cmd, struct shmid_ds *buf) 参数:    shm_id : 是shmget返回的共享内存标识符。   cmd: 它可以取3个值:     IPC_STAT 把shmid_ds结构中的数据设置为共享内存的当前关联值     IPC_SET 如果进程有足够的权限就把共享内存的当前关联值设置为shmid_ds结构中给出的值     IPC_RMID 删除共享内存段   buf:是一个指针,包含共享内存模式和访问权限的结构。   buf指向的shmid_ds结构体 一定要包含下列一些参数: struct shmid_ds { uid_t shm_perm.uid; uid_t shm_perm.gid; mode_t shm_perm.mode; }

Sockets

进程间使用sockets最大的好处就是:可以跨机器通信,目前分布式系统最主要的通信方式(RPC),具有伸缩性。

在编程上,TCP sockets 和pipe 都是一个文件描述符,用来收发字节流,都可以 read/write/fcntl/select/poll 等。不同的是,TCP 是双向的,pipe 是单向的(Linux),进程 间双向通讯还得开两个文件描述符,不方便;而且进程要有父子关系才能用pipe,这些都 限制了pipe 的使用。在收发字节流这一通讯模型下,没有比sockets/TCP 更自然的IPC 了。当然,pipe 也有一个经典应用场景,那就是写Reactor/Selector 时用来异步唤醒select (或等价的poll/epoll) 调用(Sun JVM 在Linux 就是这么做的)。 ----引用自:《多线程服务器的常用编程模型》陈硕

最新回复(0)