inode 节点 (index node):给每个文件赋予一个称为 i 节点的数据结构。
inode 一开始是存储在硬盘中的,只有当文件被打开的时候,其对应的 i 节点才加载到内存中。
总结:
Linux 中,用户态通过读写文件的 Api 进行系统调用,在内核态中,上层是虚拟文件操作系统 VFS,它为用户态提供统一接口,屏蔽底层实现细节,VFS 层定义了底层具体的文件系统需要实现的接口,VFS 层往下对接不同的具体的文件系统如 ext4,具体的文件系统再去操作磁盘的文件块信息
Linux 中每个文件对应一个称为 iNode
的数据结构,inode
中包含了文件的元数据以及若干的块地址信息,inode
一开始存储在磁盘中,当文件被打开时,inode
节点会被加载到内存当中
每个进程的task_struct
中包含files_struct
结构体,files_struct
中又包含一个fd
数组fd_array
,fd_array
中则包含对应文件的文件操作符file
,file
文件操作符是通过inode
去读写文件的,inode
中定义了inode_options
,而具体的底层文件系统则实现了inode_options
中定义的对应读写接口的具体方法
Linux 进程间通信方式:管道、共享内存、信号量、消息队列
一个文件可以同时被多个进程访问
所以,我们可以使用文件来实现进程间的通信,管道就是基于文件系统来实现的。
父进程在复制子进程时,会把父进程的相关信息全部拷贝过来,其中就包括file_struct
结构体,而这个结构体中就包含了文件读写inode
的两个文件描述符,一个 file_0
只读, 一个 file_1
只写,由于是复制的,所以父子进程的这俩文件描述符是指向的同一个文件的inode
。
此时把父进程的 file_0
只读文件描述符 close 掉,把子进程的 file_1
只写文件描述符 close 掉,父进程只保留只写文件描述符,子进程只保留只读文件描述符,这样父进程就可以和子进程通信了(父进程只写,子进程只读,半双工)。
匿名管道底层实现:
匿名管道通过虚拟文件系统 VFS 调用底层的 pipefs 内存文件系统,也就是说底层实现是基于 pipefs 文件系统的。
pipefs 文件系统的数据结构:https://www.processon.com/view/link/62822757e401fd36f6bcc5dd
管道在内存中的实现本质就是一段内核 buffer 内存,不同的文件操作符(一个读一个写)对这段 buffer 进行读写操作。
关于 ps -ef | grep systemd 命令背后的匿名管道的底层实现数据结构:
命名管道底层实现流程图:
管道是基于文件系统来实现的,也就是多个进程对同一个文件进行读写来实现进程间通信
进程和子进程之间的管道通信:父进程在fork
子进程时,会把父进程相关信息全部拷贝过来,其中包括file_struct
结构体,file_struct
中包含了文件读写inode
的两个文件描述符,一个file_0
只读,一个file_1
只写,由于是复制的,所以父子文件的这俩文件描述符是指向同一个文件的inode
, 此时把父进程的file_0
只读 fd 关闭掉,然后再把子进程的file_1
只写 fd 关闭掉,父进程只保留只写 fd ,子进程只保留只读 fd ,这样父进程就可以和子进程进行通信了(父进程写,子进程读)。
匿名管道的虚拟文件系统 VFS 对应的底层文件系统实现是基于 pipefs
内存文件系统
管道在内存中的实现本质就是一段内核 buffer 内存,不同的文件操作符(一个读一个写)对这段 buffer 进行读写操作。
用户态:read
/write
→ 内核态 VFS:task_struct
→ files_struct
→ fd_array
→ fds[0]
fds[1]
→ file0
file1
→ file_opts
→ inode
→ pipe_inode_info
→ pipe_bufs
shmget
- allocates a System V shared memory segment
#include
#include
// 返回根据 key 生成的 shmid
int shmget(key_t key, size_t size, int shmflg);
参数含义:
key
:唯一标识新创建的共享内存
size
:共享内存的大小,向上取整成PAGE_SIZE
的倍数
shmflg
:一些标志信息
IPC_CREAT
:根据 key
判断对应的共享内存段是否存在,如果不存在,则创建;如果存在,则返回已经存在的共享内存段
IPC_EXCL
: 和 IPC_CREAT
一起用,如果已经存在 key
对应的共享内存 则失败
读写权限信息
shmat
— 映射共享内存到进程的虚拟地址空间,返回映射的虚拟内存段的起始地址shmdt
— 解除映射,如果成功返回 0,否则返回 -1#include
#include
void *shmat(int shmid, const void *shmaddr, int shmflg);
int shmdt(const void *shmaddr);
参数含义:
shmid
:共享内存的唯一标识 id
,即填入由 shmget
函数返回的值shaddr
: 内存映射起始地址,如果是NULL
的话,内核会分配shaflg
:是一组标志位,通常为0
。注意:创建和映射共享内存操作只是在内核中维护一些数据结构,并没有真的分配物理内存。真正分配物理内存是在访问这块虚拟内存地址中的数据发生缺页异常时,由缺页异常处理程序维护进程页表中的虚拟页号和物理页号的映射关系的。
这里进程 A 和进程 B 访问的是同一块物理内存上的相同的物理页。
参考代码:
共享内存的底层原理是基于 tmpfs 文件系统:https://www.processon.com/view/link/6277c3921e085327716f5971
共享内存的原理:不同进程的虚拟内存地址会映射到相同的物理内存上,这样两个进程通过访问同一块物理内存,达到通信的目的。(一般情况下,不同进程的虚拟地址是映射到不同物理地址的)
在创建共享内存时并没有真的分配物理内存,真的分配是进程在读、写数据的时候,发生缺页异常,由缺页异常处理程序分配共享内存(物理内存)的页号到进程的虚拟页表中
共享内存的底层原理是基于 tmpfs
文件系统, Linux中一切皆文件
问题:mmap 内存映射和 shm 共享内存有什么区别?
在一个进程内,多个线程同时更新共享资源,有数据并发安全问题,解决方案有:
多个进程同时更新共享内存(共享资源),也有数据安全问题,解决方案:信号量
IPC 的信号量 (semaphore)
原理思想和并发编程中的信号量是一样的,但是两者的实现完全不同:
IPC 的信号量实现很复杂,是在内核态中实现的,而并发编程中的信号量是在用户态实现,基于原子操作实现
IPC 的信号量是操作系统层面用于解决多个进程之间的共享内存并发读写问题,并发编程中的信号量用于解决同一个进程的多个线程之间的共享资源读写问题
一个是在内核态实现的,一个是应用程序代码中实现的。
参考代码:
创建消息队列:msgget
- get a System V message queue identifier
#include
#include
#include
int msgget(key_t key, int msgflg); // 函数返回返回新创建的消息队列的 id
参数含义:
key
:唯一标识新创建的消息队列
msgflg
:一些标志信息
IPC_CREAT
:根据 key
判断对应的共享内存段是否存在,如果不存在,则创建;如果存在,则返回已经存在的共享内存段
IPC_EXCL
:和 IPC_CREAT
一起用,如果已经存在 key
对应的共享内存,则失败
读写权限信息
发送和接收消息:msgsnd
, msgrcv
- System V message queue operations
#include
#include
#include
int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg); // 返回值:成功返回0;失败返回-1
ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtype, int msgflg); // 成功返回接收个数,失败,返回-1
msqid
: 由msgget
函数返回的消息队列的标识符msgp
: 消息缓冲区指针,指针指向准备发送/接收的消息msgflg
: 为0
表示阻塞方式,设置IPC_NOWAIT
表示非阻塞方式异步接发消息msgsz
: 是msgp
指向的消息长度,这个长度不含保存消息类型的那个long int
长整型msgtype
:0
,那么读取消息队列中的第一条消息0
,那么读取消息队列中的第 msgtype
条消息(这里是读取类型等于msgtype
的第一条消)0
,那么读取小于等于msgtype
绝对值最小的 msgtype
的消息参考代码:
int main() {
int mq_id = get_mq_id();
struct msg_buffer buffer;
printf("enter message type: ");
scanf("%d", &buffer.mtype);
printf("enter message contenit:");
scanf("%s", &buffer.mtext);
int len = strlen(buffer.mtext) + 1;
if (msgsnd(mq_id, &buffer, len, IPC_NOWAIT) == -1) {
perror("fail to send message.");
exit(1);
}
return 0;
}
#include
#include "mq.h"
int main() {
int mq_id = get_mq_id();
struct msg_buffer buffer;
int type;
scanf("%d", &type);
if (msgrcv(mq_id, &buffer, 1024, type, IPC_NOWAIT) == -1) {
perror("fail to recv message.");
exit(1);
}
printf("received message type : %d, text: %s, \n", buffer.mtype, buffer.mtext);
return 0;
}