进程作为人类的发明,自然也免不了脱离人类的习性,也有通信的需求。如果进程之间不进行任何通信,那么进程所能完成的任务就要大打折扣。人类的通信方式无外乎对白(通过声音沟通)、打手势、写信、发电报、拥抱等方法。同理,进程也可以通过同样的方式来进行通信。本篇我们就来看看进程的这些交互方式。
#include
int pipe(int fd[2]);
一个进程向存储空间的一端写入信息,另一个进程存储空间的另外一端读取信息,这个就是管道。ls -l | grep string
:将ls -l的结果通过管道传递给grep。
一般管道的使用方式都是:父进程创建一个管道,然后fork产生一个子进程,由于子进程拥有父进程的副本,所以父子进程可以通过管道进行通信。
对于从父进程到子进程的管道,父进程关闭读端(fd[0]),子进程关闭写端(fd[1]);
对于从子进程到父进程的管道,子进程关闭读端(fd[0]),父进程关闭写端(fd[1])。
#include
int mkfifo(const char *path, mode_t mode);
int mkfifoat(int fd, const char *path, mode_t mode);
POSIX标准中的FIFO又名有名管道或命名管道。我们知道前面讲述的POSIX标准中管道是没有名称的,所以它的最大劣势是只能用于具有亲缘关系的进程间的通信。FIFO最大的特性就是每个FIFO都有一个路径名与之相关联,从而允许无亲缘关系的任意两个进程间通过FIFO进行通信。所以,FIFO的两个特性:
和管道一样,FIFO仅提供半双工的数据通信,即只支持单向的数据流;
和管道不同的是,FIFO可以支持任意两个进程间的通信。
int semget(key_t key, int num_sems, int sem_flags);
int semop(int sem_id, struct sembuf *sem_opa, size_t num_sem_ops);
int semctl(int sem_id, int sem_num, int command, ...);
原子操作:不可中断的一个或者一系列的操作,即一件事要么做要么不做。
临界资源:不同进程能够看到的一份公共资源,一次只能被一个进程使用。
PV操作:由于信号量只能进行两种操作等待和发送信号,即P(sv)和V(sv),他们的行为是这样的:
P(sv):如果信号量大于 0 ,执行 -1 操作;如果信号量等于 0,进程睡眠,等待信号量大于 0。
V(sv):对信号量执行 +1 操作,唤醒睡眠的进程让其完成 P操作。
1. 测试控制该资源的信号量;
2. 若信号量的值为正,则进程可以使用该资源,进程的信号量值减1,表示一个资源被使用;
3. 若此信号量为0,则进程进入休眠,直到该信号量值大于0;
4. 当进程不再使用一个由一个信号控制的共享资源时,该信号量加1,如果有进程正在休眠等待该信号量,则该进程会被唤醒。
问题描述:使用一个缓冲区来保存物品,只有缓冲区没有满,生产者才可以放入物品;只有缓冲区不为空,消费者才可以拿走物品。
因为缓冲区属于临界资源,因此需要使用一个互斥量 mutex 来控制对缓冲区的互斥访问。
为了同步生产者和消费者的行为,需要记录缓冲区中物品的数量。数量可以使用信号量来进行统计,这里需要使用两个信号量:empty 记录空缓冲区的数量,full 记录满缓冲区的数量。其中,empty 信号量是在生产者进程中使用,当 empty 不为 0 时,生产者才可以放入物品;full 信号量是在消费者进程中使用,当 full 信号量不为 0 时,消费者才可以取走物品。
#define N 100
semaphore mutex = 1;
semaphore empty = N;
semaphore full = 0;
void producer() {
while(true) {
P(&empty);
P(&mutex);
insert();
V(&mutex);
V(&full);
}
}
void consumer() {
while(true) {
P(&full);
P(&mutex);
consume();
V(&mutex);
V(&empty);
}
}
问题描述:允许多个进程同时对数据进行读操作,但是不允许读和写以及写和写操作同时发生。
一个整型变量 count 记录在对数据进行读操作的进程数量,一个互斥量 count_mutex 用于对 count 加锁,一个互斥量 data_mutex 用于对读写的数据加锁。
semaphore count_mutex = 1;
semaphore data_mutex = 1;
int count = 0;
void reader() {
while(true) {
P(&count_mutex);
count++;
if(count == 1) P(&data_mutex); // 第一个读者需要对数据进行加锁,防止写进程访问
V(&count_mutex);
read();
P(&count_mutex);
count--;
if(count == 0) V(&data_mutex);
V(&count_mutex);
}
}
void writer() {
while(true) {
P(&data_mutex);
write();
V(&data_mutex);
}
}
int msgget(key_t key, int msgflg);
int msgrcv(int msqid, void *msg_ptr, size_t msg_sz, long int msgtype, int msgflg);
int msgsnd(int msqid, const void *msg_ptr, size_t msg_sz, int msgflg);
int msgctl(int msqid, int cmd, struct msqid_ds *buf);
消息队列是一列具有头和尾的队列,新来的消息放在队列尾部,而读取消息则从队列头部开始,如下图所示:
这样看来,它和管道十分类似,一头读,一头写?的确,看起来很像管道,但又不是管道:
消息队列无固定的读写进程,任何进程都可以读写;而管道需要指定谁读和谁写;
消息队列可以同时支持多个进程,多个进程可以读写消息队列;即所谓的多对多,而管道是点对点;
消息队列只在内存中实现,而管道还可以在磁盘上实现。
int shmget(key_t key, size_t size, int shmflg);
void *shmat(int shm_id, const void *shm_addr, int shmflg);
int shmdt(const void *shmaddr);
int shmctl(int shm_id, int command, struct shmid_ds *buf);
共享内存就是两个进程共同拥有同一片物理内存。对于这片内存中的任何内容,二者均可以访问。要使用共享内存进行通信,进程A首先需要创建一片内存空间作为通信用,而其他进程B则将片内存映射到自己的(虚拟)地址空间。这样,进程A读写自己地址空间中对应共享内存的区域时,就是在和进程B进行通信。
使用共享内存使得进程间的数据不用传送,而是直接访问内存,也加快了程序的效率。
可用于任意两个进程之间通信。
但是两个进程必须在同一台物理机上,而且完全性脆弱。
套接字(Socket)的功能非常强大,可以支持不同层面、不同应用、跨网络的通信。使用套接字进行通信需要双方均创建一个套接字,其中一方作为服务器方,另外一方作为客户方。服务器方必须首先创建一个服务区套接字,然后在该套接字上进行监听,等待远方的连接请求。客户方也要创建一个套接字,然后向服务器方发送连接请求。服务器套接字在受到连接请求之后,将在服务器方机器上新建一个客户套接字,与远方的客户方套接字形成点到点的通信通道。之后,客户方和服务器方便可以直接通过类似于send和recv的命令在这个创建的套接字管道上进行交流了。