什么是进程间的通信
IPC的全称是是Inter-Process Communication,代表是信息在进程之间流通的方式。
为什么需要这个机制?
在进程的运行中,获取数据是利用虚拟地址(Virtual Address)来获取的,虚拟地址映射到了内存上的物理地址。正常情况下系统不会分配两个不同进程的虚拟地址映射给同一个物理地址,所以进程没有办法访问其他进程使用的物理空间所以完全没有办法知道其他进程在干什么。有时候我们是需要共享一些数据或者告诉其他进程一些信息,这个机制就是为了满足这些要求。
全部的进程通信机制
管道(PIPE)
管道是由系统分配到内核空间的一块缓冲区,由于位置是在内核空间,所有进程都可以访问到但是必须通过系统调用(System Call)转化成内核模式(Kernel Mode)才可以执行操作。
可以把管道当作一个特殊的文件,因为我们可以使用文件描述符(File Descriptor, FD)去操作它。创建一个管道会返回两个FD(读和写)。通常我们利用fork去创建子进程然后实现父子进程之间的数据单向流通(一个进程读一个进程写,多余的FD需要关闭)。当全部的进程关闭了FD,这个管道会自动关闭,缓冲区的空间也会被释放。
这个缓冲区的工作原理
上面提到的缓冲区是扇形缓冲区(Circular Buffer)是一种数据结构,也称为环形队列(Ring Buffer)。非常适合流式的处理数据,一个写指针在前面写数据,一个读指针在后面读刚刚写下来的数据。如果读指针追上了写指针,说明现在没有多余的数据。如果写指针追上了读指针,说明现在这个Buffer满了。
代码
创建PIPE
int pipe(int pipefd[2]);
pipefd:传入的数组,用于存储两个文件描述符,
pipefd[0] 为读端,pipefd[1] 为写端。
为什么不在在非亲缘关系的进程之间使用呢?
管道是一块临时的缓冲区而且是运行的时候才由系统分配到内存空间的,所有就算它可以被当作一个特殊的文件但是我们无法通过路径去访问这个特殊文件。当我在一个进程里面创建一个管道的时候,唯一可以帮助我们使用这个缓冲区是FD,通过fork出来的子进程可以继承父进程的文件表(File Descriptor Table)这样可以指向全局文件表(Open File Table)相同的一个项。
或许我们可以通过文件描述符传递(File Descriptor Passing)机制给FD去一个没有血缘关系的进程然后实现PIPE在没有血缘关系的进程里面通信但是我感觉没有意义去这样做哈哈。
命名管道(FIFO)
FIFO是也是一个特殊的文件,但是它克服了PIPE不方便在非血缘关系的进程里面通信的缺点。它是一个存在在文件系统的特殊文件不止是只能出现在内存所以它可以在文件系统里面通过路径访问。就算利用这个管道的进程都结束了,这个FIFO仍然以文件的形式存在在这个文件系统。FIFO的整体实现逻辑也是利用类似PIPE的缓冲区。
代码
创建一个FIFO
int mkfifo(const char *pathname, mode_t mode);
打开FIFO
int open(const char *pathname, int flags);
读写
ssize_t read(int fd, void *buf, size_t count);
ssize_t write(int fd, const void *buf, size_t count);
消息队列(Message Queues)
通常是单向的通信方式,一个进程当作生产者方式放送消息给队列,另外一个进程当作消费者获取信息从队列,这个队列是先进先出(FIFO)的。
特性
- 异步通信: 可以放送完消息之后做其他事情
- 有序性: 可以按照一定的顺序比如先进先出或者是优先度
- 持久性:消息队列是存在在内核空间的,进程的结束也不会影响消息队列,所以消息队列在系统重启或删除之前会一直存在。
代码
创建或打开消息队列
mqd_t mq = mq_open("/my_queue", O_CREAT | O_RDWR, 0644, NULL);
发送消息
char message[] = "Hello, POSIX Message Queue!";
mq_send(mq, message, sizeof(message), 0); // 优先级为 0
接受消息
char buffer[128];
mq_receive(mq, buffer, sizeof(buffer), NULL);
删除消息队列
mq_close(mq);
mq_unlink("/my_queue");
共享内存(Share Memory)
修改多个进程的映射关系,绑定每一个他们中的一个虚拟地址到一个相同的物理地址让每一个进程都可以通过虚拟地址映射到一个共享物理地址。共享内存的特性是不需要系统调用来操作这个共享的内存空间因为这个虚拟地址不在内核空间里面。
代码
创建或者打开一个共享内存对象
int shm_open(const char *name, int oflag, mode_t mode);
设置共享内存的大小
int ftruncate(int fd, off_t length);
创建映射关系
void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);
取消映射关系
int munmap(void *addr, size_t length);
删除共享内存对象
int shm_unlink(const char *name);
信号量(Semaphore)
信息量的通常是控制对一个资源的访问数量做了一个限制,它的count和资源都是在内核空间。需要使用系统调用来获取资源。
操作只有两种,获取资源的操作(P)和释放资源的操作(V)。
POSIX信号量
是一个利用信号量这个机制操作的已经提供好的一套接口。
代码
创建一个信息量
sem_t *sem = sem_open("/mysem", O_CREAT, 0644, 1); // 创建或打开信号量,初始值为 1
P操作
sem_wait(sem); // 等待信号量,进入临界区
V操作
sem_post(sem); // 释放信号量,离开临界区
关闭和删除信号量
sem_close(sem); // 仅关闭当前进程对信号量的引用
sem_unlink("/mysem"); // 删除命名信号量,其他进程不能再通过 "/mysem" 打开信号量
为什么算一种通信方法?
它也是一种同步机制,为什么也是一种通信方法呢。因为信息量可以潜在的告诉我们是否其他进程也在访问这个资源,这个也是消息。
信号(Signal)
信号是一种类似消息队列的异步通信方法。由其他进程,操作系统,或者是自己放送信号到目标进程的进程控制块(PCB)中的信号队列。接收信号的进程会在合适的时机查看信号队列比如上下文切换,系统调用和被调度运行等。
操作系统有一组信号,每一个有默认的处理行为。我们还可以选择忽略一些信号或者修改一些信号的默认行为但是不可能创建新的信号了。
代码
发送信号
int kill(pid_t pid, int sig);
自定义信号的行为
void (*signal(int sig, void (*handler)(int)))(int);
sig是信号, 后面是一个函数指针代表信号处理程序。
更多自定义行为(例如阻塞其他信号或恢复默认信号处理)
int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
给自己发送信号
int raise(int sig);
套接字 (Socket)
套件字是一个非常强大的通信方法,因为他不仅仅局限一个机器中的进程,而是不同机器中的进程通信。
套接字是什么?
我们一般认为套接字是应用层(Application Layer)和传输层(Transportation Layer)中的桥梁因为套接字就需要跟TCP和IP协议打交道。 每次创建一个socket的时候,实际上创建了一个特殊文件里面包含了需要的全部的元数据,socket文件有很多不同的类型所以里面的内容也不是一样的。TCP socket会包含TCP连接的TCB但是UDP没有连接所以没有。大致有这些元数据
- 套接字的类型
- 套接字使用的协议
- IP地址、源端口号、目的IP地址、目的端口号
- 指向套接字缓冲区的文件描述符(FD)(可能)
- 可能有TCB (可能)
网络套接字(TCP/UDP 套接字)
我们一开始需要创建一个套接字,如果是TCP,第一个套接字是监听(Listen)套接字目的是建立TCP连接,但是如果是UDP的话,直接创建UDP套接字因为UDP不需要连接。对于两种类型的套接字,第一个套接字都需要绑定(Bind)需要监听的IP和端口。然后,TCP需要监听(Listen)然后在接受(Accept)一个TCP连接之后把连接分配到一个TCP套接字然后再给我们,监听套接字继续监听,这个连接由TCP套接字独自处理。利用这个TCP套接字我们已经使用来发送和接收消息了。对于UDP套件字,我们也是已经可以直接使用它发送和接收消息了。
TCP套接字如何访问TCP连接然后获取数据?
对于TCP套接字,我们需要四元组(IP地址、源端口号、目的IP地址、目的端口号)定位TCP连接的控制块(TCB)。TCB 是内核维护的一个数据结构,存储着与该 TCP 连接相关的信息,如状态、窗口大小、发送和接收缓冲区等。通过这个TCB,可以获取tcp的发送缓冲区(Send Buffer)和接收缓冲区(Receive Buffer)。socket从TCB找到他们然后会把他的发送和接收缓冲区的内容放到tcp的缓冲区。
UDP套接字怎么获取数据的?
UDP套接字不需要寻找连接,所以数据直接发送到UDP的接收缓冲区,应用层可以直接从这个缓冲区抓取数据。
Unix 套接字(Unix Domain Socket)
也是有SOCK_STREAM和SOCK_DGRAM两种type但是domain是AF_UNIX了。所以逻辑跟上面也是差不多了。
通常用在同一个主机的不同进程通信。不需要处理和tcp的连接。这样其实发送过程就是系统把数据从发送缓冲区放到接收缓冲区。
两种套接字的区别?
网络套接字没有路径,没有真的存在在文件系统里面。他是类似PIPE只是存在在内存中,进程结束了就被释放了。
Unix套接字就是文件系统里面的特殊文件,可以通过路径访问到。进程终止后,套接字的路径文件仍然存在。
代码
创建套接字
int socket(int domain, int type, int protocol);
服务端
绑定当前主机的 IP 地址(可能有多个)和端口。
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
等待客户端连接
int listen(int sockfd, int backlog);
从等待的连接中接受一个连接,并返回新的套接字文件描述符。
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
客户端用于连接服务端
客户端
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
接送和发送
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
关闭套接字
int close(int sockfd);