轻松Linux-9.进程间通信
来了来了
1.序言
这一章,我们就来看看进程间通信,程序和系统的运作都离不开进程之间的通信。
进程间通信一般是为了以下几点:
数据传输:进程间的数据传输。
资源共享:将指定的资源共享给多个进程。
通知事件:进程工作时,也需要通知其它一个或一组进程,告诉它们发送了什么事件(进程结束时要通知其父进程)。
进程控制:有时我们需要一个进程部分或完全控制另一个进程,拦截其它进程所有陷入和异常的状态,以及时的知道该进程的状态,例如:debug时。
进程间通信主要有这几种:
管道:又分为匿名管道和命名管道。
System V IPC:System V消息队列、System V共享内存、System V信号量。
POSIX IPC:消息队列、共享内存、信号量、互斥量、条件变量、读写锁。
IPC(Inter-Process Communication,进程间通信) 是计算机操作系统中,不同进程之间交换数据或同步操作的机制。它允许独立运行的进程(可能由不同用户或程序创建)协调工作,共享资源或传递信息,从而构建复杂的分布式或并行系统。
2.管道
2.1匿名管道
需要包的头文件
#include <unistd.h>函数功能:创建一个匿名管道
int pipe(int fd[2]);参数:传入一个文件描述符数组,fd[0]表示读端,fd[1]表示写端。成功返回0,失败返回错误代码。
//从键盘输入,再从管道读取。
#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文件就是命名管道。
Shell或者控制台可以用这串指令创建命名管道
mkfifo filename(文件名)
#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方式,只要将内存映射到目标进程的地址空间内,之后的数据传输就不再需要进入内核的系统调用,即不再涉及内核。
↓内核中的数据结构↓
/* 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共享内存所需函数:
定义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。唤醒一个等待的进程。
核心函数:
#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结构
bro参考的Linux内核版本是linux-5.0-rc3struct ipc_ids {int in_use;unsigned short seq;struct rw_semaphore rwsem;struct idr ipcs_idr;int max_idx;
#ifdef CONFIG_CHECKPOINT_RESTOREint next_id;
#endifstruct 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;