--- title: coincainer_of 和 shared memory 方式 tags: C 語言筆記 --- ## coincainer_of 和 shared memory 方式 ### struct 記憶體分配 * ex1: ```c= struct Test01 { char c; short s; int i; double d; }t1; struct Test02 { char c; double d; int i; short s; }t2; ``` * 記憶體分布 ![](https://i.imgur.com/bF6js37.png) * t1 的 size = 16 byte * t2 的 size = 24 byte * ex2: ```c= struct Test03 { short s; double d; char c; int i; }t3; struct Test04 { double d; char c; int i; short s; }t4; ``` * 記憶體分布 ![](https://i.imgur.com/AKweIpa.png) * t3 的 size = 24 byte * t4 的 size = 24 byte [參考](https://iter01.com/673663.html) ### offsetof * 定義於 <stddef.h> * 可接受給定成員的型態及成員的名稱,傳回「成員的位址減去 struct 的起始位址」,意同將 struct 起始位置視為零 * ex: p 為 實際 struct 的成員 c 的位址,x 為 struct 起始位址 ``` p = (char *) &x + offsetof(typeof(struct), struct 成員名稱); ``` * 原始碼 ```c= #define offsetof(TYPE, MEMBER) ((size_t)&((TYPE *)0)->MEMBER) ``` * 原理是先將 TYPE 型態結構體的地址變更為 0,然後再加上成員 MEMBER 的偏移量 * ```size_t``` 代表系統上的數據單位,可以從 ```sizeof()``` 回傳的單位來理解,例如 ```sizeof(int)``` 會回傳 4 ==byte== > std::size_t is the unsigned integer type of the result of the sizeof operator ### container_of * 程式碼 ```c= #define container_of(ptr, type, member) \ __extension__({ \ const __typeof__(((type *) 0)->member) *__pmember = (ptr); \ (type *) ((char *) __pmember - offsetof(type, member)); \ }) ``` * ```__extension__```用於版本相容 * 先透過 ```__typeof__``` 得到 type 中的成員 ```member``` 的型別,並宣告一個指向該型別的指標 ```__pmember``` * 將 ```ptr``` 指派到 ```__pmember```(```__pmember``` 目前指向的是 ```member``` 的位址) * ```offsetof(type, member)``` 可得知 ```member``` 在 type 這個結構體位移量,即 offset * 將絕對位址 ```(char *) __pmember``` 減去 ```offsetof(type, member)``` ,可得到結構體的起始位址 * [為何要用 ```(char*)```](https://stackoverflow.com/questions/20421910/the-char-casting-in-container-of-macro-in-linux-kernel): 因為以 ```(float)a - (size_t)b``` 為例,在計算時 ```b``` 會被改成 ```(float)b```,造成結果的錯誤 * 最後再用 ```(type *)``` 將起始位置轉型為指向 type 的指標 ### 共享記憶體(shared memory) * 定義: 在多處理器的計算機系統中,可以被不同中央處理器(CPU)訪問的大容量記憶體。由於多個CPU需要快速訪問存儲器,這樣就要對存儲器進行暫存(Cache)。任何一個暫存的數據被更新後,由於其他處理器也可能要存取,共享記憶體就需要立即更新,否則不同的處理器可能用到不同的數據 * shared memory 的特點: * shared memory 是行程間共享數據的一種最快的方法。一個行程向共享的記憶體寫入了數據,共享這個記憶體區域的所有行程就可以立刻看到其中的內容 * 多個行程之間對一個共用記憶體的存取是互斥的 * 因為所有行程共享同一塊記憶體,所以在各種行程間通信方式中具有最高的效率。同時也避免了對數據的各種不必要的複製 * 共享內記憶體的方式 * SysV 共享記憶體 * 寫入端 ```c= #include <sys/shm.h> #include <unistd.h> #include <string.h> int main(int argc, char **argv) { key_t key = ftok("/dev/shm/myshm", 0); int shm_id = shmget(key, 0x400000, IPC_CREAT); char *p = (char *) shmat(shm_id, NULL, 0); memset(p, 'A', 0x400000); shmdt(p); return 0; } ``` * 讀取端 ```c= #include <sys/shm.h> #include <unistd.h> #include <stdio.h> int main(int argc, char **argv) { key_t key = ftok("/dev/shm/myshm", 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; } ``` [參考](https://mp.weixin.qq.com/s?__biz=Mzg2OTc0ODAzMw==&mid=2247502402&idx=1&sn=4e94df2bd934cf5b23f7839dc4be0e18&source=41#wechat_redirect) * POSIX 共享記憶體 * 用 POSIX 寫入共享記憶體 ```c= #include <sys/mman.h> #include <unistd.h> #include <fcntl.h> /* For O_* constants */ #include <string.h> int main(int argc, char **argv) { int fd = shm_open("posixsm", 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; } ``` * 用 POSIX 讀取共享記憶體 ```c= #include <sys/mman.h> #include <unistd.h> #include <fcntl.h> /* For O_* constants */ #include <stdlib.h> #include <stdio.h> int main(int argc, char **argv) { int fd = shm_open("posixsm", 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; } ``` * [mmap函數](https://blog.csdn.net/ababab12345/article/details/102931841) * memfd_create * memfd_create 函式特別之處,在於會回傳一個匿名記憶體檔案的 fd,該 fd 沒有對應到實體的檔案路徑 * 因為沒有對應到實體的檔案路徑,所以兩個行程要共享檔案時需要透過 fd 的傳遞,但若在兩個行程間單純傳 fd 這個值,則兩個行程會開到不同的記憶體空間,因為 fd 是個行程級別的機制 * 所以要用 cmsg ,透過 socket 把 fds 指向的 n 個 fd 發送給另一個行程 * send_fd.c ```c= #include <sys/socket.h> static void send_fd(int socket, int *fds, int n) { char buf[CMSG_SPACE(n * sizeof(int))], data; memset(buf, '\0', sizeof(buf)); struct iovec io = {.iov_base = &data, .iov_len = 1}; struct msghdr msg = {.msg_iov = &io, .msg_iovlen = 1, .msg_control = buf, .msg_controllen = sizeof(buf)}; /*用 cmsg 來實作msg內容(cmsg 指到 first cmsghdr in mag*/ struct cmsghdr *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) perror("Failed to send message"); } ``` * 接受這個 fd * recv_fd.c ```c= static int *recv_fd(int socket, int n) { int *fds = malloc(n * sizeof(int)); char buf[CMSG_SPACE(n * sizeof(int))], data; memset(buf, '\0', sizeof(buf)); struct iovec io = {.iov_base=&data, .iov_len=1}; struct msghdr msg = {.msg_iov = &io, .msg_iovlen = 1, .msg_control = buf, .msg_controllen = sizeof(buf)}; if (recvmsg(socket, &msg, 0) < 0) perror("Failed to receive message"); struct cmsghdr *cmsg = CMSG_FIRSTHDR(&msg); memcpy(fds, (int *) CMSG_DATA(cmsg), n * sizeof(int)); return fds; } ``` * 如果這個 fb 號碼再 process A 裡面是 100,則傳給process B 之後可以是任意的號碼如 200 等(前面的 199 個 fd 在 B 裡面有人用了),但最後都是只到同一個檔案 * memfd_create 基本用法(shm_open 改成 memfd_creat 而已) ```c= 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); ``` * 實際應用: 行程 A 透過 memfd_create 建立 兩個 4MB 的記憶體空間,並透過 socket(/tmp/fd-pass.socket) 來送 fds 給 行程B * 行程 A ```c= int main(int argc, char *argv[]) { int sfd = socket(AF_UNIX, SOCK_STREAM, 0); if (sfd == -1) perror("Failed to create socket"); struct sockaddr_un addr; 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 int fds[2]; fds[0] = memfd_create("shma", 0); if (fds[0] < 0) perror("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) perror("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) perror("Failed to connect to socket"); send_fd(sfd, fds, 2); exit(EXIT_SUCCESS); } ``` * 行程 B ```c= int main(int argc, char *argv[]) { char buffer[256]; int sfd = socket(AF_UNIX, SOCK_STREAM, 0); if (sfd == -1) perror("Failed to create socket"); if (unlink("/tmp/fd-pass.socket") == -1 && errno != ENOENT) perror("Removing socket file failed"); struct sockaddr_un addr; 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) perror("Failed to bind to socket"); if (listen(sfd, 5) == -1) perror("Failed to listen on socket"); int cfd = accept(sfd, NULL, NULL); if (cfd == -1) perror("failed to accept incoming connection"); int *fds = recv_fd(cfd, 2); for (int i = 0; i < 2; ++i) { fprintf(stdout, "Reading from passed fd %d\n", fds[i]); ssize_t nbytes; while ((nbytes = read(fds[i], buffer, sizeof(buffer))) > 0) write(1, buffer, nbytes); *buffer = '\0'; } if (close(cfd) == -1) perror("Failed to close client socket"); return 0; } ``` * memfd_create 還有 sealing 功能 (加上限制條件) 經典使用場景:如果 graphics client (如下圖中的 WebKit,後者是 Chrome 網頁瀏覽器的引擎) 將其與 graphics compositor 共享的主記憶體 (對應下圖的 “shared memory”) 交給 compositor 去 render (算繪,一如人類在藝術作品中的描繪,但改用電腦運算),compositor 必須保證可拿到這個記憶體區塊,然而隱藏著一個風險 —— client 可能透過 ftruncate 裁減記憶體空間,這樣 compositor 就拿不到完整的 buffer 而會造成程式操作的崩潰。為此,compositor 只願接受含有 SEAL_SHRINK 封印的 fd * dma_buf * 使用場景: 攝影裝置 A 取得的影像資料需要送到 GPU 進行運算和再處理。負責資料蒐集和編碼的模組是 Linux 上不同的裝置 B 的驅動程式。其中的資料由 A 送到 B 的過程就可以使用 dma_buf * 簡單地來說,dma_buf 可以實現 buffer 在多個裝置 (device) 的共享 ![](https://i.imgur.com/0y0Zmxi.png) * 上圖中,行程 A 存取裝置 A 並獲得其使用 buffer 的 fd,之後再透過 socket 把 fd 傳送給行程 B,行程 B 再將 fd 導入回裝置 B,獲得對裝置 A 中 buffer 的共享存取。如果 CPU 也需要在 user mode 存取這塊 buffer,則進行 mmap 的動作。 ### 應用案例: 雙向環狀鏈結串列 * 作業實作 * [雙向環狀鏈結串列](https://hackmd.io/@sysprog/linux-macro-containerof#%E6%87%89%E7%94%A8%E6%A1%88%E4%BE%8B-%E9%9B%99%E5%90%91%E7%92%B0%E7%8B%80%E9%8F%88%E7%B5%90%E4%B8%B2%E5%88%97) * [Linux鏈結串列struct list_head 研究](https://myao0730.blogspot.com/2016/12/linux.html)