文章目录

进程间通信(IPC)方法:管道、信号量、共享内存、消息队列、套接字

一、有名管道

管道可以用来在两个进程之间传递数据,如: ​​ps -ef | grep bash​​​, 其中​​|​​就是管道,其作用就是将 ps 命令的结果写入管道文件,然后 grep 再从管道文件中读出该数据进行过滤。管道通信方式为 半双工点对点通信

Linux中的进程通信_linux


创建管道文件命令:​​mkfifo <文件名>​​ 默认大小:4096字节

Linux中的进程通信_linux_02

  1. 写入数据时,写指针后移;读取数据时,读指针后移(两指针在内存中循环移动)
  2. 管道文件可读数据为读写指针之间的数据
  3. 每次读取的数据量为min(​​管道可读数据量​​​,​​期望读的数据量​​)
  4. 当读指针追上写指针,表示管道文件读空了;当写指针追上读指针,表示管道文件写满了;

读进程

#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<string.h>
#include<fcntl.h>

int main(){
int fd = open("fifo", O_RDONLY);
if(fd == -1){
exit(0);
}
printf("fd = %d\n", fd);

while(1){
char buff[128] = {0};
// 对于管道文件,写入一次,不会被同一进程读取数据
int n = read(fd, buff, 128);
printf("n = %d\n", n);
if(n == 0){
// 返回值为0,说明写进程关闭管道文件
// 阻塞说明等待写进程写入
break;
}
printf("%s", buff);
}
close(fd);
exit(0);
}

写进程

#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<string.h>
#include<fcntl.h>
#include<signal.h>

void sig_fun(int sig){
printf("sig = %d\n", sig);
}

int main(){
signal(SIGPIPE, sig_fun);
int fd = open("fifo", O_WRONLY);
if(fd == -1){
exit(0);
}
printf("fd = %d\n", fd);
while(1){
char buff[128] = {0};
printf("input:");
fflush(stdout);
fgets(buff, 128, stdin);
if(strcmp(buff, "end\n") == 0){
break;
}
// 当读进程关闭时,再写入则收到内核发送的SIGPIPE,导致程序结束
int n = write(fd, buff, strlen(buff));
}
close(fd);
exit(0);
}

当读进程关闭时,再写入则收到内核发送的SIGPIPE,导致程序结束

管道文件在内存里,不在硬盘里,显示永远为0

Linux中的进程通信_信号量_03

二、无名管道

父子进程可利用无名管道进行通信,fork后管道文件描述符的引用计数分别加1

#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>

int main(){
// fd[0]:读 fd[1]: 写
int fd[2];
if(pipe(fd) == -1){
printf("create pipe error\n");
exit(0);
}

// 创建子进程
pid_t pid = fork();
// fork后读写端的文件描述符的引用计数分别加1
if(pid == -1){
close(fd[0]);
close(fd[1]);
exit(0);
}

if(pid == 0){
// 子进程读数据,关闭写端,即写端的引用计数减1
close(fd[1]);
char buff[128] = {0};
read(fd[0], buff, 128);
printf("child read : %s\n", buff);
close(fd[0]);
}else{
// 父进程写数据,关闭读端,即读端的引用计数减1
close(fd[0]);
write(fd[1], "hello", 5);
close(fd[1]);
}
exit(0);
}

管道特点:

  • 无论有名还是无名,写入管道的数据都在内存中
  • 管道是一种半双工通信方式
  • 有名和无名管道的区别:有名可以在任意进程间使用,而无名主要在父子进程间

无名管道创建后只有2个文件描述符,一般是通过fork给子进程进行通信;而对于有名管道,只要能open都可以进行读或写

三、信号量

临界资源: 同一时刻,只允许被一个进程或线程访问的资源
临界区: 访问临界资源的代码段

使用信号量实现3个进程打印:ABCABCABC…
​​​sem.h​

#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<string.h>
#include<sys/sem.h>

#define SEM_NUM 3
#define SEM1 0
#define SEM2 1
#define SEM3 2

union semun{
int val;
};

void sem_init();

// 操作第几个信号量
void sem_p(int index);

void sem_v(int index);

void sem_destroy();

​sem.c​

#include "sem.h"

static int semid = -1;

void sem_init(){
// 信号量初始值
int arr[SEM_NUM] = {1,0,0};
// 全新创建信号量集合(玩游戏建房)
semid = semget((key_t)2345, SEM_NUM, IPC_CREAT|IPC_EXCL|0600);
if(semid == -1){
// 若信号量已经被创建了,则全新创建失败
// 现根据key_gen获取已有信号量
semid = semget((key_t)2345, SEM_NUM, 0600);
if(semid == -1){
printf("semget error\n");
return;
}
}else{
// 全新创建成功,需要对信号量进行初始化
union semun a;
for(int i=0; i<SEM_NUM; i++){
a.val = arr[i];
// 给信号量集合semid中的i号信号量设置初始值a
if(semctl(semid, i, SETVAL, a) == -1){
printf("semctl setval\n");
}
}
}
}

void sem_p(int index){
if(index < 0 || index >= SEM_NUM){
printf("index out of range");
return ;
}
struct sembuf buf;
// 信号量集合的索引
buf.sem_num = index;
// 对信号量的操作
buf.sem_op = -1;
// 异常中止时,系统自动归还资源
buf.sem_flg = SEM_UNDO;
// 进行PV操作,1表示从&buf开始只有一个元素
if(semop(semid, &buf, 1) == -1){
printf("semop error");
}
}

void sem_v(int index){
if(index < 0 || index >= SEM_NUM){
printf("index out of range");
return ;
}
struct sembuf buf;
// 信号量集合的索引
buf.sem_num = index;
// 对信号量的操作
buf.sem_op = 1;
// 异常中止时,系统自动归还资源
buf.sem_flg = SEM_UNDO;
// 进行PV操作
if(semop(semid, &buf, 1) == -1){
printf("semop error");
}
}

void sem_destroy(){
// 信号量集合id,信号量集合下标
// 销毁只看id和命令,不看第二个参数:下标
if(semctl(semid, 0, IPC_RMID) == -1){
printf("semctl IPC_RMID error");
}
}

​a.c​

#include "sem.h"

int main(){
sem_init();
for(int i=0; i<5; i++){
sem_p(SEM1);
printf("A");
fflush(stdout);
sem_v(SEM2);
sleep(1);
}
}

​b.c​

#include "sem.h"

int main(){
sem_init();
for(int i=0; i<5; i++){
sem_p(SEM2);
printf("B");
fflush(stdout);
sem_v(SEM3);
sleep(1);
}
}

​c.c​

#include "sem.h"

int main(){
sem_init();
for(int i=0; i<5; i++){
sem_p(SEM3);
printf("C");
fflush(stdout);
sem_v(SEM1);
sleep(1);
}
// 最后打印C,所以由c.c销毁
sem_destroy();
}

​ipcs -s​​ :查看信号量

​ipcrm -s <sem-id>​​:删除信号量

Linux中的进程通信_linux_04

四、信号量实现生产者消费者模型

理论可参考:​经典进程同步和互斥问题​

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <time.h>
#include <pthread.h>
#include <semaphore.h>
#include <sys/stat.h>

#define BUFF_SIZE 10
#define PRODUCER 2
#define CONSUMER 3

volatile int buffer[BUFF_SIZE];
volatile int in = 0;
volatile int out = 0;

sem_t empty;
sem_t full;
pthread_mutex_t mutex;

void* producer(void* id){
int t_id = (int)id;
while(1){
sem_wait(&empty);
pthread_mutex_lock(&mutex);
buffer[in] = rand() % 100;
printf("生产者线程%d在%d的位置放出了数据%d\n", t_id, in, buffer[in]);
in = (in + 1) % BUFF_SIZE;
pthread_mutex_unlock(&mutex);
sem_post(&full);
}
}

void* consumer(int id){
int t_id = (int)id;
while(1){
sem_wait(&full);
pthread_mutex_lock(&mutex);
printf("消费者线程%d在%d的位置放出了数据%d\n", t_id, out, buffer[out]);
out = (out + 1) % BUFF_SIZE;
pthread_mutex_unlock(&mutex);
sem_post(&empty);
}
}

int main(){
srand((unsigned int)time(NULL));

// 信号量,能否在两个进程间使用,初始值
sem_init(&empty, 0, BUFF_SIZE);
sem_init(&full, 0, 0);
// 锁,锁的属性
pthread_mutex_init(&mutex, NULL);
pthread_t producer_id[PRODUCER];
pthread_t consumer_id[CONSUMER];

for(int i = 0; i < PRODUCER; i++){
// 创建线程成功后,线程id会保存在第一个参数
pthread_create(&producer_id[i], NULL, producer, (void*)i);
}

for(int i = 0; i < CONSUMER; i++){
pthread_create(&consumer_id[i], NULL, consumer, (void*)i);
}

for(int i = 0; i < PRODUCER; i++){
// 等待线程结束,第二个参数为返回给主线程的信息
pthread_join(producer_id[i], NULL);
}

for(int i = 0; i < CONSUMER; i++){
pthread_join(consumer_id[i], NULL);
}

// 销毁信号量、互斥锁
sem_destroy(&empty);
sem_destroy(&full);
pthread_mutex_destroy(&mutex);

exit(0);
}

五、共享内存

共享内存为多个进程之间共享和传递数据提供了一种有效的方式。

共享内存是先在物理内存上申请一块空间,多个进程可以将其映射到自己的虚拟地址空间中。所有进程都可以访问共享内存中的地址,就好像它们是由 malloc 分配的一样,无需拷贝,就是使用的公共内存

如果某个进程向共享内存写入了数据,所做的改动将立刻被可以访问同一段共享内存的任何其他进程看到。由于它并未提供同步机制,所以我们通常需要用其他的机制来同步对共享内存的访问。

Linux中的进程通信_管道文件_05

六、消息队列

FIFO

消息队列独立于进程存在,只要写入了,消息一直都在(除非读取),和程序是否运行没有关系,消除在同步命名管道的打开和关闭时可能出现的问题。

需要添加消息类型,才能写入消息队列,以消息为单位进行写入和读取

读取的时候,需要指定消息类型,同一消息类型的数据块按顺序读取,直到消息队列中此类型的消息被读空。在函数中,也可指定消息类型为​​0​​,表示所有类型的数据都可以读取。

Linux中的进程通信_#include_06