Linux四种共享内存技术(附源码):SystemV、POSIX mmap、memfd_create、dma-buf

Linux 下的进程间通信:管道、消息队列、共享文件、共享内存

【共享内存】基于共享内存的无锁消息队列设计

File Sealing & memfd_create() | 文件密封和memfd_create()

Linux环境无文件渗透执行ELF:memfd_create、ptrace

利用ptrace和memfd_create混淆程序名和参数

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

先给出宋宝华老师的结论:

目录

共享内存的方式

SYS V共享内存

POSIX共享内存

memfd_create

dma_buf

dma_buf定义


 

共享内存的方式


  • 1.基于传统SYS V的共享内存;
  • 2.基于POSIX mmap文件映射实现共享内存;
  • 3.通过memfd_create()和fd跨进程共享实现共享内存;
  • 4.多媒体、图形领域广泛使用的基于dma-buf的共享内存。

 

SYS V共享内存


历史悠久、年代久远、API怪异,对应内核代码linux/ipc/shm.c,当你编译内核的时候不选择CONFIG_SYSVIPC,则不再具备此能力。

你在Linux敲ipcs命令看到的share memory就是这种共享内存:

代码地址:https://github.com/Rtoax/test/tree/master/c/glibc/sys/ipc/shm_rw

writer.c

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

int main()
{
	key_t key = ftok("/dev/shm/myshm1", 0);
	int shm_id = shmget(key, 0x400000, IPC_CREAT | 0666);
	char *p = (char*)shmat(shm_id, NULL, 0);

	memset(p, 'A', 0x400000);
	shmdt(p);

	return 0;
}

reader.c

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

int main()
{
	key_t key = ftok("/dev/shm/myshm1", 0);
	int shm_id = shmget(key, 0x400000, 0666);
	char *p = (char*)shmat(shm_id, NULL, 0);

	printf("%c %c %c %c .\n", p[0], p[1], p[2], p[3]);
	shmdt(p);

	return 0;
}

 

POSIX共享内存


我对POSIX shm_open()、mmap () API系列的共享内存的喜爱,远远超过SYS V 100倍。原谅我就是一个懒惰的人,我就是讨厌ftok、shmget、shmat、shmdt这样的API。

代码地址:https://github.com/Rtoax/test/blob/master/c/glibc/sys/mman/shm_rw/

writer.c

#include <sys/mman.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdlib.h>
#include <string.h>

int main()
{
	int fd = shm_open("posix_shm", O_CREAT|O_RDWR, 0666);
	ftruncate(fd, 0x400000);

	char *p = mmap(NULL, 0x400000, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0);

	memset(p, 'A', 0x400000);
	munmap(p, 0x400000);

	return 0;
}

reader.c

#include <sys/mman.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdlib.h>
#include <string.h>
#include <stdio.h>

int main()
{
	int fd = shm_open("posix_shm", O_RDONLY, 0666);
	ftruncate(fd, 0x400000);

	char *p = mmap(NULL, 0x400000, PROT_READ, MAP_SHARED, fd, 0);

	printf("%c %c %c %c\n", p[0], p[1], p[2], p[3]);
	munmap(p, 0x400000);

	return 0;
}

 

memfd_create


如果说POSIX的mmap让我找到回家的感觉,那么memfd_create()则是万般惊艳。见过这种API,才知道什么叫天生尤物——而且是尤物中的尤物,它完全属于那种让码农第一眼看到就会两眼充血,恨不得眼珠子夺眶而出贴到它身上去的那种API;一般人见到它第一次,都会忽略了它的长相,因为它的身材实在太火辣太抢眼了。

先不要浮想联翩,在所有的所有开始之前,我们要先提一下跨进程分享fd(文件描述符,对应我们很多时候说的“句柄”)这个重要的概念。

众所周知,Linux的fd属于一个进程级别的东西。进入每个进程的/proc/pid/fd可以看到它的fd的列表:

这个进程的0,1,2和那个进程的0,1,2不是一回事。

某年某月的某一天,人们发现,一个进程其实想访问另外一个进程的fd。当然,这只是目的不是手段。比如进程A有2个fd指向2片内存,如果进程B可以拿到这2个fd,其实就可以透过这2个fd访问到这2片内存。这个fd某种意义上充当了一个中间媒介的作用。有人说,那还不简单吗,如果进程A:

fd = open();

open()如果返回100,把这个100告诉进程B不就可以了吗,进程B访问这个100就可以了。这说明你还是没搞明白fd是一个进程内部的东西,是不能跨进程的概念。你的100和我的100,不是一个东西。这些基本的东西你搞不明白,你搞别的都是白搭。

Linux提供一个特殊的方法,可以把一个进程的fd甩锅、踢皮球给另外一个进程(其实“甩锅”这个词用在这里不合适,因为“甩锅”是一种推卸,而fd的传递是一种分享)。我特码一直想把我的bug甩(分)锅(享)出去,却发现总是被人把bug甩锅过来。

那么如何甩(分)锅(享)fd呢?

Linux里面的甩锅需要借助cmsg,用于在socket上传递控制消息(也称Ancillary data),使用SCM_RIGHTS,进程可以透过UNIX Socket把一个或者多个fd(file descriptor)传递给另外一个进程。

比如下面的这个函数,可以透过socket把fds指向的n个fd发送给另外一个进程:

代码地址:https://github.com/Rtoax/test/tree/master/ipc/socket/unsocket-sendmsg-iovec

server.c

#include <fcntl.h>
#include <stdio.h>
#include <errno.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/un.h>
#include <sys/wait.h>
#include <sys/socket.h>

#define handle_error(msg) do { perror(msg); exit(EXIT_FAILURE); } while(0)

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))], dup[256];
    memset(buf, '\0', sizeof(buf));
    
    struct iovec io = { .iov_base = &dup, .iov_len = sizeof(dup) };

    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 receive message");

    cmsg = CMSG_FIRSTHDR(&msg);

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

    return fds;
}

int main(int argc, char *argv[]) 
{
    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 ("/tmp/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, "/tmp/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(sfd, 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 (stdout, "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;
}

client.c

#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/un.h>
#include <sys/wait.h>
#include <sys/socket.h>

#define handle_error(msg) do { perror(msg); exit(EXIT_FAILURE); } while(0)

static void send_fd(int socket, int *fds, int n)  // send fd by socket
{
    struct msghdr msg = {0};
    struct cmsghdr *cmsg;
    char buf[CMSG_SPACE(n * sizeof(int))], dup[256];
    memset(buf, '\0', sizeof(buf));
    struct iovec io = { .iov_base = &dup, .iov_len = sizeof(dup) };


    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->cmsg_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");
}

int main(int argc, char *argv[]) 
{
    int sfd, fds[2];
    struct sockaddr_un addr;

    if (argc != 3) {
        fprintf (stderr, "Usage: %s <file-name1> <file-name2>\n", argv[0]);
        exit (1);
    }

    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, "/tmp/fd-pass.socket", sizeof(addr.sun_path)-1);

    fds[0] = open(argv[1], O_RDONLY);
    if (fds[0] < 0)
        handle_error ("Failed to open file 1 for reading");
    else
        fprintf (stdout, "Opened fd %d in parent\n", fds[0]);

    fds[1] = open(argv[2], O_RDONLY);
    if (fds[1] < 0)
        handle_error ("Failed to open file 2 for reading");
    else
        fprintf (stdout, "Opened fd %d in parent\n", fds[1]);

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

    send_fd (sfd, fds, 2);

    exit(EXIT_SUCCESS);
}

那么问题来了,如果在进程A中有一个文件的fd是100,发送给进程B后,它还是100吗?不能这么简单地理解,fd本身是一个进程级别的概念,每个进程有自己的fd的列表,比如进程B收到进程A的fd的时候,进程B自身fd空间里面自己的前面200个fd都已经被占用了,那么进程B接受到的fd就可能是201。数字本身在Linux的fd里面真地是一点都不重要,除了几个特殊的0,1,2这样的数字外。同样的,如果你把 cat /proc/interrupts 显示出的中断号就看成是硬件里面的中断偏移号码(比如ARM GIC里某号硬件中断),你会发现,这个关系整个是一个瞎扯。

知道了甩锅API,那么重要的是,当它与memfd_create()结合的时候,我们准备甩出去的fd是怎么来?它是memfd_create()的返回值。

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

//	fds[0] = memfd_create("shma", 0);
	fds[0] = syscall(SYS_memfd_create, "shma", 0);
	if(fds[0] < 0){
		handle_error ("SYS_memfd_create error.");
	} else {
		printf("Opened fd %d in parent\n", fds[0]);
	}
	ftruncate(fds[0], SIZE);
	void *ptr0 = mmap(NULL, SIZE, PROT_READ|PROT_WRITE, MAP_SHARED, fds[0], 0);
	memset(ptr0, 'A', SIZE);
	munmap(ptr0, SIZE);

我们透过memfd_create()创建了一个“文件”,但是它实际映射到一片内存,而且在/xxx/yyy/zzz这样的文件系统下没有路径!没有路径!没有路径!

所以,当你在Linux里面编程的时候,碰到这样的场景:需要一个fd,当成文件一样操作,但是又不需要真实地位于文件系统,那么,就请立即使用memfd_create()吧,它的manual page是这样描述的:

memfd_create

memfd_create() creates an anonymous file and returns a file descriptor that refers to it. The file behaves like a regular file, and so can be modified, truncated, memory-mapped, and so on. However, unlike a regular file, it lives in RAM and has a volatile backing storage.

重点理解其中的regular这个单词。它的行动像一个regular的文件,但是它的背景却不regular。

那么,它和前面我们说的透过UNIX Socket甩锅fd又有什么关系呢?memfd_create()得到了fd,它在行为上类似规则的fd,所以也可以透过socket来进行甩锅,这样A进程相当于把一片与fd对应的内存,分享给了进程B。

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

代码地址:https://github.com/Rtoax/test/tree/master/ipc/socket/unsocket-sendmsg-iovec-memfd_create

server.c与上面的一毛一样,但是我还是贴出来

#include <fcntl.h>
#include <stdio.h>
#include <errno.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/un.h>
#include <sys/wait.h>
#include <sys/socket.h>

#define handle_error(msg) do { perror(msg); exit(EXIT_FAILURE); } while(0)

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))], dup[256];
    memset(buf, '\0', sizeof(buf));
    
    struct iovec io = { .iov_base = &dup, .iov_len = sizeof(dup) };

    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 receive message");

    cmsg = CMSG_FIRSTHDR(&msg);

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

    return fds;
}

int main(int argc, char *argv[]) 
{
    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 ("/tmp/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, "/tmp/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(sfd, 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 (stdout, "Reading from passed fd %d\n", fds[i]);
        while ((nbytes = read(fds[i], buffer, sizeof(buffer))) > 0)
            write(1, buffer, nbytes);
        *buffer = '\0';
    }write(1, "\n", 1);

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

    return 0;
}

client.c

#define _GNU_SOURCE         /* See feature_test_macros(7) */
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/un.h>
#include <sys/wait.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/mman.h>
#include <sys/syscall.h>

#define handle_error(msg) do { perror(msg); exit(EXIT_FAILURE); } while(0)

static void send_fd(int socket, int *fds, int n)  // send fd by socket
{
    struct msghdr msg = {0};
    struct cmsghdr *cmsg;
    char buf[CMSG_SPACE(n * sizeof(int))], dup[256];
    memset(buf, '\0', sizeof(buf));
    struct iovec io = { .iov_base = &dup, .iov_len = sizeof(dup) };


    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->cmsg_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");
}

int main(int argc, char *argv[]) 
{
    int sfd, fds[2];
    struct sockaddr_un addr;

#if 0
    if (argc != 3) {
        fprintf (stderr, "Usage: %s <file-name1> <file-name2>\n", argv[0]);
        exit (1);
    }
#endif

    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, "/tmp/fd-pass.socket", sizeof(addr.sun_path)-1);

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

    fds[1] = open(argv[2], O_RDONLY);
    if (fds[1] < 0)
        handle_error ("Failed to open file 2 for reading");
    else
        fprintf (stdout, "Opened fd %d in parent\n", fds[1]);
#else
//    0x400000 - 4M
#define SIZE	0xff
//	fds[0] = memfd_create("shma", 0);
	fds[0] = syscall(SYS_memfd_create, "shma", 0);
	if(fds[0] < 0){
		handle_error ("SYS_memfd_create error.");
	} else {
		printf("Opened fd %d in parent\n", fds[0]);
	}
	ftruncate(fds[0], SIZE);
	void *ptr0 = mmap(NULL, SIZE, PROT_READ|PROT_WRITE, MAP_SHARED, fds[0], 0);
	memset(ptr0, 'A', SIZE);
	munmap(ptr0, SIZE);

    
//	fds[1] = memfd_create("shmb", 0);
	fds[1] = syscall(SYS_memfd_create, "shmb", 0);
	if(fds[1] < 0){
		handle_error ("SYS_memfd_create error.");
	} else {
		printf("Opened fd %d in parent\n", fds[1]);
	}
	ftruncate(fds[1], SIZE);
	void *ptr1 = mmap(NULL, SIZE, PROT_READ|PROT_WRITE, MAP_SHARED, fds[1], 0);
	memset(ptr1, 'B', SIZE);
	munmap(ptr1, SIZE);

#endif
    if (connect(sfd, (struct sockaddr *) &addr, sizeof(struct sockaddr_un)) == -1)
        handle_error ("Failed to connect to socket");

    send_fd (sfd, fds, 2);

    exit(EXIT_SUCCESS);
}

上述代码参考了:

https://openforums.wordpress.com/2016/08/07/open-file-descriptor-passing-over-unix-domain-sockets/ 

或者查看:https://github.com/Rtoax/test/blob/master/ipc/socket/unsocket-sendmsg-iovec-memfd_create/readme.txt

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

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

那么,透过socket发送memfd_create() fd来进行进程间共享内存这种方法,它究竟惊艳在哪里?

我认为首要的惊艳之处在于编程模型的惊艳。API简单、灵活、通用。进程之间想共享几片内存共享几片内存,想怎么共享怎么共享,想共享给谁共享给谁,无非是多了几个fd和socket的传递过程。比如,我从互联网上面收到了jpeg的视频码流,一帧帧的画面,进程A可以创建多片buffer来缓存画面,然后就可以透过把每片buffer对应的fd,递交给另外的进程去解码等。Avenue to Jane(大道至简),简单的才是最好的!

memfd_create()的另外一个惊艳之处在于支持“封印”(sealing,就是你玩游戏的时候的封印),sealing这个单词本身的意思是封条,在这个场景下,我更愿意把它翻译为“封印”。中国传说中的封印,多是采用如五行、太极、八卦等手段,并可有例如符咒、法器等物品的辅助。现指对某个单位施加一种力量,使其无法正常使用某些能力的本领(常出现于玄幻及神魔类作品,游戏中)。我这一生,最喜欢玩的游戏就是《仙剑奇侠传》和《轩辕剑——天之痕》,不知道是否暴露年龄了。

采用memfd_create()的场景下,我们同样可以用某种法器,来控制共享内存的shrink、grow和write。最初的设想可以详见File Sealing & memfd_create()这篇文章:

https://lwn.net/Articles/591108/ 或者 File Sealing & memfd_create() | 文件密封和memfd_create()

我们如果在共享内存上施加了这样的封印,则可以限制对此片区域的ftruncate、write等动作,并建立某种意义上进程之间的相互信任,这是不是很拉风?

封印

  • * SEAL_SHRINK: If set, the inode size cannot be reduced
  • * SEAL_GROW: If set, the inode size cannot be increased
  • * SEAL_WRITE: If set, the file content cannot be modified

File Sealing & memfd_create()文中举到的一个典型使用场景是,如果graphics client把它与graphics compoistor共享的内存交给compoistor去render,compoistor必须保证可以拿到这片内存。这里面的风险是client可能透过ftruncate()把这个memory shrink小,这样compositor就拿不到完整的buffer,会造成crash。所以compositor只愿意接受含有SEAL_SHRINK封印的fd,如果没有,对不起,我们不能一起去西天取经。

在支持memfd_create()后,我们应尽可能地使用这种方式来替代传统的POSIX和SYS V,基本它也是一个趋势,比如我们在wayland相关项目中能看到这样的patch:https://patchwork.freedesktop.org/patch/244675/

 

dma_buf

这部分由于我的内核今天出了问题,一直没有试验,以后有时间机会会更新在别的文章里,这里直接贴出宋宝华老师文章的内容。侵删。


dma_buf定义

The DMABUF framework provides a generic method for sharing buffers between multiple devices. Device drivers that support DMABUF can export a DMA buffer to userspace as a file descriptor (known as the exporter role), import a DMA buffer from userspace using a file descriptor previously exported for a different or the same device (known as the importer role), or both.

DMABUF 框架提供了一种通用方法,用于在多个设备之间共享缓冲区。支持 DMABUF 的设备驱动程序可以将 DMA 缓冲区导出到用户空间作为文件描述符(称为导出器角色),使用以前为其他或同一设备导出的文件描述符(称为导入器角色)从用户空间导入 DMA 缓冲区(称为导入器角色),或两者同时导出。

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

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

为什么我们要共享DMA buffer?

想象一个场景:你要把你的屏幕framebuffer的内容透过gstreamer多媒体组件的服务,变成h264的视频码流,广播到网络上面,变成流媒体播放。在这个场景中,我们就想尽一切可能的避免内存拷贝。

技术上,管理framebuffer的驱动可以把这片buffer在底层实现为dma_buf,然后graphics compositor给这片buffer映射出来一个fd,之后透过socket发送fd 把这篇内存交给gstreamer相关的进程,如果gstreamer相关的“color space硬件转换”组件、“H264编码硬件组件”可以透过收到的fd还原出这些dma_buf的地址,则可以进行直接的加速操作了。比如color space透过接收到的fd1还原出framebuffer的地址,然后把转化的结果放到另外一片dma_buf,之后fd2对应这片YUV buffer被共享给h264编码器,h264编码器又透过fd2还原出YUV buffer的地址。

这里面的核心点就是fd只是充当了一个“句柄”,用户进程和设备驱动透过fd最终寻找到底层的dma_buf,实现buffer在进程和硬件加速组件之间的zero-copy,这里面唯一进行了exchange的就是fd。

再比如,如果把方向反过来,gstreamer从网络上收到了视频流,把它透过一系列动作转换为一片RGB的buffer,那么这片RGB的buffer最终还要在graphics compositor里面渲染到屏幕上,我们也需要透过dma_buf实现内存在video的decoder相关组件与GPU组件的共享。

Linux内核的V4L2驱动(encoder、decoder多采用此种驱动)、DRM(Direct Rendering Manager,framebuffer/GPU相关)等都支持dma_buf。比如在DRM之上,进程可以透过

int drmPrimeHandleToFD(int fd,
    uint32_t handle,
    uint32_t flags,
    int * prime_fd );

获得底层framebuffer对应的fd。如果这个fd被分享给gstreamer相关进程的video的color space转换,而color space转换硬件组件又被实现为一个V4L2驱动,则我们可以透过V4L2提供的如下接口,将这片buffer提供给V4L2驱动供其导入:

如果是multi plane的话,则需要导入多个fd:

相关细节可以参考这个文档:

https://linuxtv.org/downloads/v4l-dvb-apis/uapi/v4l/dmabuf.html

一切都是文件不是文件创造条件也要把它变成文件!这就是Linux的世界观。是不是文件不重要,关键是你得觉得它是个文件。在dma_buf的场景下,fd这个东西,纯粹就是个"句柄",方便大家通过这么一个fd能够对应到最终硬件需要访问的buffer。所以,透过fd的分享和传递,实际实现跨进程、跨设备(包括CPU)的内存共享。

如果说前面的SYS V、POSIX、memfd_create()更加强调内存在进程间的共享,那么dma_buf则更加强调内存在设备间的共享,它未必需要跨进程。比如:

有的童鞋说,为嘛在一个进程里面设备A和B共享内存还需要fd来倒腾一遍呢?我直接设备A驱动弄个全局变量存buffer的物理地址,设备B的驱动访问这个全局变量不就好了吗?我只能说,你对Linux内核的只提供机制不提供策略,以及软件工程每个模块各司其责,高内聚和低耦合的理解,还停留在裸奔的阶段。在没有dma_buf等类似机制的情况下,如果用户空间仍然负责构建策略并连接设备A和B,人们为了追求代码的干净,往往要进行这样的内存拷贝:

dma_buf的支持依赖于驱动层是否实现了相关的callbacks。比如在v4l2驱动中,v4l2驱动支持把dma_buf导出(前面讲了v4l2也支持dma_buf的导入,关键看数据方向),它的代码体现在:

drivers/media/common/videobuf2/videobuf2-dma-contig.c中的:

其中的vb2_dc_dmabuf_ops是一个struct dma_buf_ops,它含有多个成员函数:

当用户call VIDIOC_EXPBUF这个IOCTL的时候,可以把dma_buf转化为fd:

int ioctl(int fd, VIDIOC_EXPBUF, struct v4l2_exportbuffer *argp);

对应着驱动层的代码则会调用dma_buf_fd():

应用程序可以通过如下方式拿到底层的dma_buf的fd:

dma_buf的导入侧设备驱动,则会用到如下这些API:

dma_buf_attach()
dma_buf_map_attachment()
dma_buf_unmap_attachment()
dma_buf_detach()

下面这张表,是笔者对这几种共享内存方式总的归纳:

本文分享自微信公众号 - Linux阅码场(LinuxDev),作者:宋宝华

本人rtoax进行了代码的编写与整理工作,这里声明为原创,如有侵权请联系后更改为“转载”或删除。

原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。

原始发表时间:2019-12-09

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

 

已标记关键词 清除标记
©️2020 CSDN 皮肤主题: 酷酷鲨 设计师:CSDN官方博客 返回首页