Linux:共享内存
Zane Lv4

共享内存


定义

共享内存是Linux中进程通信最直接最快速的方法,共享内存允许两个进程访问的同一块物理内存,则其中一个进程对缓存数据进行更新后,其他进程就会受到影响,因此就可以完成多个进程间的通信。


方法

  1. 基于传统SYS V的共享内存

  2. 基于POSIX mmap文件映射实现共享内存

  3. 通过memfd_create()和fd跨进程共享实现共享内存

  4. 多媒体、图形领域广泛使用的基于dma-buf的共享内存


System V共享内存的使用和原理

共享内存不像消息队列、信号量是内核空间的系统对象,不需要花费额外的数据拷贝,但正因如此没有预防竟态条件,即当多进程利用共享内存进行通信时,需要程序员自己设计锁实现同步关系

创建过程

创建一块共享内存,其实就是在tmpfs中创建一个文件(存储在内存中),同时意味着tmpfs中创建了一个iNode节点;

共享内存创建后不会随着进程的结束而被释放,只有系统重启或被进程明确删除

ipcrm + 共享内存 id 命令删除


API的使用
ftok

把一个已存在的路径名和一个整数标识符转换成一个key_t值,称为System V IPC键值

C
1
2
3
4
5
6
# include <sys/shm.h>
# include <sys/types.h>
key_t ftok ( const char* fname, int proj_id )


成功返回一个key_t值, 失败返回-1

fname 一个必须存在且能被访问到的目录

proj_id id是子序号,它是一个8bit的整数。即范围是0~255。可以自己约定


shmget
C
1
2
3
4
5
6
7
8
#include <sys/shm.h>
int shmget (key_t key, size_t size, int shmflag);

用来创建一块共享内存并返回其 id
或者获得一块已经被创建的共享内存的


成功返回一个正整数值,即为共享内存的标识符,失败时返回-1,错误存储 errno

key_t key用来唯一标识一段全局共享内存, 通常通过 ftok 函数获得

size_t size 是创建的共享内存的大小, 单位为字节, 如果是要创建一块共享内存, 此参数必须被指定, 如果是要获取一块创建好的共享内存的 id, 可以将其设置为 0

int shmflag 为 0 为获取共享内存 id, 为 IPC_CREAT 时是创建一个新的共享内存, 通常要同时指定权限 (和权限进行 | 运算)
同时还能取IPC_EXCL , 只有在共享内存不存在的时候,新的共享内存才建立,否则就产生错误。

ps: 使用 shmget 创建的共享内存段会全部被初始化为 0, 同时和他关联的内核数据结构 shmid_ds 将被创建和初始化

C
1
2
3
4
5
6
7
8
9
10
11
12
13
struct shmid_ds { 
struct ipc_perm shm_perm; /* 操作权限 */
size_t shm_segsz; /* 大小,单位是字节 */
__kernel_time_t shm_atime; /* 对这段内存最后一次调用shmat的时间 */
__kernel_time_t shm_dtime; /* 最后一次调用shmdt的时间*/
__kernel_time_t shm_ctime; /* 最后一次调用shmctl的时间*/
__kernel_ipc_pid_t shm_cpid; /* 创建者的pid*/
__kernel_ipc_pid_t shm_lpid; /* 最后一次执行shmat或shmdt的进程的pid*/
unsigned short shm_nattch; /*目前关联到次共享内存的进程的数量*/
unsigned short shm_unused; /* 以下为填充 */
void *shm_unused2; /* ditto - used by DIPC */
void *shm_unused3; /* unused */
};

shmat

创建共享内存就是创建一个文件,需要映射其到当前进程,使用shmget创建一块共享内存后便用到shmat将他关联到当前进程,使用完不再需要后,需要使用shmdt将其分离(暂时理解成绑定—解绑的关系)

C
1
2
3
4
#include <sys/shm.h>
void* shmat ( int shm_id, const void* shm_addr, int shmflag );

成功返回映射到进程的地址空间即共享内存地址 失败返回 (void *)-1并将错误存储于 errno

shm_id由函数shmget 返回的共享内存id

shm_addr指定共享内存在进程内存的值的映射位置,一般使用NULL,即由内核自己决定

shmflag SHM_RDONLU:只读方式映射

                  0:可读可写

shmat 调用成功后, 会修改 shmid_ds 的部分字段:
    将 shm_nattch 加一 ,可以理解成被连接到这一地址的进程数
    将 shm_lpid 设置为调用进程 pid
    将 shm_atime 设置为当前时间


shmdt

在共享内存使用后将其从进程地址空间中分离

C
1
2
3
4
#include <sys/shm.h>
int shmdt ( const void* shm_addr );

成功返回 0, 失败返回 -1

shm_addr 参数是共享内存在进程的映射地址, 即 shmat 返回的值

shmdt 并不会删除共享内存
调用成功时修改内核数据结构 shmid_ds 部分字段:
    将 shm_nattch 减一
    将 shm_lpid 设置为调用进程的pid
    将 shm_dtime 设置为当前时间


shmctl

管理共享内存

C
1
2
3
4
#include 
int shmctl ( int shm_id, int command, struct shmid_ds* buf );

失败返回-1, 并存储错误于 errno, 成功时的返回值取决于 command

shm_id 是共享内存标识符
command 指定要执行的命令

常用命令为

                    IPC_RMID, 即 删除共享内存

                    IPC_STAT获取共享内存的属性信息,由参数buf返回

                    IPC_SET设置共享内存的属性,由参数buf传入

若共享内存已经与所有访问它的进程断开了连接,则调用IPC_RMID子命令后,系统将立即删除共享内存的标识符,并删除该共享内存区,以及所有相关的数据结构;

如果仍有别的进程与该共享内存保持连接,则调用IPC_RMID子命令后,该共享内存并不会被立即从系统中删除,而是被设置为IPC_PRIVATE状态,并被标记为”已被删除”;直到已有连接全部断开,该共享内存才会最终从系统中消失。


System V 实例 01

要求

创建一个长度为10的 Stu 数组, 向其中写入数据, 另一个进程读取
代码很简单, 就不做注释了

C
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
/*
write.c
*/

# include <stdio.h>
# include <stdlib.h>
# include <string.h>
# include <unistd.h>
# include <sys/ipc.h>
# include <sys/shm.h>

char Stu[10];

int main(){
int key_t = ftok ( "/home/czz/task/Linux/shared_memory/instance_01", 233 );
if( key_t == -1 )
{
printf("获取key_t失败\n");
exit(1);
}
int shm_id = shmget( key_t, sizeof(Stu), IPC_CREAT | 0644 );
if(shm_id == -1){
perror("shmget "),exit(1);
}
char *addr = shmat(shm_id, NULL, 0);
for( int i = 0; i < 5; i++ ){
addr[i] = 'a';
}
shmdt(addr);

return 0;
}

C
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
/*
read.c
*/


#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/ipc.h>
#include <sys/shm.h>

char Stu[10];

int main(){
int key_t = ftok ( "(绝对路径)", 233 );
if( key_t == -1 )
{
printf("获取key_t失败\n");
exit(1);
}
int shm_id = shmget( key_t, sizeof(Stu), IPC_CREAT);
if( shm_id == -1 )
{
perror("shmget "), exit(1);
}

void *p = NULL;
p = shmat( shm_id, NULL, 0 );
if ( p == (void *)-1 ){
perror("***** : ");
}
char *data = p;
printf("here is message:\n");

for( int i = 0; i < 10; i++ ){
printf("%c\n", data[i]);
}
shmdt(p);

shmctl(shm_id, IPC_RMID, 0);


return 0;
}

运行结果

C
1
2
3
4
5
6
7
8
// read.c
czz@CzzPC:~/task/Linux/shared_memory/instance_01$ ./read
here is message:
a
a
a
a
a

代码比较简单就不再进行注释,函数步骤就是上面5个API的使用。


POSIX共享内存

创建过程
  1. 指定一个名字参数调用shm_opem, 以创建一个新的共享内存区对象或打开一个已存在的共享内存区对象

  2. 调用mmap把这个共享内存区映射到调用进程的地址空间


API的使用
shm_open

穿件并打开一个新的共享内存对象或者打开一个既存的共享内存对象, 与函数open的用法是类似的. 函数返回值是一个文件描述符,会被下面的API使用.

C
1
2
3
4
5
int shm_open ( const char *name, int ofalg, mode_t mode )


返回一个非负整数,表示编号最小的整数 未使用的文件描述符。
否则,它应返回 -1 并设置 errno 以指示错误。

name

name 参数符合路径名的构造规则,

flags

O_RDONLY

仅对读取访问权限开放。

O_RDWR

打开以进行读取或写入访问。

可以在 oflag的值中指定其余标志的任意组合:

O_CREAT

如果共享内存对象存在,则此标志不起作用,除非在下面O_EXCL中所述。否则,共享内存 对象被创建;共享内存对象的用户 ID 应设置为进程的有效用户 ID;组 ID 的 共享内存对象设置为系统缺省组标识或进程的有效组标识。的权限位 共享内存对象应设置为 mode 参数的值,但在文件模式创建掩码 过程。当设置了文件权限位以外的模式中的位时,效果是未指定的。mode 参数不会影响是否打开共享内存对象进行读取、写入或同时打开两者。共享内存对象 大小为零。

O_EXCL

如果设置了 O_EXCL 和 O_CREAT,那么如果共享内存对象存在,则 shm_open() 将失败。检查是否存在 共享内存对象和对象的创建(如果不存在)对于执行 shm_open() 的其他进程使用 O_EXCL 和 O_CREAT 集命名相同的共享内存对象是原子的。如果设置了O_EXCL而未设置O_CREAT,则 结果未定义。

O_TRUNC

如果共享内存对象存在,并且O_RDWR成功打开,则该对象应被截断为零长度,并且 此函数调用的模式和所有者应保持不变。将O_TRUNC与O_RDONLY一起使用的结果是不确定的。

shm_open的mode参数总是必须指定,当指定了O_CREAT标志时,mode为用户权限位,否则将mode设为0

shm_open的返回值是一个描述符,它随后用作mmap的第五个参数fd


删除一个Posix共享内存对象

C
1
2
int shun_unlink(const char*name);
成功返回 1, 失败返回 -1

ftruncate

设置共享内存对象的大小,新创建的共享内存对象大小为0,或修改已经存在的Posix共享内存对象大小

C
1
2
3
#include <unistd.h>
//成功返回0,失败返回-1
int ftruncate( int fd, off_t length):
  • 对于普通文件,若文件长度大于length,额外的数据会被丢弃;若文件长度小于length,则扩展文件大小到length

  • 对于Posix共享内存对象,ftruncate把该对象的大小设置成length字节

创建新的Posix共享内存对象时指定大小是必须的,否则访问mmap返回的地址会报bus error错误


stat
C
1
2
3
4
5
#include <sys/stat.h>
#include <sys/types.h>

//成功返回0,失败返回-1
int fstat(int fd, struct stat *buf);

stat结构有12个或以上的成员,然而当fd指代一个Posix共享内存对象时,只有四个成员含有信息

C
1
2
3
4
5
6
7
struct stat
{
mode_t st_mode; //用户访问权限
uid_t st_uid; //user id of owner
gid_t st_gid; //group id of owner
off_t st_size; //文件大小
};

mmap

mmap函数把一个文件或一个Posix共享内存对象映射到调用进程的地址空间

  • 使用普通文件以提供内存映射IO

  • 使用特殊文件以提供匿名内存映射

  • 使用Posix共享内存对象以提供Posix共享内存区

C
1
2
//成功返回映射内存的起始地址,失败返回MAP_FAILED
void *mmap(void *addr, size_t len, int prot, int flags, int fd, off_t offset);

addr 指定映射内存的起始地址,通常是NULL,即由内核自主决定

len 被映射到调用进程地址空间中的字节数,从被映射文件fd开头起第offset个字节处开始算的,offset通常设置为0

prot 指定应摄取的保护通常为PROT_READ | PROT_WRITE

flags 必须在MAP_SHARED 和 MAP_PRIVATE 这两个标志中选择一个,进程间共享内存给需要使用MAP_SHARED

prot 说明 flags 说明
PROT_READ 数据可读 MAP_SHARED 变动是共享的
PROT_WRITE 数据可写 MAP_PRIVATE 变动是私有的
PROT_EXEC 数据可执行 MAP_FIXED 准确地解释addr参数
PROT_NONE 数据不可访问

mmap成功返回后,可以关闭fd,这对已建立的映射关系没有影响。
注意,不是所有文件都能进行内存映射,例如终端和套接字就不可以。


munmap

mmap 建立的映射关系通过该函数删除,其中addrmmap返回的地址,len是映射区的大小,同mmaplen参数

C
1
2
//成功返回0,失败返回-1
int munmap(void *addr, size_len);

msync

默认情况下,内核采用虚拟内存算法保持内存映射文件与内存映射区的同步,前提是指定了MAP_SHARED标志,但这种同步可能不是立即生效的,而是在随后某个时间进行。

但有时候我们修改完数据并进行下一步操作之前,需要确认数据已经同步完成,这时可调用msync函数。

1
2
//成功返回0,失败返回-1
int msync(void *addr, size_t len, int flags);

其中addrlen含义同munmapflags使用下表中的常值,其中MS_ASYNCMS_SYNC这两个常值中必须选择指定一个。

flags 说明
MS_ASYNC 执行异步写,msync立即返回
MS_SYNC 执行同步写,msync等同步完成才返回
MS_INVALIDATE 使高速缓存的数据失效

POSIX 实例

要求

  • 父子进程通过内存映射IO共享一片内存
  • 父子进程共同给共享内存区中的一个计数器持续加1

无名信号量:用于线程间同步或互斥

有名信号量:一般用于进程的同步或互斥

父子进程同步—-有名信号量
C
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
# include <stdio.h>
# include <stdlib.h>
# include <string.h>
# include <unistd.h>
# include <fcntl.h>
# include <semaphore.h>
# include <sys/mman.h>

struct Shared{
int count;
};

// #define MMAP_FILE "/home/delphi/mmap_file"
// #define SEM_PATH "/sem_mmap"

int main(){
struct Shared shared;
struct Shared *ptr; //共享内存的地址,mmap返回的类型是地址
sem_t *mutex; //进程同步,使用二值信号量模拟互斥锁
int fd;
int i;

fd = shm_open ( "POSIX", O_RDWR | O_CREAT, 0666 ); // 打开一个新的共享内存对象
if( fd == -1 )
{
perror("shm_open "), exit(1);
}
ftruncate( fd, sizeof(shared) ); // 设置共享内存大小
memset(&shared, 0, sizeof(shared));
write( fd, &shared, sizeof(shared) ); //将映射文件内容初始化为0
if( write( fd, &shared, sizeof(shared) ) == -1 )
{
perror( "write" ),exit(1);
}

ptr = mmap( NULL, sizeof(shared), PROT_WRITE | PROT_READ, MAP_SHARED, fd, 0 );
// 使用Posix共享内存对象以提供Posix共享内存, 返回映射内存的起始地址

shm_unlink( "POSIX" );

mutex = sem_open( "sem_mmap", O_CREAT, 0666 );
sem_unlink( "sem_mmap" );

setbuf( stdout, NULL ); // 设置标准输出为无缓冲

if( fork() == 0 ) // 若是子进程
{
for ( i = 0; i < 10; i++ )
{
sem_wait(mutex); // P操作
printf( "child: %d\n", ptr->count );
ptr->count++;
sem_post( mutex ); // V操作
}

exit(0); //最后执行的是这个语块,所以要exit
}

for ( i = 0; i < 10; i++ )
{
sem_wait( mutex );
printf( "parent: %d\n",ptr->count );
ptr->count++;
sem_post ( mutex );
}

return 0;
}

父子进程同步—-无名信号量
C
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
# include <stdio.h>
# include <stdlib.h>
# include <string.h>
# include <unistd.h>
# include <fcntl.h>
# include <semaphore.h>
# include <sys/mman.h>

struct Shared{
sem_t mutex;
int count;
};


int main(){
struct Shared shared;
struct Shared *ptr; //共享内存的地址,mmap返回的类型是地址
sem_t *mutex; //进程同步,使用二值信号量模拟互斥锁
int fd;
int i;

fd = shm_open ( "POSIX", O_RDWR | O_CREAT, 0666 ); // 打开一个新的共享内存对象
if( fd == -1 )
{
perror("shm_open "), exit(1);
}
ftruncate( fd, sizeof(shared) ); // 设置共享内存大小
memset(&shared, 0, sizeof(shared));
write( fd, &shared, sizeof(shared) ); //将映射文件内容初始化为0,否则数据会被污染


ptr = mmap( NULL, sizeof(shared), PROT_WRITE | PROT_READ, MAP_SHARED, fd, 0 );
// 使用Posix共享内存对象以提供Posix共享内存, 返回映射内存的起始地址

shm_unlink( "POSIX" );

sem_init( &ptr->mutex, 1, 1 ); //Posix 无名信号量建立在ptr指向的共享内存中
setbuf( stdout, NULL ); // 设置标准输出为无缓冲

if( fork() == 0 ) // 若是子进程
{
for ( i = 0; i < 10; i++ )
{
sem_wait( &ptr->mutex ); // P操作
printf( "child: %d\n", ptr->count );
ptr->count++;
sem_post( &ptr->mutex ); // V操作
}

exit(0); //最后执行的是这个语块,所以要exit
}

for ( i = 0; i < 10; i++ )
{
sem_wait( &ptr->mutex );
printf( "parent: %d\n",ptr->count );
ptr->count++;
sem_post ( &ptr->mutex );
}
sem_destroy( &ptr->mutex );
return 0;
}

输出结果

C
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
parent: 0
parent: 1
parent: 2
parent: 3
parent: 4
parent: 5
parent: 6
parent: 7
parent: 8
parent: 9
child: 10
child: 11
child: 12
child: 13
child: 14
child: 15
child: 16
child: 17
child: 18
child: 19

父子进程同步—-匿名内存映射

上面两种内存映射IO代码中,前两步都是

  • 调用 shm_open 创建一个POSIX共享内存对象

  • 调用 write 将该对象内容初始化为0

如果仅仅用于父子进程间共享内存,可以使用匿名内存避免文件的显示创建和初始化,方法是:

  • mmap 调用时,参数flags设为MAP_SHARED | MAP_ANON, fd 设为 -1 offset设为0

  • 匿名内存映射保证这样的内存区初始化为0

C
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <semaphore.h>
#include <sys/mman.h>

struct Shared
{
sem_t mutex;
int count;
};

int main()
{
struct Shared *ptr;
int i;

ptr = mmap( NULL, sizeof(struct Shared), PROT_READ | PROT_WRITE, MAP_SHARED | MAP_ANON, -1, 0 );
sem_init( &ptr->mutex, 1, 1 );
setbuf( stdout, NULL );



if (fork() == 0)
{
for (i = 0; i < 10; i++)
{
sem_wait(&ptr->mutex);
printf("child: %d\n", ptr->count);
ptr->count++;
sem_post(&ptr->mutex);
}

exit(0);
}

for (i = 0; i < 10; i++)
{
sem_wait(&ptr->mutex);
printf("parent: %d\n", ptr->count);
ptr->count++;
sem_post(&ptr->mutex);
}

sem_destroy(&ptr->mutex);

return 0;


}

menfd_create

C
1
2
3
4
int memfd_create(const char *name, unsigned int flags);


const char* name:表示的是因为文件的文件名,不必真实存在

memfd_create()会创建一个匿名文件并返回一个指向这个文件的文件描述符.

这个文件就像是一个普通文件一样,所以能够被修改,截断,内存映射等等.

不同于一般文件,此文件是保存在RAM中.一旦所有指向这个文件的连接丢失,那么这个文件就会自动被释放.匿名内存用于此文件的所有的后备存储.

所以通过memfd_create()创建的匿名文件和通过mmap以MAP_ANONYMOUS的flag创建的匿名文件具有相同的语义.

这个文件的初始化大小是0,之后可以通过ftruncate或者write的方式设置文件大小.

memfd_create()函数提供的文件名,将会在/proc/self/fd所指向的连接上展现出来,但是文件名通常会包含有memfd的前缀.

这个文件名仅仅只是用来debug,对这个匿名文件的使用没有任何的影响,同时多个文件也能够有一个相同的文件名.

Linux 中的进程可以通过cmgs,用于在 Socket上 传递控制信息(也称 Ancillary Data),使用 SCM_RIGHTS ,进程可以透过 UNIX Socket 把一个或多个fd传递给另一个进程;


发送fd函数
C
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
static void send_fd( int socket, int *fds, int n )
{
struct msghdr msg = {0};
struct cmsghdr *cmsg;
char buf[CMSG_SPACE(n * sizeof(int))],data;
memset(buf, '\0', sizeof(buf));
struct iovec io = { .iov_base = &data, .iov_len = 1 };

msg.msg_iov = &io;
msg.msg_iovlen = 1;
msg.msg_control = buf;
msg.msg_controllen = sizeof(buf);

cmsg = CMSG_FIRSTHDR(&msg);
cmsg->cmsg_level = SOL_SOCKET;
cmsg->cmsg_type = SCM_RIGHTS;
cmsg->csmg_len = CMSG_LEN(n*sizeof(int));

memcpy ((int *) CMSG_DATA(cmsg), fds, n * sizeof(int));

if (sendmsg ( socket, &msg, 0 ) < 0)
handle_error ("Failed to send message");
}

接受fd函数
C
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
static int * recv_fd( int socket, int n )
{
int *fds = malloc ( n * sizeof(int) );
struct msghdr msg = {0};
struct cmsghdr *cmsg;
char buf[CMSG_SPACE(n*sizeof(int))], data;
memset(buf, '\0', sizeof(buf));
struct iovec io = { .iov_base = &data, .iov_len = 1 };

msg.msg_iov = &io;
msg.msg_iovlen = 1;
msg.msg_control = buf;
msg.msg_controllen = sizeof(buf);

if (recvmsg (socket, &msg, 0 ) < 0)
handle_error("Failed to recive message");

cmsg = CMSG_FIRSTHDR(&msg);

memcpy ( fds, (int *) CMSG_DATA(cmsg), n * sizeof(int) );

return fds;
}

这两个函数的解析就不再详细展开,参考引用中的blog

神奇的地方在于,假设进程A作为发送端有一个文件的fd是100,经过上面两个函数以后,进程B接收到的还是100么?

答案是否定的,比如进程B收到进程A的fd的时候,进程B自身fd空间里面自己的前面200个fd都已经被占用了,那么进程B接受到的fd就可能是201。


memfd_create 的使用

memfd_create()这个函数的玄妙之处在于它会返回一个“匿名”内存“文件”的fd,而它本身并没有实体的文件系统路径

用法
C
1
2
3
4
5
6
int fd = memfd_create( "shma", 0 );
ftruncate( fd, size );

void *ptr = mmap(NULL, size, PROT_READ | prot_write, MAP_SHARED, fd, 0);
strcpy (ptr, "hello A");
munmap(ptr, size);
解析

通过memfd_create()创建一个”文件”,但实际映射的是一片内存,并非真的有路径的文件

因此,可以对一个fd进行当成文件一样的操作

memfd_create()得到了fd,它在行为上类似规则的fd,所以也可以透过socket来进行甩锅,这样A进程相当于把一片与fd对应的内存,分享给了进程B。

实例

说明

进程A通过memfd_create()创建了2片4MB的内存,并且透过socket(路径/tmp/fd-pass.socket)发送给进程B这2片内存对应的fd:

C
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
int main(int argc, char *argv[]){
int sfd, fds[2];
struct sockaddr_un addr;

sfd = socket(AF_UNIX, SOCK_STREAM, 0);
if(sfd == -1)
handle_error("Failed to create socket");

memset(&addr, 0, sizeof(struct sockaddr_un));
addr.sun_family = AF_UNIX;
strncpy(addr.sun_path, "/tem/fd-pass.sock", sizeof(addr.sun_path) - 1 );

#define SIZE 0X400000
fds[0] = memfd_create("shma", 0);
if( fds[0] < 0 )
handle_error( "Failed to open file 1 for reading" );
else
fprintf( stdout, "Opened fd %d in parent\n", fds[0] );

ftruncate( fds[0], SIZE );
void *ptr0 = mmap( NULL, SIZE, PROREAD | PRO_WRITE, MAP_SHARED, fds[0], 0 );
memset( ptr0, 'A', SIZE );

fds[1] = memfd_create("shmb", 0);
if(fds[1] < 0)
handle_error("Faild to open file 2 for reading");
else
fprintf( stdout, "Opened fd %d in parent\n", fds[1] );
ftruncate(fds[1], SIZE)

void *ptr1 = mmap( NULL, SIZE, PROREAD | PRO_WRITE, MAP_SHARED, fds[1], 0 );
memset( ptr1, 'B', SIZE );
munmap(ptr1, SIZE);

if(connect(sfd, (struct sockaddr *) &addr, sizeof(struct sockaddr_un) );
handle_error("Failed to connect to socket");

send_fd(sfd, fds, 2);

exit(EXIT_SUCCESS);
}

下面的代码进程B透过相同的socket接受这2片内存对应的fd,之后通过read()读取每个文件的前256个字节并打印:

C
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
int main(int argc, char *argc[])
{
ssize_t nbytes;
char buffer[256];
int sfd, cfd, *fds;
struct sockaddr_un addr;

sfd = socket( AF_UNIX, SOCK_STREAM, 0 );
if(sfd == -1)
handle_error("Failed to create socket");
if(unlink("/tem/fd-pass.socket") == -1 && errno != ENOENT )
handle_error("Removing socket file failed");

memset(&addr, 0 ,sizeof(struct sockaddr_un));
addr.sun_family = AF_UNIX;
strncpy(addr.sun_path, "/emp/fd-pass.socket", sizeof(addr.sun_path) - 1);

if(bind(sfd, (struct sockaddr *) &addr,sizeof(struct sockaddr_un)) == -1)
handle_error("Failed to bind to socket");
if(listen(std, 5) == -1)
handle_error("Failed to listen on socket");
cfd = accept(sfd, NULL, NULL);
if (cfd == -1)
handle_error("Failed to accept incoming connection");
fds = recv_fd(cfd, 2);

for(int i=0; i<2; i++)
{
fprintf(stdot, "Reading from passed fd %d\n", fds[i]);
while((nbytes = read(fds[i], buffer, sizeof(buffer))) > 0)
write(1, buffer, nbytes);
*buffer = '\0';
}

if(close(cfd) == -1)
handle_error("Failed to close client socket");
return 0;
}

上述的代码中,进程B是在进行read(fds[i], buffer, sizeof(buffer)),这体现了基于fd进行操作的regular特点。当然,如果是共享内存,现实的代码肯定还是多半会是mmap:

mmap(NULL, SIZE, PROT_READ, MAP_SHARED, fd, 0);

dma_buf

dma_buf可以实现buffer在多个设备的共享,应用可以把一片底层驱动A的buffer导出到用户空间成为一个fd,也可以把fd导入到底层驱动 B。当然,如果进行mmap()得到虚拟地址,CPU

进程A访问设备A并获得其使用的buffer的fd,之后通过socket把fd发送给进程B,而后进程B导入fd到设备B,B获得对设备A中的buffer的共享访问。如果CPU也需要在用户态访问这片buffer,则进行了mmap()动作。


总结

这次的学习有太多没搞明白的地方,SYS V 以及 POSIX 都还好,但是 memefd_create 那个例子完全没搞明白,跨设备的方法dma_buf也只是草草看了几眼。

目的 了解程度
System V 内存跨进程共享 ⭐⭐⭐⭐
POSIX 内存跨进程共享 ⭐⭐⭐⭐
memfd_create 内存跨进程共享、封存 ⭐⭐
dma_buf 内存跨设备共享

TO-DO(挖坑)

  • 详细学习memfd_create以及dma_buf

参考

tmpfs

tmpfs是一种虚拟内存文件系统,而不是块设备。是基于内存的文件系统,创建时不需要使用mkfs等初始化
它最大的特点就是它的存储空间在VM(virtual memory),VM是由linux内核里面的vm子系统管理的。

用途

LINUX中可以把一些程序的临时文件放置在tmpfs中,利用tmpfs比硬盘速度快的特点提升系统性能。

iNode

操作系统读取硬盘的时候,不会一个个扇区地读取,这样效率太低,而是一次性连续读取多个扇区,即一次性读取一个”块”(block)。这种由多个扇区组成的”块”,是文件存取的最小单位。”块”的大小,最常见的是4KB,即连续八个 sector组成一个 block

而索引节点 iNode 是用来存储文件的元信息,包括文件的创建者、创建时间、大小等

信息

ASCIIDOC
1
2
3
4
5
6
7
8
9
10
11
12
13
* 文件的字节数

* 文件拥有者的User ID

* 文件的Group ID

* 文件的读、写、执行权限

* 文件的时间戳,共有三个:ctime指inode上一次变动的时间,mtime指文件内容上一次变动的时间,atime指文件上一次打开的时间。

* 链接数,即有多少文件名指向这个inode

* 文件数据block的位置

iNode的大小

硬盘格式化的时候,操作系统自动将硬盘分成两个区域。一个是数据区,存放文件数据;另一个是inode区(inode table),存放inode所包含的信息。

每个inode节点的大小,一般是128字节或256字节。inode节点的总数,在格式化时就给定,一般是每1KB或每2KB就设置一个inode。假定在一块1GB的硬盘中,每个inode节点的大小为128字节,每1KB就设置一个inode,那么inode table的大小就会达到128MB,占整块硬盘的12.8%。

查看每个硬盘分区的inode总数和已经使用的数量,可以使用df命令。

如果出现了 iNode 已经用光,但磁盘还没满的情况,这时就无法在硬盘中创建文件。


使用过程

Unix/Linux系统内部不使用文件名,而使用inode号码来识别文件。对于系统来说,文件名只是inode号码便于识别的别称或者绰号。

  1. 系统找到文件名对应的iNode

  2. 通过iNode号获取信息

  3. 根据iNode信息找到文件数据的block,读出数据


引用

剖析Linux内核文件系统之(inode) - 知乎 (zhihu.com)

Linux 下 IPC 之 System V 共享内存的使用和原理

shmctl(2) - Linux manual page (man7.org)

Posix共享内存土豆西瓜大芝麻的博客

int main( int argc, char* argv[] ) 中arg和argv参数的解析及示例_菜又学的博客

宋宝华:世上最好的共享内存(Linux共享内存最透彻的一篇)

简介struct cmsghdr结构cmsghdr

linux结构详解,Linux msghdr结构讲解

整理:Linux网络编程之sockaddr与sockaddr_in,sockaddr_un