轻松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;

《进击的巨人》

相关推荐
上海达策TECHSONIC7 小时前
经验分享:如何让SAP B1数据库性能提升50%
运维·数据库·运维开发
月光在发光7 小时前
19_内核模块挂载问题处理
linux·运维·服务器
Liang_GaRy7 小时前
心路历程-Linux如何赋予权限?
linux·运维·服务器
落羽的落羽7 小时前
【C++】C++11的包装器:function与bind简介
c++·学习
Hello阿尔法7 小时前
基于 NFS 的文件共享实现
linux·嵌入式
打不了嗝 ᥬ᭄7 小时前
【Linux】线程概念与控制
linux·c++
pengfei_M7 小时前
四、FVP启动linux
linux·单片机·嵌入式硬件
路溪非溪7 小时前
Linux的gpio子系统
linux·运维·服务器