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();
}
相关推荐
Lana学习中18 分钟前
【运维杂记】连接不上远程服务器的问题处理
运维·服务器
1892280486127 分钟前
NV023固态MT29F16T08GWLCEJ9-QBES:C
大数据·服务器·人工智能·科技·缓存
AOwhisky1 小时前
MySQL 学习笔记(第一期):数据库基础与 MySQL 初探
运维·数据库·笔记·学习·mysql·云计算
Peace1 小时前
【Prometheus】
linux·运维·prometheus
LZZ and MYY3 小时前
RTS 在windows和Linux之间ShareMem
linux·运维·服务器
aningx3 小时前
openSUSE Leap 16.0 运行 sunshine 报错的解决方法
linux
爱学习的徐徐3 小时前
Linux 基础IO
linux·服务器
蛋蛋的学习记录3 小时前
C#窗体应用中使用EasyModbusCore通讯
服务器·c#·tcp
zt1985q3 小时前
本地部署源代码管理解决方案 Bitbucket Data Center 并实现外部访问
运维·服务器·数据库·网络协议·postgresql·源代码管理
xiaobobo33303 小时前
面向对象:linux内核中函数转数据的用法
linux·面向对象·隔离·函数指针绑定