
来了来了
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 通常为4KB 或512B,取决于系统。
匿名管道一般只能用于有共同祖先(具有亲缘关系,如父子进程)进程的通信,一般是一个进程创建管道,然后调用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;
《进击的巨人》
