轻松Linux-9.进程间通信

来了来了


1.序言

这一章,我们就来看看进程间通信,程序和系统的运作都离不开进程之间的通信。

进程间通信一般是为了以下几点:

**数据传输:**进程间的数据传输。

**资源共享:**将指定的资源共享给多个进程。

**通知事件:**进程工作时,也需要通知其它一个或一组进程,告诉它们发送了什么事件(进程结束时要通知其父进程)。

**进程控制:**有时我们需要一个进程部分或完全控制另一个进程,拦截其它进程所有陷入和异常的状态,以及时的知道该进程的状态,例如:debug时。

进程间通信主要有这几种:

**管道:**又分为匿名管道和命名管道。

**System V IPC:**System V消息队列、System V共享内存、System V信号量。

**POSIX IPC:**消息队列、共享内存、信号量、互斥量、条件变量、读写锁。

IPC(Inter-Process Communication,进程间通信) 是计算机操作系统中,不同进程之间交换数据或同步操作的机制。它允许独立运行的进程(可能由不同用户或程序创建)协调工作,共享资源或传递信息,从而构建复杂的分布式或并行系统。


2.管道

2.1匿名管道

cpp 复制代码
需要包的头文件
#include <unistd.h>

函数功能:创建一个匿名管道
int pipe(int fd[2]);

参数:传入一个文件描述符数组,fd[0]表示读端,fd[1]表示写端。
      成功返回0,失败返回错误代码。
cpp 复制代码
//从键盘输入,再从管道读取。
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>

int main()
{
    int fd[2];
    char buffer[233];
    int len;

    if(pipe(fd) == -1)
    {
        perror("make pipe");
        exit(1);
    }

    while(fgets(buffer, 233, stdin))
    {
        len = strlen(buffer);
        if(write(fd[1], buffer, len) != len)
        {
            perror("write pipe");
            exit(1);
        }
        
        memset(buffer, 0, sizeof(buffer));
    
        if((len = read(fd[0], buffer, sizeof(buffer))) == -1)
        {
            perror("read pipe");
            exit(1);
        }
        
        if(write(1, buffer, len) == -1)
        {
            perror("write stdout");
            exit(1);
        }
    }    

    return 0;
}

若要父子间进程通信,可参看下图↓

再关闭父进程或子进程的读端或写端,就可实现一方负责读,一方负责写。

父子进程的文件描述符如下

↓从内核的角度来看就是↓

从这里可以看出,管道其实也被抽象成了文件,内核通过file_operation来调用管道相关的接口。

2.1.1匿名管道的读写以及特点

当管道没有数据可读时(读相关):

未设置O_NONBLOCK:read函数会被阻塞,即进程会被暂停执行,直到有数据可读为止。

设置了O_NONBLOCK:read调用会返回-1,errno的值会被设置为EAGAIN

当管道数据已满时(写相关):

未设置O_NONBLOCK:write同样会被阻塞,直到可以写入为止。

设置了O_NONBLOCK:write调用同样会直接返回-1,并将errno设置为EAGAIN

EAGAIN是:Linux/Unix 系统中的一个错误码(errno),表示资源暂时不可用,但稍后重试可能成功。可以理解为"资源暂时不可用,稍后再试"。

如果管道所有写端的文件描述符被关闭,read函数会返回0。

如果管道所有读端的文件描述符被关闭,write函数会产生SIGPIPE信号,进而可能会使write进程退出。

当要写入的数据量不大于PIPE_BUF 时,linux将保证写入原子性
当要写入的数据量大于PIPE_BUF 时,linux将不再保证写入原子性PIPE_BUF 通常为4KB512B,取决于系统。

匿名管道一般只能用于有共同祖先(具有亲缘关系,如父子进程)进程的通信,一般是一个进程创建管道,然后调用fork函数,之后父子进程就可以通信了。

管道(Pipe)提供流式服务 ,意味着它以连续、无固定边界的数据流形式传输数据,而非一次性传输完整的数据块。它具有先进先出的特点,有点像水流(字节流)。可以用两个管道来实现全双工(同时进行写或读)。

管道是半双工的,即只能双方交替进行数据传输,不能同时进行读或写,内核会对管道操作进行同步和互斥,并且一般管道的生命周期同进程一样。

2.2命名管道

匿名管道还是不方便,只能用于有亲缘关系进程间的通信,有没有更强的呢?

有的兄弟,有的。命名管道就是一个解决方案,我们可以创建FIFO文件来完成这份工作,FIFO文件就是命名管道。

bash 复制代码
Shell或者控制台可以用这串指令创建命名管道
mkfifo filename(文件名)
cpp 复制代码
#include <sys/types.h>  // 提供系统数据类型定义(如mode_t)
#include <sys/stat.h>   // 提供文件模式和权限相关定义(如S_IRUSR、S_IWUSR等)

int mkfifo(const char* pathname, mode_t mode);

参数:
pathname:有名管道的路径名(如 "/tmp/my_fifo")。
mode:设置管道文件的权限(如 0666 表示所有用户可读写)。

成功时返回 0,失败时返回 -1 并设置 errno。

删除管道文件要使用unlink()函数:
#include <unistd.h>  // 包含 unlink() 的声明
 
int unlink(const char *pathname);
参数:
pathname:为要删除的文件或符号链接的路径。

创建好管道后可以使用open()函数来操作,就行文件操作一样。

命名管道与匿名管道的区别:创建、删除和打开略有区别,除此之外几乎没有不同。

2.2.1命名管道的打开规则

如果当前为读打开FIFO文件:

设置O_NONBLOCK:open()函数会阻塞,直到有进程以写打开。

未设置O_NONBLOCK:直接返回成功。但后续read()会返回-1,并设置errno为EAGAIN。

如果当前为写打开FIFO文件:

设置O_NONBLOCK:open()函数会阻塞,直到有进程以读打开。

未设置O_NONBLOCK:直接失败返回-1,errno设置为ENXIO。

目的:确保数据生产者(写端)和数据消费者(读端)同时存在,避免无效操作。


3.System V共享内存

System V共享内存是最快的IPC方式,只要将内存映射到目标进程的地址空间内,之后的数据传输就不再需要进入内核的系统调用,即不再涉及内核。

↓内核中的数据结构↓

cpp 复制代码
/* Obsolete, used only for backwards compatibility and libc5 compiles */
struct shmid_ds {
	struct ipc_perm		shm_perm;	/* operation perms */
	int			shm_segsz;	/* size of segment (bytes) */
	__kernel_time_t		shm_atime;	/* last attach time */
	__kernel_time_t		shm_dtime;	/* last detach time */
	__kernel_time_t		shm_ctime;	/* last change time */
	__kernel_ipc_pid_t	shm_cpid;	/* pid of creator */
	__kernel_ipc_pid_t	shm_lpid;	/* pid of last operator */
	unsigned short		shm_nattch;	/* no. of current attaches */
	unsigned short 		shm_unused;	/* compatibility */
	void 			*shm_unused2;	/* ditto - used by DIPC */
	void			*shm_unused3;	/* unused */
};

System V共享内存所需函数:

cpp 复制代码
定义System V IPC(进程间通信)相关的结构体和常量(如key_t类型、IPC_CREAT、IPC_EXCL等标志)。
#include <sys/ipc.h>

包含共享内存函数的声明及共享内存状态结构体(如struct shmid_ds)的定义。
#include <sys/shm.h>

定义基本数据类型(如key_t、size_t),这些类型在共享内存操作中用于标识键值和内存大小。
#include <sys/types.h>

提供ftok函数(用于生成唯一的键值key_t)
#include <unistd.h>

//用于创建共享内存
int shmget(key_t key, size_t size, int shmflg);
参数:
    key:这个共享内存段名字。
    size:共享内存大小。
    shmflg:由九个权限标志构成,它们的用法和创建文件时使用的mode模式标志是一样的
           取值为IPC_CREAT:共享内存不存在,创建并返回;共享内存已存在,获取并返回。
           取值为IPC_CREAT | IPC_EXCL:共享内存不存在,创建并返回;共享内存已存在,出
           错返回。

返回值:成功返回一个非负整数,即该共享内存段的标识码;失败返回-1
-------------------------------------------------------------------------------------
//将共享内存映射到进程地址空间
void *shmat(int shmid, const void *shmaddr, int shmflg);
参数:
    shmid: 共享内存标识。
    shmaddr:指定连接的地址。
    shmflg:它的两个可能取值是SHM_RND和SHM_RDONLY。
返回值:成功返回一个指针,指向共享内存第一个节;失败返回-1。
注:
1.如果shmaddr为NULL,有系统选择地址。
2.shmaddr不为NULL且shmflg无SHM_RND标记,则以shmaddr为连接地址。
3.shmaddr不为NULL且shmflg设置了SHM_RND标记,则连接的地址会自动向下调整为SHMLBA的整数倍。
  公式:shmaddr - (shmaddr % SHMLBA)。
4.shmflg=SHM_RDONLY,表示连接操作用来只读共享内存。
-------------------------------------------------------------------------------------
//让当前进程与共享内存脱离,但不等于删除共享内存片段
int shmdt(const void *shmaddr);
参数:
    shmaddr: 由shmat所返回的指针。
返回值:成功返回0;失败返回-1。
-------------------------------------------------------------------------------------
//控制共享内存
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
参数:
    shmid:由shmget返回的共享内存标识码。
    cmd:将要采取的动作(有三个可取值)。
    buf:指向一个保存着共享内存的模式状态和访问权限的数据结构。
返回值:成功返回0;失败返回-1。

cmd参数的取值:
IPC_STAT: 获取共享内存段的状态信息,将内核中的struct shmid_ds数据复制到用户空间的buf指针指向的结构体中。(需要当前进程有共享内存的读权限)
IPC_SET: 修改共享内存的属性(如权限、所有者ID等)。
        仅允许修改struct shmid_ds中的以下字段:
        shm_perm.uid(所有者用户ID)
        shm_perm.gid(所有者组ID)
        shm_perm.mode(权限位,仅低9位有效)。
        需要当前进程的用户ID是共享内存的创建者或所有者。
        即,改变其他进程(所属其他用户或用户组)的访问权限。
IPC_RMID: 删除共享内存段。内核会标记该段为"待删除",但实际释放内存需等待所有附加进程分离(通过shmdt)后异步完成。
权限需求与IPC_SET一样。

System V消息队列这里不作详细介绍,可以看看其他佬的博文。


4.System V信号量

System V信号量是一种用于进程间同步与互斥的机制,也属于System V IPC。它通过管理公共资源的访问权限,来协调多个进程对同一份临界区资源的操作,避免数据竞争和不一致的问题。

信号量的本质是:一个非负整数的计数器,用于记录可用资源的数量,我们可以通过P、V操作来对信号量进行操作,实现资源的申请和释放。

一些相关概念:

1.多个执行流(进程)看到的公共资源,称之为共享资源。

2.被保护起来的资源,叫做临界资源。

3.常见的保护方式:同步和互斥。多个执行流,访问临界资源的时候,且有一定的顺序性,为同步。任意时刻,只能有一个执行流访问临界资源,叫做互斥。

4.系统中的某些资源一次只允许一个执行流访问,叫做互斥资源。

5.在程序中涉及到临界资源部分的代码,叫做临界区,代码也可以分为:临界区和非临界区。

所以对共享资源进行保护,实则是在限制访问临界资源的执行流。

信号量:

**一元信号量:**一个非0即1的计数器,用于互斥锁(用于确保只有一个进程可以进行操作)。

**计数信号量:**值可以为任何非负整数,用于管理多份同类资源(例如数据库连接池)。

关键操作--P、V操作(P\V操作是原子的,确保多进程并发时的正确性和唯一性):

**P操作:**用于申请资源,信号量计数器-1。如果计数器<=0,则会阻塞,直到计数器>0才会执行。

**V操作:**用于释放操作,信号量计数器+1。唤醒一个等待的进程。

核心函数:

cpp 复制代码
#include <sys/sem.h>

//创建或获取信号量集
int semget(key_t key, int nsems, int semflg);
参数:
    key:唯一标识信号量集的键值(通常用ftok生成)。
    nsems:集中信号量的数量。
    semflg:权限标志(如0666)和控制标志(如IPC_CREAT、IPC_EXCL)。
返回值:成功返回信号量集标识符(semid),失败返回-1。
-------------------------------------------------------------------------------------
//控制信号量集
int semctl(int semid, int semnum, int cmd, ...);
参数:
    semid:信号量集标识符。
    semnum:信号量编号(信号量集中的索引)。
    cmd:控制命令(如SETVAL设置初始值、IPC_RMID删除信号量集、GETVAL获取值)。
    ...:可选参数(如union semun用于SETVAL)。
返回值:成功返回0或特定值,失败返回-1。
-------------------------------------------------------------------------------------
//执行P/V操作
int semop(int semid, struct sembuf *sops, size_t nsops);
参数:
    semid:信号量集标识符。
    sops:指向struct sembuf数组的指针,定义操作类型(P/V)和信号量编号。
    nsops:操作数量(通常为1)。
返回值:成功返回0,失败返回-1。
-------------------------------------------------------------------------------------
定义P/V操作的结构体,是 System V 信号量操作的核心结构体,定义在 <sys/sem.h> 头文件中。
struct sembuf {
    unsigned short sem_num; // 信号量编号
    short sem_op;          // 操作值(P:-1,V:+1)
    short sem_flg;         // 操作标志(如SEM_UNDO)
};


↓需要自己定义↓
用于semctl的SETVAL等命令:
union semun {
    int val;                // 信号量初始值(SETVAL)
    struct semid_ds *buf;   // 信号量集属性(IPC_STAT/IPC_SET)
    unsigned short *array;  // 信号量值数组(GETALL/SETALL)
};

想要深入了解System V更多的信息,可以去查阅相关资料或问问ai。


5.内核中的IPC结构

cpp 复制代码
bro参考的Linux内核版本是linux-5.0-rc3

struct ipc_ids {
	int in_use;
	unsigned short seq;
	struct rw_semaphore rwsem;
	struct idr ipcs_idr;
	int max_idx;
#ifdef CONFIG_CHECKPOINT_RESTORE
	int next_id;
#endif
	struct rhashtable key_ht;
};
......
/* used by in-kernel data structures */
struct kern_ipc_perm {
	spinlock_t	lock;
	bool		deleted;
	int		id;
	key_t		key;
	kuid_t		uid;
	kgid_t		gid;
	kuid_t		cuid;
	kgid_t		cgid;
	umode_t		mode;
	unsigned long	seq;
	void		*security;

	struct rhash_head khtnode;

	struct rcu_head rcu;
	refcount_t refcount;
} ____cacheline_aligned_in_smp __randomize_layout;
......
/* one msq_queue structure for each present queue on the system */
struct msg_queue {
	struct kern_ipc_perm q_perm;
	time64_t q_stime;		/* last msgsnd time */
	time64_t q_rtime;		/* last msgrcv time */
	time64_t q_ctime;		/* last change time */
	unsigned long q_cbytes;		/* current number of bytes on queue */
	unsigned long q_qnum;		/* number of messages in queue */
	unsigned long q_qbytes;		/* max number of bytes on queue */
	struct pid *q_lspid;		/* pid of last msgsnd */
	struct pid *q_lrpid;		/* last receive pid */

	struct list_head q_messages;
	struct list_head q_receivers;
	struct list_head q_senders;
} __randomize_layout;
......
/* One queue for each sleeping process in the system. */
struct sem_queue {
	struct list_head	list;	 /* queue of pending operations */
	struct task_struct	*sleeper; /* this process */
	struct sem_undo		*undo;	 /* undo structure */
	struct pid		*pid;	 /* process id of requesting process */
	int			status;	 /* completion status of operation */
	struct sembuf		*sops;	 /* array of pending operations */
	struct sembuf		*blocking; /* the operation that blocked */
	int			nsops;	 /* number of operations */
	bool			alter;	 /* does *sops alter the array? */
	bool                    dupsop;	 /* sops on more than one sem_num */
};
......
struct shmid_kernel /* private to the kernel */
{
	struct kern_ipc_perm	shm_perm;
	struct file		*shm_file;
	unsigned long		shm_nattch;
	unsigned long		shm_segsz;
	time64_t		shm_atim;
	time64_t		shm_dtim;
	time64_t		shm_ctim;
	struct pid		*shm_cprid;
	struct pid		*shm_lprid;
	struct user_struct	*mlock_user;

	/* The task created the shm object.  NULL if the task is dead. */
	struct task_struct	*shm_creator;
	struct list_head	shm_clist;	/* list by creator */
} __randomize_layout;

《进击的巨人》

相关推荐
FII工业富联科技服务几秒前
GW级AI数据中心怎么建?Omniverse、CFD、液冷与BMS技术路径拆解
服务器·人工智能·算法·能源·制造
AAA代码批发商1 分钟前
Days 32 Linux 多线程:线程分离属性、多进程 vs 多线程、线程同步互斥锁、信号量
linux·笔记
技术不支持3 分钟前
wsl 离线安装 Debian 11, Debian 12等指定版本
运维·debian
Databuff6 分钟前
使用Pinpoint作分布式链路跟踪系统
运维·分布式·运维开发·开源软件
李少兄8 分钟前
Linux 服务器从零部署 kkFileView 完全指南(Docker 方案)
linux·服务器·docker
小张同学a.9 分钟前
OpenStack 私有云实战 3—— Neutron 网络服务与 Dashboard 控制台部署
linux·运维·服务器·openstack
DogDaoDao11 分钟前
HarmonyOS 深度解析:从微内核到 ArkTS 工程实战
android·linux·ios·华为·程序员·harmonyos·harmonyos next
味悲15 分钟前
HAProxy 负载均衡实战:从环境搭建到七层/四层/HTTPS 全解析
运维·https·负载均衡
从零开始的嵌入式之旅16 分钟前
day31
linux·c语言·经验分享·笔记·嵌入式硬件
云飞云共享云桌面24 分钟前
机械设计研发成本优化:怎样用一台高性能服务器替代 10 台 SolidWorks 工作站?
大数据·运维·服务器·汽车·制造