- 作者: 雪山肥鱼
- 时间:20210615 13:36
- 目的:跨进程共享文件描述符
# socket 传递 fd
## 内存描述成文件
# memfd_create
## memfd 与封印 sealing
# 内存的跨设备共享
## 需求一
## 需求二
## dma_buf usage flow
socket 传送 fd
memfd_create 与 dma-buffer 都是建立于跨进程共享fd之上的。
每一个进程都是资源封装的单位,那么有一个指针会指向一个 fd_array, 标识进程都打开了哪些文件描述符。
以一个最简单的进程为例:
main () {
int i = 0;
while(1) {
printf("hello world i:%d\n", i++);
}
}
![](https://img.haomeiwen.com/i25953572/3704365399b91b86.png)
默认打开的fd 有 0 1 2
进程跑起来后,会有 pidof a.out。
进入到进程的pid目录:
cd /proc/2122/fd
会看到进程打开的所有fd情况。
文件描述成内存
在linux中,我们最熟悉的一句话莫过于,一切皆文件。all is file。
很多东西,在linux中,都是可以被文件抽象掉的。
![](https://img.haomeiwen.com/i25953572/0e978f2f8e592cfa.png)
把共享内存也描述成一个fd,那么fd指向一片内存。即用文件描述一片内存
![](https://img.haomeiwen.com/i25953572/c5a34e4754951958.png)
当然 fd 在p1 可能是100,共享到p2后,有可能会编程 120. 因为双方所打开的文件并不一致。这都不重要,重要的是 双方指向同一个fd即可。
这种fd的发送,不能简单的把这个fd作为消息发出去,而必须把fd作为socket中比较特殊的一种msg,即cmsg 发出去。当然这是 unix domain socket支持的。
/*sendfd.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)
{
struct msghdr msg = {0};
struct cmsghdr *cmsg;
char buf[CMSG_SPACE(n * sizeof(int))], data;
/*自定义数据*/
data = 'f';
memset(buf, '\0', sizeof(buf));
//构建数据向量,详细可以搜索 iovec 这个结构体
//可以自定义组织数据协议
//即用户level 的数据协议
struct iovec io = { .iov_base = &data, .iov_len = 1 };
msg.msg_iov = &io;
msg.msg_iovlen = 1; //数据的片数,几个vector,可以发几片buffer
msg.msg_control = buf;//控制 message 包括控制message的头部与,控制message的头部
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));
//CMSG_DATS 是控制msg的数据部分。发送2个fd
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);
while(1);
exit(EXIT_SUCCESS);
}
/*recvfd.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))], 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 receive message");
cmsg = CMSG_FIRSTHDR(&msg);
/*取出自定义数据*/
printf("data = %c\n", *(char*)((msg.msg_iov)->iov_base));
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;
}
运行:
gcc -o recvfd recvfd.c
gcc -o sendfd sendfd.c -std=c99
./recvfd &
./sendfd recvfd.c sendfd.c
![](https://img.haomeiwen.com/i25953572/65715b16ccce2340.png)
除了两个文件内容以外,还有自定义的数据 ‘f'
![](https://img.haomeiwen.com/i25953572/cbf8a917ed49b1a5.png)
数据msg和控制cmsg 的内容是相互独立的。
通过cmsg 发送的数据,可以包含两部分,数据msg 和控制msg.
![](https://img.haomeiwen.com/i25953572/e29337675ef91278.png)
工程中是很有必要的。
比如APP 有一个窗口的fd指向一片内存。这个APP需要把fd发送给 compositor,但是compositor并不知道这片内存是多大的,或者说app 可以通过cmsg告诉compositor 这个窗口时300*200的,并且在屏幕的哪个位置上。约定一个简单的协议即可。还是很方便的。
memfd_create
发送的不是文件,而是一个匿名fd。让fd指向一片内存。
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 sotrage.
与ramdisk是不同的概念
ramdisk 是将ram模拟成一个磁盘,将fd存放于此。
而memfd_create 并不需要存储在任何形式的磁盘中,就驻足于内存中。就是常规的内存,在常规内存里折腾。
可以用mmap、read、wirte去操作这片fd,因为从某种角度看,它是一种规则的文件。
这次发送的并非真实的fd,而是fd指向的内存,那么这就是一种共享内存的模式。当然memfd_create也并不局限于共享内存。只要你存在以下的需求:
- 需要一个文件
- 但这个文件并不需要在磁盘存在
- 又想像文件一样去操作这片内存。
- 都可以用memfd_create
![](https://img.haomeiwen.com/i25953572/87a770335a63d3d0.png)
P1 将fd指向的共享内存,发送给P2,那么两个进程就实现了共享内存。
memfd_create vs shm 共享内存
- memfd_create 更为灵活,可以配置多个,相当于有几个fd,就有几片共享内存。
- 并且可以配置每个fd的大小
- shm 相对死板
比如app 于 compositor的交互,app 可能有多个窗口于 compositor进行通讯。
代码示例:只修改sendfd.c即可
/*sendmemfd.c*/
#define _GNU_SOURCE /* See feature_test_macros(7) */
#include <sys/mman.h>
#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)
{
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->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;
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);
#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, PROT_READ | PROT_WRITE, MAP_SHARED, fds[0], 0);
memset(ptr0, 'A', SIZE);
munmap(ptr0, SIZE);
fds[1] = memfd_create("shmb", 0);
if (fds[1] < 0)
handle_error ("Failed 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, PROT_READ | PROT_WRITE, MAP_SHARED, fds[1], 0);
memset (ptr1, 'B', SIZE);
munmap(ptr1, SIZE);
if (connect(sfd, (struct sockaddr *) &addr, sizeof(struct sockaddr_un)) == -1)
handle_error ("Failed to connect to socket");
send_fd (sfd, fds, 2);
while(1);
exit(EXIT_SUCCESS);
}
与 传送fd进行对比:vimdiff
![](https://img.haomeiwen.com/i25953572/6482c4865e709402.png)
memfd 与 sealing
实际业务需求:p1 将 fd 发给 p2, 实现共享内存,p2 想在自己操作完这片共享内存的过程中,不希望p1去修改p2.
- 场景 1:比如p1 有 窗口,窗口绘制好了,交给p2, p2是compositor, p2 会用gpu把p1 进程 和 其他进程的窗口都叠加起来,在屏幕上显示,那么p2在叠加p1分享的窗口过程中,不希望p1 修改buffer,gpu又在渲染,你本身又在修改,则画出来的东东会有一种撕裂感。p2希望p1不再修改这片buffer。
- 场景2:p1 与 p2 共享了1M的内存,但是p1随后觉得512k就够了,所以truancate掉了512k,但是p2不知道,则p2在操作这篇共享内存的过程中,必定会出现crush。
所以p1 会调用
/*p1*/
fcntl(fd, F_ADD_SEAL, seals) //这个API ,不会再操作文件,比如增大,缩小和写。
/*p2*/
seals = fcntl(fd, F_GET_SEALS);
然后对seals进行匹配。确认发送者确实实行了封印,即seals,才会进行后续操作。否则可以直接reject掉共享内存。
这个过程就建立了彼此的信任。
代码示例
/*seals.c*/
#define _GNU_SOURCE /* See feature_test_macros(7) */
#include <sys/mman.h>
#include <fcntl.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <stdio.h>
#define errExit(msg) do { perror(msg); exit(EXIT_FAILURE); \
} while (0)
int
main(int argc, char *argv[])
{
int fd;
unsigned int seals;
char *addr;
char *name, *seals_arg;
ssize_t len;
if (argc < 3) {
fprintf(stderr, "%s name size [seals]\n", argv[0]);
fprintf(stderr, "\t'seals' can contain any of the "
"following characters:\n");
fprintf(stderr, "\t\tg - F_SEAL_GROW\n");
fprintf(stderr, "\t\ts - F_SEAL_SHRINK\n");
fprintf(stderr, "\t\tw - F_SEAL_WRITE\n");
fprintf(stderr, "\t\tS - F_SEAL_SEAL\n");
exit(EXIT_FAILURE);
}
name = argv[1];
len = atoi(argv[2]);
seals_arg = argv[3];
/* Create an anonymous file in tmpfs; allow seals to be
placed on the file */
fd = memfd_create(name, MFD_ALLOW_SEALING);
if (fd == -1)
errExit("memfd_create");
/* Size the file as specified on the command line */
if (ftruncate(fd, len) == -1)
errExit("truncate");
printf("PID: %ld; fd: %d; /proc/%ld/fd/%d\n",
(long) getpid(), fd, (long) getpid(), fd);
/* Code to map the file and populate the mapping with data
omitted */
/* If a 'seals' command-line argument was supplied, set some
seals on the file */
if (seals_arg != NULL) {
seals = 0;
if (strchr(seals_arg, 'g') != NULL)
seals |= F_SEAL_GROW;
if (strchr(seals_arg, 's') != NULL)
seals |= F_SEAL_SHRINK;
if (strchr(seals_arg, 'w') != NULL)
seals |= F_SEAL_WRITE;
if (strchr(seals_arg, 'S') != NULL)
seals |= F_SEAL_SEAL;
if (fcntl(fd, F_ADD_SEALS, seals) == -1)
errExit("fcntl");
}
/* Keep running, so that the file created by memfd_create()
continues to exist */
if (write(fd, "hello", 5) < 0)
perror("failed to write\n");
if (ftruncate(fd, len*2) < 0)
perror("failed to truncate grow\n");
if (ftruncate(fd, len/2) < 0)
perror("failed to truncate shrink\n");
pause();
exit(EXIT_SUCCESS);
}
dma_buffer 内存的跨设备共享
dma_buffer 导出成 一个文件后,就支持了dma_buffer 的 跨进程共享
需求一:
1.假设有一摄像头,驱动是v4l2 vedio的驱动。假设摄像头拍下的图像,存到一片1080p的内存中。图片格式假设为RGB。
- 我可能将图片叠加到屏幕上显示 --- GPU driver
-
我也可能将1080p的图像进行编码,编成h.264
需求.png
图像在v4l2 vedio里产生,但是却要共享给其他驱动。1080p的这片内存,也是通过dma的api从 cma 中申请到的。
在userspace 中 管理 内存共享的逻辑。linux只提供机制,策略是上层应用决定的。
解决方案一:
![](https://img.haomeiwen.com/i25953572/ee313726bc874b14.png)
上层APP,也就是用户空间,可以同时将摄像头和编码器管理起来。双方通过内存拷贝。
- 用户空间将摄像头的内存mmap到用户空间
- 编码器的输入buffer mmap 到用户空间
- cpu 做一遍 memcpy
- 效率非常低,内存拷贝即占内存贷款,也占CPU的利用率。
解决方案二:
dma_buffer 的方式是通过 匿名fd的方式指向一片内存
![](https://img.haomeiwen.com/i25953572/11f61af697a550ac.png)
相比方案一,减少了内存拷贝,且更为灵活,只要增加fd即可实现更多的内存共享。
从 stream I/O (DMA buffer importing) 摘录:
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 export role), import a DMA buffer from userspace using a file descriptor previously epxorted for a different or the same device(known as the importer role), or both. This section describes the DMABUF importer role API in V4L2.
需求二:
![](https://img.haomeiwen.com/i25953572/685afa99f085f2d9.png)
linux中的DRM驱动,管理显存,在DRM代码之上运行一个 compositor. 讲很多程序的应用界面叠加到屏幕上。
h.264 和色彩转换等都支持fd的导入。然后通过rtp实时视频流显示出去。
compositor与video pipeline 跨进程共享fd。只要化身为fd,就具有了天然共享的能力。即便不在同一个进程中,无非是把fd share一下而已。
dma_buf usage flow - 绕不开文件
![](https://img.haomeiwen.com/i25953572/f226ff230ce57c02.png)
网友评论