0

Triển khai container_of trong C

Mở đầu

container_of là một macro được dùng trong linux kernel.

Để có thể hiểu về contaier_of, trước hết cần phải hiểu offsetof() trong thư viện stddef.h

Thế offsetof(): dùng để làm gì ?

  • Macro này sẽ trả về 1 giá trị kiểu size_t.
  • Giá trị này là khoảng cách các byte của member trong struct.

Hãy xem ví dụ sau đây để có thể hiểu rõ hơn về cách sử dụng offsetof()

#include <stdio.h>
#include <stdint.h>
#include <stddef.h>

struct Register{
  uint8_t setup[2];
  uint8_t input;
  uint8_t output[3];
};

int main()
{
  printf("setup offset: %d\n", offsetof(struct Register, setup));
  printf("input offset: %d\n", offsetof(struct Register, input));
  printf("output offset: %d\n", offsetof(struct Register, output));
  printf("Size cua struct Register: %d\n", sizeof(struct Register));
}

Output

setup offset: 0
input offset: 2
output offset: 3
Size cua struct Register: 6

Như có thể thấy kết quả ở đây, thì

  • setup nằm ở vị trí đầu tiên, nên offset so với struct Register sẽ là 0.
  • input nằm ở vị trí thứ 2, và offset so với struct Register là 2. Câu hỏi: "Tại sao lại là 2 mà không phải là giá trị nào khác ?"
    • Bởi vì 2 byte đầu tiên do, setup đã chiếm trong struct Register nên input nằm ở vị trí byte thứ 3.
  • Tương tự, outputoffset = 3 , chiếm vị trí bắt đầu ở byte 4.

image.png

Tại vì sao, tôi lại tìm hiểu vấn đề này ?

Trong quá trình phát triển 1 os theo kiến trúc event-driven, os này dành cho dự án IoT cá nhân của tôi. Có 1 vấn đề xảy ra, cùng không hẳng là vấn đề hihi.

Mô tả như sau:

Tôi có 1 list gọi là Ready list - chứa những task sẵn sàng chạy

Ở đây tôi dùng Double linked list để lưu trữ những task ready này.
struct Dlist
{
    DNode first;
    DNode last;
    size; 
}

struct DNode
{
    DNode next;
    DNode prev;
}

struct Task
{
    DNode Node_task; // đại diện task trong ready list
    int data;
    ....
}

Đây là một đoạn mã đơn giãn để tôi có thể lưu trữ các task này vào DList, nhưng 1 vấn đề là khi mà ta lưu trữ task dưới dạng node trong list thì list đó sẽ không nhận diện được task nào đang nắm giữ node đó, mặc dù node có ở trong list.

  • Lúc này ta cần phải tạo 1 con trỏ để có thể trỏ tới task đang nắm giữa node này

image.png

Lúc này, cần phải chỉnh sửa struct DNode một tý

struct DNode
{
    struct Task* Qwner;
    DNode next;
    DNode prev;
}

Lúc này thì con trỏ Qwner sẽ trỏ tới task dang nắm giữ node này. OK => Thế là vấn đề trên đã được giải quyết.

Nhưng chưa dùng lại ở đó, tôi đã tìm hiểu được 1 pp hay hơn, không cần phải thêm con trỏ vào struct như trên => Tiết kiệm được 1 phần memory.

Đó chỉnh là sử dụng container_of

Khái niệm: dùng địa chỉ của member trong struct để có thể tính ra được địa chỉ của struct đang nắm giữ member này

Theo ví dụ trên: ta sẽ dựa vào dịa chỉ của DNode chứa trong struct Task, để có thể tìm ra dược địa chỉ của Task nắm giữa node này.

Triển khai container_of

#define container_of(ptr_member, type, member) \
    ((type *)((char *)(ptr_member) - offsetof(type, member)))

Trong đó:

  • ptr_member: con trỏ tới member trong struct
  • type: kiểu struct cha
  • member" tên của member
  • offsetof: độ lệch của member so với struct

Để có thể hiểu các triển khai trên thì cần phải hiểu cách mà có thể truy cập member của 1 struct thông qua địa chỉ

struct Register{
  uint8_t setup[2];
  uint8_t input;
  uint8_t output[3];
};
  • Địa chỉ setup = địa chỉ của struct + offsetof(setup) => setup = địa chỉ của struct + 0
  • Địa chỉ input = địa chỉ của struct + offsetof(input) => input = địa chỉ của struct + 2
  • Địa chỉ output = địa chỉ của struct + offsetof(output) => output = địa chỉ của struct + 3

Qua 3 cách tính, kết luận được: Địa chỉ member = Địa chỉ struct + offsetof(member) = > Địa chỉ struct = Địa chỉ member - offsetof(member)

Nhưng hãy chú ý, nếu theo công thức trên thì macro phải như thế này

#define container_of(ptr_member, type, member) \
    ((type *)((ptr_member) - offsetof(type, member)))

Vậy mục đích char * ở đây để làm gì ?

#include <stdio.h>
#include <stdint.h>
#include <stddef.h>

int main()
{
  int arr[10];
  int* p = arr;
  
  // giả sử muôn lùi lại 1 byte
  int* q = p - 1;
  
  printf("Địa chỉ p đang trỏ tới: %p\n", p);
  printf("Địa chỉ q đang trỏ tới: %p\n", q);
}
// OUTPUT
Địa chỉ p đang trỏ tới: 0x7ffea0128ec0
Địa chỉ q đang trỏ tới: 0x7ffea0128ebc

=> 0x7f...c0 - 0x7f...bc = 4 byte

Điều này không mong đợi, do tôi muốn lùi 1 byte địa chỉ thôi

Cần phải ép con trỏ về kiểu dữ liệu nào mà có 1 byte như char, uint8_t, ... Nếu không thì nó sẽ dịch chuyển bằng đúng với kích thước của kiểu dữ liệu của con trỏ trong hệ thống.


  char* q = (char *)p - 1;

Output

#include <stdio.h>
#include <stdint.h>
#include <stddef.h>

struct Register{
  uint8_t setup[2];
  uint8_t input;
  uint8_t output[3];
};

int main()
{
  int arr[10];
  int* p = arr;
  
  // giả sử muôn lùi lại 1 byte
  char* q = (char*)p - 1;
  
  printf("Địa chỉ p đang trỏ tới: %p\n", p);
  printf("Địa chỉ q đang trỏ tới: %p\n", q);
 
 // OUTPUT
Địa chỉ p đang trỏ tới: 0x7ffe0350bce0
Địa chỉ q đang trỏ tới: 0x7ffe0350bcdf

-> 0x7f...e0 - 0x7f...df = 1 byte
}

Example

Tìm địa chỉ của struct json thông qua id.

#include <stdio.h>
#include <stddef.h>

#define container_of(ptr_member, type, member) \
    ((type *)((char *)(ptr_member) - offsetof(type, member)))

struct json{
  int data;
  int id;
  char name[5];
};

int main()
{
  struct json JSON;
  
  struct json *p;
  
  p = container_of(&JSON.id, struct json, id);
  
  printf("địa chỉ JSON - p: %p\n", p);
  printf("địa chỉ struct: %p\n", &JSON);
}

OUTPUT

địa chỉ JSON - p: 0x7ffe6d3f5fb0
địa chỉ struct:   0x7ffe6d3f5fb0

All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí