Nhu cầu của mảng không có phần tử là gì?


122

Trong mã nhân Linux, tôi tìm thấy điều sau đây mà tôi không thể hiểu được.

 struct bts_action {
         u16 type;
         u16 size;
         u8 data[0];
 } __attribute__ ((packed));

Mã ở đây: http://lxr.free-electrons.com/source/include/linux/ti_wilink_st.h

Nhu cầu và mục đích của một mảng dữ liệu không có phần tử nào?


Tôi không chắc chắn nếu có nên hoặc là một zero-length-mảng hoặc struct hack thẻ ...
hippietrail

@hippietrail, bởi vì thường khi ai đó hỏi cấu trúc này là gì, họ không biết rằng nó được gọi là "thành viên mảng linh hoạt". Nếu họ làm vậy, họ có thể dễ dàng tìm ra câu trả lời cho mình. Vì họ không có, họ không thể gắn thẻ câu hỏi như vậy. Đó là lý do tại sao chúng tôi không có một thẻ như vậy.
Shahbaz

10
Bỏ phiếu để mở lại. Tôi đồng ý rằng đây không phải là một bản sao, vì không có bài đăng nào khác đề cập đến sự kết hợp giữa "struct hack" không chuẩn với độ dài bằng 0 và thành viên mảng linh hoạt có tính năng C99 được xác định rõ ràng. Tôi cũng nghĩ rằng cộng đồng lập trình C luôn có lợi khi làm sáng tỏ bất kỳ đoạn mã khó hiểu nào từ nhân Linux. Chủ yếu là vì nhiều người có ấn tượng rằng nhân Linux là một loại mã C hiện đại, không rõ lý do. Trong khi trên thực tế, đó là một mớ hỗn độn khủng khiếp tràn ngập những khai thác phi tiêu chuẩn mà không bao giờ được coi là một quy luật C.
Lundin

5
Không trùng lặp - không phải là lần đầu tiên tôi thấy ai đó đóng câu hỏi một cách không cần thiết. Ngoài ra, tôi nghĩ câu hỏi này bổ sung vào cơ sở Kiến thức SO.
Aniket Inge

Câu trả lời:


139

Đây là một cách để có kích thước dữ liệu thay đổi mà không cần phải gọi malloc( kmalloctrong trường hợp này) hai lần. Bạn sẽ sử dụng nó như thế này:

struct bts_action *var = kmalloc(sizeof(*var) + extra, GFP_KERNEL);

Điều này từng không phải là tiêu chuẩn và được coi là một hack (như Aniket đã nói), nhưng nó đã được tiêu chuẩn hóa trong C99 . Định dạng tiêu chuẩn cho nó bây giờ là:

struct bts_action {
     u16 type;
     u16 size;
     u8 data[];
} __attribute__ ((packed)); /* Note: the __attribute__ is irrelevant here */

Lưu ý rằng bạn không đề cập đến bất kỳ kích thước nào cho datatrường. Cũng lưu ý rằng biến đặc biệt này chỉ có thể xuất hiện ở cuối cấu trúc.


Trong C99, vấn đề này được giải thích trong 6.7.2.1.16 (tôi nhấn mạnh):

Trong trường hợp đặc biệt, phần tử cuối cùng của cấu trúc có nhiều hơn một phần tử được đặt tên có thể có kiểu mảng không đầy đủ; đây được gọi là một thành viên mảng linh hoạt. Trong hầu hết các tình huống, thành viên mảng linh hoạt bị bỏ qua. Đặc biệt, kích thước của cấu trúc giống như thể thành viên mảng linh hoạt bị bỏ qua ngoại trừ việc nó có thể có nhiều dấu đệm hơn so với hàm ý bị bỏ sót. Tuy nhiên, khi a. Toán tử (hoặc ->) có toán hạng bên trái là (con trỏ tới) một cấu trúc có thành viên mảng linh hoạt và toán hạng bên phải đặt tên cho thành viên đó, nó hoạt động như thể thành viên đó được thay thế bằng mảng dài nhất (với cùng loại phần tử ) điều đó sẽ không làm cho cấu trúc lớn hơn đối tượng đang được truy cập; phần bù của mảng sẽ vẫn là phần bù của mảng linh hoạt, ngay cả khi phần bù này khác với phần bù của mảng thay thế. Nếu mảng này không có phần tử,

Hay nói cách khác, nếu bạn có:

struct something
{
    /* other variables */
    char data[];
}

struct something *var = malloc(sizeof(*var) + extra);

Bạn có thể truy cập var->datavới các chỉ mục trong [0, extra). Lưu ý rằng sizeof(struct something)sẽ chỉ tính toán kích thước cho các biến khác, tức là cung cấp datakích thước bằng 0.


Cũng có thể thú vị khi lưu ý cách tiêu chuẩn thực sự đưa ra các ví dụ về kết cấu mallocnhư vậy (6.7.2.1.17):

struct s { int n; double d[]; };

int m = /* some value */;
struct s *p = malloc(sizeof (struct s) + sizeof (double [m]));

Một lưu ý thú vị khác theo tiêu chuẩn ở cùng một vị trí là (tôi nhấn mạnh):

giả sử rằng lệnh gọi tới malloc thành công, đối tượng được trỏ tới bởi p sẽ hoạt động, cho hầu hết các mục đích, như thể p đã được khai báo là:

struct { int n; double d[m]; } *p;

(có những trường hợp mà sự tương đương này bị phá vỡ; đặc biệt, hiệu số của thành viên d có thể không giống nhau ).


Để rõ ràng, mã gốc trong câu hỏi vẫn không phải là mã chuẩn trong C99 (hoặc C11) và sẽ vẫn bị coi là một bản hack. Chuẩn hóa C99 phải bỏ qua giới hạn mảng.
MM

Là gì [0, extra)?
SS Anne


36

Trên thực tế, đây là một vụ hack đối với GCC ( C90 ).

Nó còn được gọi là hack cấu trúc .

Vì vậy, lần sau, tôi sẽ nói:

struct bts_action *bts = malloc(sizeof(struct bts_action) + sizeof(char)*100);

Nó sẽ tương đương với việc nói:

struct bts_action{
    u16 type;
    u16 size;
    u8 data[100];
};

Và tôi có thể tạo bất kỳ số lượng đối tượng cấu trúc nào như vậy.


7

Ý tưởng là cho phép một mảng có kích thước thay đổi ở cuối cấu trúc. Có lẽ, bts_actionlà một số gói dữ liệu có tiêu đề kích thước cố định (trường typesize) và datathành viên có kích thước thay đổi . Bằng cách khai báo nó là một mảng độ dài 0, nó có thể được lập chỉ mục giống như bất kỳ mảng nào khác. Sau đó, bạn sẽ phân bổ một bts_actioncấu trúc, có datakích thước ví dụ 1024 byte , như vậy:

size_t size = 1024;
struct bts_action* action = (struct bts_action*)malloc(sizeof(struct bts_action) + size);

Xem thêm: http://c2.com/cgi/wiki?StructHack


2
@Aniket: Tôi không hoàn toàn chắc chắn về ý tưởng đó từ khi nào .
sheu

trong C ++ có, trong C, không cần thiết.
amc

2
@sheu, nó xuất phát từ việc phong cách hành văn của mallocbạn khiến bạn phải lặp lại nhiều lần và nếu có actionthay đổi kiểu thì bạn phải sửa nhiều lần. So sánh hai cái sau cho chính bạn và bạn sẽ biết: struct some_thing *variable = (struct some_thing *)malloc(10 * sizeof(struct some_thing));so với cái struct some_thing *variable = malloc(10 * sizeof(*variable));thứ hai ngắn hơn, rõ ràng hơn và dễ thay đổi hơn.
Shahbaz

5

Mã không hợp lệ C ( xem phần này ). Nhân Linux, vì những lý do rõ ràng, không quan tâm một chút đến tính di động, vì vậy nó sử dụng nhiều mã không chuẩn.

Những gì họ đang làm là mở rộng không theo tiêu chuẩn GCC với kích thước mảng 0. Một chương trình tuân thủ tiêu chuẩn sẽ được viết u8 data[];và nó sẽ có ý nghĩa tương tự. Các tác giả của hạt nhân Linux dường như thích làm cho mọi thứ trở nên phức tạp và không cần tiêu chuẩn một cách không cần thiết, nếu một tùy chọn để làm điều đó tự lộ diện.

Trong các tiêu chuẩn C cũ hơn, kết thúc một cấu trúc bằng một mảng trống được gọi là "hack struct". Những người khác đã giải thích mục đích của nó trong các câu trả lời khác. Việc hack struct, trong tiêu chuẩn C90, là hành vi không xác định và có thể gây ra sự cố, chủ yếu là do trình biên dịch C miễn phí thêm bất kỳ số byte đệm nào vào cuối cấu trúc. Các byte đệm như vậy có thể va chạm với dữ liệu mà bạn đã cố gắng "hack" vào ở cuối cấu trúc.

GCC ngay từ đầu đã thực hiện một phần mở rộng không chuẩn để thay đổi điều này từ hành vi không xác định thành hành vi được xác định rõ. Tiêu chuẩn C99 sau đó đã điều chỉnh khái niệm này và bất kỳ chương trình C hiện đại nào do đó có thể sử dụng tính năng này mà không gặp rủi ro. Nó được gọi là thành viên mảng linh hoạt trong C99 / C11.


3
Tôi nghi ngờ rằng "nhân linux không quan tâm đến tính di động". Có lẽ ý bạn là tính di động cho các trình biên dịch khác? Đúng là nó khá quấn lấy các tính năng của gcc.
Shahbaz

3
Tuy nhiên, tôi nghĩ đoạn mã cụ thể này không phải là đoạn mã chính thống và có lẽ bị bỏ qua vì tác giả của nó không chú ý nhiều đến nó. Giấy phép cho biết nó về một số trình điều khiển công cụ texas, vì vậy không chắc các lập trình viên cốt lõi của hạt nhân đã chú ý đến nó. Tôi khá chắc chắn rằng các nhà phát triển nhân liên tục cập nhật mã cũ theo các tiêu chuẩn mới hoặc tối ưu hóa mới. Nó quá lớn để đảm bảo mọi thứ đều được cập nhật!
Shahbaz

1
@Shahbaz Với phần "hiển nhiên", ý tôi muốn nói là khả năng di chuyển sang các hệ thống hoạt động khác, điều này đương nhiên sẽ không có ý nghĩa gì. Nhưng dường như họ cũng không quan tâm đến khả năng di động đối với các trình biên dịch khác, họ đã sử dụng rất nhiều phần mở rộng GCC đến nỗi Linux có thể sẽ không bao giờ được chuyển sang trình biên dịch khác.
Lundin

3
@Shahbaz Đối với trường hợp của bất kỳ thứ gì có nhãn Texas Instruments, bản thân TI nổi tiếng với việc tạo ra mã C vô dụng, điên rồ, ngây thơ nhất từng thấy, trong ghi chú ứng dụng của họ cho các chip TI khác nhau. Nếu mã bắt nguồn từ TI, thì tất cả các cược liên quan đến cơ hội diễn giải điều gì đó hữu ích từ nó sẽ bị tắt.
Lundin

4
Đúng là linux và gcc không thể tách rời. Nhân Linux cũng khá khó hiểu (chủ yếu là vì dù sao thì một hệ điều hành cũng phức tạp). Tuy nhiên, quan điểm của tôi là không hay khi nói rằng "Các tác giả của hạt nhân Linux rõ ràng thích làm cho mọi thứ trở nên phức tạp và phi tiêu chuẩn một cách không cần thiết, nếu một tùy chọn để làm như vậy tự lộ diện" do thực hành mã hóa tồi của bên thứ ba .
Shahbaz

1

Một cách sử dụng khác của mảng độ dài bằng không là như một nhãn được đặt tên bên trong một cấu trúc để hỗ trợ kiểm tra độ lệch cấu trúc thời gian biên dịch.

Giả sử bạn có một số định nghĩa cấu trúc lớn (kéo dài nhiều dòng trong bộ nhớ cache) mà bạn muốn đảm bảo rằng chúng được căn chỉnh với ranh giới dòng bộ nhớ cache cả ở phần đầu và phần giữa nơi nó vượt qua ranh giới.

struct example_large_s
{
    u32 first; // align to CL
    u32 data;
    ....
    u64 *second;  // align to second CL after the first one
    ....
};

Trong mã, bạn có thể khai báo chúng bằng các phần mở rộng GCC như:

__attribute__((aligned(CACHE_LINE_BYTES)))

Nhưng bạn vẫn muốn đảm bảo điều này được thực thi trong thời gian chạy.

ASSERT (offsetof (example_large_s, first) == 0);
ASSERT (offsetof (example_large_s, second) == CACHE_LINE_BYTES);

Điều này sẽ hoạt động cho một cấu trúc duy nhất, nhưng sẽ khó bao hàm nhiều cấu trúc, mỗi cấu trúc có tên thành viên khác nhau để căn chỉnh. Rất có thể bạn sẽ nhận được mã như bên dưới, nơi bạn phải tìm tên của thành viên đầu tiên của mỗi cấu trúc:

assert (offsetof (one_struct,     <name_of_first_member>) == 0);
assert (offsetof (one_struct,     <name_of_second_member>) == CACHE_LINE_BYTES);
assert (offsetof (another_struct, <name_of_first_member>) == 0);
assert (offsetof (another_struct, <name_of_second_member>) == CACHE_LINE_BYTES);

Thay vì đi theo cách này, bạn có thể khai báo một mảng có độ dài bằng 0 trong cấu trúc hoạt động như một nhãn được đặt tên với tên nhất quán nhưng không sử dụng bất kỳ khoảng trống nào.

#define CACHE_LINE_ALIGN_MARK(mark) u8 mark[0] __attribute__((aligned(CACHE_LINE_BYTES)))
struct example_large_s
{
    CACHE_LINE_ALIGN_MARK (cacheline0);
    u32 first; // align to CL
    u32 data;
    ....
    CACHE_LINE_ALIGN_MARK (cacheline1);
    u64 *second;  // align to second CL after the first one
    ....
};

Sau đó, mã xác nhận thời gian chạy sẽ dễ duy trì hơn nhiều:

assert (offsetof (one_struct,     cacheline0) == 0);
assert (offsetof (one_struct,     cacheline1) == CACHE_LINE_BYTES);
assert (offsetof (another_struct, cacheline0) == 0);
assert (offsetof (another_struct, cacheline1) == CACHE_LINE_BYTES);

Ý tưởng thú vị. Chỉ cần lưu ý rằng tiêu chuẩn không cho phép các mảng có độ dài 0, vì vậy đây là một thứ dành riêng cho trình biên dịch. Ngoài ra, có thể là một ý tưởng hay khi trích dẫn định nghĩa của gcc về hành vi của mảng có độ dài 0 trong một định nghĩa struct, ít nhất là để cho thấy liệu nó có thể giới thiệu padding trước hoặc sau khai báo hay không.
Shahbaz
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.