Linux——进程间通信

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


假设 A、B两个进程

单工:单向性;确定通信方向后固定,不能相互传输数据

全双工:双向性;同一时刻,两方可以互相传输数据

半双工:双向性;同一时刻,只能一方给另一方传输数据

一、管道

管道可以用来在两个进程之间传递数据。

特性:

(1)管道是单向的,数据只能从左到右

(2)管道会创建子进程来执行命令

(3)管道中的命令是并行执行的

(4)管道可以串联,形成管道链

(一)有名管道

1.有名管道

通过 mkfifo命令创建管道;可以在任意两个进程之间通信。

2.特性

(1)有可见的文件名(存在于文件系统中)

(2)可以被任意进程(需要有权限)访问

(3)数据传输遵循先进先出原则

(4)本质上是内存中的特殊文件,不占用磁盘空间

(5)关闭所有引用它的进程后,管道文件仍然存在(需要手动删除

3.测试

(1)测试一:在命令行输入

读端 :持续监听管道 ;cat < 管道文件

持续读取:while true; do cat < 管道文件; done

写端 :多次写入内容 ;cat > 管道文件

(2)测试二:在文件写入和读取

读文件:使用read函数读文件

注意:read函数的第三个参数不能是初始化的strlen

写文件:使用write函数写数据到管道文件中

有名管道的双向阻塞特性

(1)以 O_WRONLY 打开管道的写端,会阻塞等待 ,直到有进程以 O_RDONLY 方式打开同一个管道。

(2)以 O_RDONLY 打开管道的读端,也会阻塞等待 ,直到有进程以 O_WRONLY 方式打开同一个管道。

只有当读写两端都成功打开管道时,双方的 open() 调用才会解除阻塞,继续执行后续代码。

(二)无名管道

1.无名管道

无名称,仅通过文件描述符访问;主要应用于父子进程间的通信

2.特性

(1)仅支持亲缘进程:父子进程、兄弟进程

(2)创建方式:**pipe()**系统调用

(3)生命周期:随进程的结束而销毁

3.无名管道的使用

(三)管道的特点

|-----------|--------------|------------|
| | 有名管道 | 无名管道 |
| 名称 | 有名,存在文件系统中 | 无名,不在文件系统中 |
| 进程的关系 | 任意进程(有权限) | 父子进程、兄弟进程 |
| 生命周期 | 随文件系统的销毁而i销毁 | 随进程的销毁而销毁 |
| 创建方式 | mkfifo | pipe |

注意事项

(1)避免管道破裂 :读端已关闭的情况下,写端还在输出数据时,会触发 SIGPIPE 信号,终止写端进程

(2)数据无边界 :管道是字节流数据;若按照消息的格式,一条一条发送,需要自定义信息格式(例如长度+数据),避免多读少读

(3)缓冲区阻塞问题 :读空、写满会阻塞进程,所以需要 **fcunt()**设置属性进行处理

(4)资源泄露问题:进程退出前提前关闭管道描述符,会导致缓冲区中的数据丢失

管道的通信方式为半双工

二、共享内存

(一)共享内存原理

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

共享内存是先在物理内存上申请一块空间,多个进程可以将其映射到自己的虚拟地址空间中。所有进程都可以访问共享内存中的地址,就好像它们是由malloc分配的一样。如果某个进程向共享内存写入了数据,所做的改动将立刻被可以访问同一段共享内存的任何其他进程看到。

由于它并未提供同步机制,所以我们通常需要用其他的机制来同步对共享内存的访问。

(二)共享内存的函数与使用

ipcs:可以查看当前系统中进程间通信资源

1.shmget

创建一块共享内存空间。

bash 复制代码
int shmget(key_t key, size_t size, int shmflg);

(1)key:共享内存段的唯一标识

(2)size:表示创建的共享内存空间的大小

创建新共享空间时,该参数填正整数;获取已有共享空间时,该参数可以填0

(3)shmflg:标志位,用来控制函数行为

创建标志

IPC_CREAT:如果 key 不存在,则创建

IPC_EXCL:与 IPC_CREAT 配合使用,如果 key 已经创建则报错并设置错误原因给errno


权限控制:需要指定权限位(例如0600)

(4)返回值:-1表示共享内存创建失败

2.shmat

将一个已存在的共享内存映射到进程的地址空间中。

bash 复制代码
void *shmat(int shm_id, const void *shm_addr, int shmflg);

(1)shm_id:共享内存段的标识符,通常是 shmget 函数的返回值

(2)shm_addr:指定共享内存段要附加到进程地址空间的哪个地址

NULL:让系统内核自动选择一块合适的,未使用的地址来附加共享内存段

非NULL(填具体地址):系统会将共享内存段附加到指定位置;如果指定位置不能使用,则报错,附加失败

(3)shmflg:控制附加操作标志位

0:默认权限,根据 shmget 创建时的权限进行读写

SHM_RDONLY :以只读方式附加共享内存段,如果该进程尝试写入,则引发段错误

SHM_EXEC:允许在共享内存段上执行代码

共享内存的工作方式时全双工

三、信号量

(一)信号量

信号量,是操作系统用于进程间同步与互斥 的关键机制;解决多个进程对共享资源的竞争问题,避免数据冲突或不一致。

信号量的本质是一个受保护的整数变量 ,其值反映了共享资源的可用数量 ,只能通过特定的原子操作(不可被中断的操作)修改。

原子操作:对信号量的修改(加、减),避免多进程并发修改导致数据不一致。


堵塞机制:当信号量为0时,试图获取该信号量的进程会被阻塞,直到有其他进程释放资源。

(二)信号量的类型

1.二值信号量

值只能是0和1,用于实现互斥(类似锁,一次只允许一个进程运行)

2.计数信号量

值可以是任意非负数,用于控制对多个相同资源的并发访问(假设信号量值为3,则表示最大可支持3个进程同时运行)

(三)P、V原语

P、V原语,信号量的行为由两个基本操作定义(称为"原语",确保原子性)

1.P操作(获取资源)

将信号量值减1

结果 >= 0 ,进程可继续执行;若结果 < 0,进程被阻塞,放入信号量的等待队列

> 0:有可用资源,直接获取

= 0:资源刚好用完,进程也能通过,但下一个进程就会被阻塞

< 0:资源不足,进程必须阻塞等待

2.V操作(释放资源)

将信号量值加1

结果 > 0 ,进程可继续执行;若结果 <= 0,从等待队列中唤醒一个进程,使其继续执行

V操作不会阻塞,永远释放资源

(四)临界资源、临界区

1.临界资源

同一时刻,只允许被一个进程或线程访问的资源

2.临界区

访问临界资源的代码段

(五)信号量的函数

1.semget

用于创建或获取信号量集的系统调用

bash 复制代码
int semget(key_t key, int nsems, int semflg);

(1)key :用于表示信号量集的唯一性,确保不同进程能通过相同的key访问同一信号量集

(2)nsems :信号量的数量(要创建的信号量集中包含的信号量个数)

若是创建新信号量集,则nsems必须大于0;

若是获取已存在的信号量集,则nsems可以设置为0

(3)semflg(标志位):控制信号量集的创建/获取行为

IPC_CREAT:若对应的信号量集不存在,则创建新集;若已存在,则返回其ID

IPC_EXCL:与 IPC_CREAT 组合使用;若信号量集已存在,则创建失败(确保获得新资源)

权限:例如0600

(4)返回值:成功返回信号量的ID,失败返回 -1

2.semctl

用于控制操作信号量集。

bash 复制代码
int semctl(int semid, int semnum, int cmd, ...);

(1)semid:信号量集的标识符(由 semget() 返回),指定要操作的信号量集

(2)semnum:信号量在集中的索引(从0开始),指定要操作的单个信号量

若对整个信号量集进行操作(IPC_RMID),则此参数忽略

(3)cmd:要执行的控制命令,决定 semctl 的行为

SETVAL :设置 semnum 指定的信号量的初始值(需配合第四个参数传递)

GETVAL :设置 semnum 指定的信号量的当前值(返回值为该信号量的值)

IPC_RMID删除整个信号量集(所有进程都无法访问)

IPC_STAT :获取信号量集的状态信息,存入 semid_ds 结构体(创建时间,权限)

IPC_SET :设置信号量集的属性,需要通过 semid_ds 结构体传递新属性

(4)可变参数(可选) :根据 cmd 参数,可能需要传递一个 union semun 类型的参数(需要自定义)

bash 复制代码
union semun
{
    int val; //用于 SETVAL 命令(设置信号量值)
    struct semid_ds *buf; //IPC_STAT 或者 IPC_SET(状态信息)
    unsigned short *array; //GETALL 或者 SETALL(批量操作)
    struct seminfo *_buf; //用于IPC_INFO(系统限制信息)
}

(5)返回值:成功返回 0,失败返回 -1

3.semop

用于执行信号量的操作,对信号量进行改变,做P操作 或者V操作

bash 复制代码
int semop(int semid, struct sembuf *sops, size_t nsops);

(1)semid:信号量集的标识

(2)sops :指向struct sembuf 的结构体指针

bash 复制代码
struct sembuf
{
    unsigned short sem_num; //信号量在集中的索引
    short sem_op; //操作类型:其值为-1,代表P操作;其值为+1,代表V操作
    short sem_flg; //操作标志,一般使用 SEM_UNDO(确保进程异常退出时,系统撤销对信号量的修改,避免永久堵塞)
}

(3)nsops :数组中 sops 元素的数量(要执行的操作总数)

(4)返回值:成功返回 0,失败返回 -1

(六)信号量的使用

bash 复制代码
vi sem.h
cpp 复制代码
/*sem.h*/

#include <stdio.h>
#include <sys/sem.h>

//信号量初始化联合体
union semun
{
        int val;
};

void sem_init(); //信号量的创建、初始化

void sem_p(int val); //P操作

void sem_v(int val); //V操作

void sem_clear(); //清除信号量
bash 复制代码
vi sem.c
cpp 复制代码
/*sem.c*/

#include <stdio.h>
#include <stdlib.h>
#include <sys/sem.h>
#include "sem.h"

int semid; //用来保存信号量集的ID,全局变量方便所有函数操作

//信号量集的创建、初始化
void sem_init()
{
        semid = semget((key_t)1234, 2, IPC_CREAT|IPC_EXCL|0600); //尝试全新创建信号量集(包含2个信号量)
        if(semid == -1)
        {
                semid = semget((key_t)1234, 2, IPC_CREAT|0600); //信号量集已存在,直接获取ID
                if(semid == -1)
                {
                        exit(1);
                }
        }
        else //如果该信号量集第一次创建,则需要初始化两个信号量的值
        {
                union semun a;
                int arr[2] = {1, 0}; //s1: arr[0] = 1;s2: arr[1] = 0
                for(int i = 0; i < 2; i++)
                {
                        a.val = arr[i]; //给val赋值

                        //设置第i个信号量的值为a.val
                        if(semctl(semid, i, SETVAL, a) == -1) //判断信号量集操作是否成功
                        {
                                exit(1);
                        }
                }
        }
}

//P操作
void sem_p(int val)
{
        struct sembuf buf; //用来描述一次信号量操作的结构体
        buf.sem_num = val; //要操作的信号量下标(0 或 1)
        buf.sem_op = -1; //P操作:信号量值减1
        buf.sem_flg = SEM_UNDO; //进程异常退出时自动恢复信号量值

        //执行信号量操作
        if(semop(semid, &buf, 1) == -1)
        {
                printf("P err!\n");
        }
}

//V操作
void sem_v(int val)
{
        struct sembuf buf; //用来描述一次信号量操作的结构体
        buf.sem_num = val; //要操作的信号量下标(0 或 1)
        buf.sem_op = +1; //V操作:信号量值加1
        buf.sem_flg = SEM_UNDO; //进程异常退出时自动恢复信号量值

        //执行信号量操作
        if(semop(semid, &buf, 1) == -1)
        {
                printf("V err!\n");
        }
}

//清除信号量
void sem_clear()
{
        //删除整个信号量集
        if(semctl(semid, 0, IPC_RMID) == -1)
        {
                printf("rmid err!\n");
        }
}
bash 复制代码
vi 6.w.c
cpp 复制代码
/*6.w.c*/

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

int main()
{
        //创建/获取共享内存段
        int shmid = shmget((key_t)1234, 128, IPC_CREAT|0600);
        if(shmid == -1)
        {
                printf("shmget err!\n");
                exit(1);
        }

        //将共享内存映射到当前进程的虚拟地址空间
        char *s = (void*)shmat(shmid, NULL, 0);
        if(s == (char*) -1)
        {
                printf("shmat err!\n");
                exit(1);
        }

        sem_init(); //初始化信号量集

        char buff[128]; //用于存放用户输入的缓冲区

        //循环写入数据
        while(1)
        {
                fgets(buff, 128, stdin); //从键盘读取一行输入到buff
                sem_p(0); //P操作:申请互斥锁(sem[0]减1)
                strcpy(s, buff); //将buff中的数据写入共享数据
                sem_v(1); //V操作:通知读端"数据已就绪"(sem[1]加1)

                //如果写入的前3个字符是"end",退出循环
                if(strncmp(s, "end", 3) == 0)
                {
                        break;
                }
        }
}
bash 复制代码
vi 6.r.c
cpp 复制代码
/*6.r.c*/

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

int main()
{
        //获取共享内存段
        int shmid = shmget((key_t)1234, 128, IPC_CREAT|0600);
        if(shmid == -1)
        {
                printf("shmget err!\n");
                exit(1);
        }

        //映射共享内存
        char *s = (void*)shmat(shmid, NULL, 0);
        if(s == (char*) -1)
        {
                printf("shmat err!\n");
                exit(1);
        }

        sem_init(); //初始化信号量集

        //循环读取数据
        while(1)
        {
                sem_p(1); //P操作:等待写端数据就绪(sem[1]减1)
                printf("%s\n", s); //打印共享内存中的数据

                //如果读到的前3个字符是"end",退出循环
                if(strncmp(s, "end", 3) == 0)
                {
                        strcpy(s, "123"); //写入一个结束标记,避免写端再次读到"end"
                        break;
                }

                sem_v(0); //V操作:释放互斥锁(sem[0]加1)
        }

        //清理信号信号量集
        sem_clear();
}
相关推荐
AOwhisky1 小时前
Docker 学习笔记:Docker Compose 多容器编排
linux·运维·笔记·学习·docker·容器
j_xxx404_1 小时前
Linux进程信号:内核数据结构与捕捉递达全流程
linux·运维·服务器·人工智能·ai
STARFALL0011 小时前
MySQL 运维
运维·数据库·mysql
浪客灿心1 小时前
Linux网络NAT
linux·网络
Black蜡笔小新1 小时前
企业私有化AI训练推理一体工作站/自动化AI算法训练服务器DLTM让企业AI自主可控
服务器·人工智能·自动化
怀旧,1 小时前
【Linux网络编程】10. NAT技术、代理服务、内网穿透
linux·网络·智能路由器
人生苦短1281 小时前
Ubuntu 系统常用操作命令大全
linux·chrome·ubuntu
醇氧1 小时前
CentOS 7 安装 MySQL 8.0.28 el7 (完美兼容 OpenSSL 1.1)
linux·mysql·centos
Only丿阿海1 小时前
当运维与AI结合 — 用 AI Agent 去维护 Nginx
运维·人工智能·nginx·agent·agent4j