---
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;
```
* 記憶體分布

* 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;
```
* 記憶體分布

* 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) 的共享

* 上圖中,行程 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)