У чому потреба масиву з нульовими елементами?


122

У коді ядра Linux я знайшов таке, чого я не можу зрозуміти.

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

Код тут: http://lxr.free-electrons.com/source/include/linux/ti_wilink_st.h

Яка потреба та призначення масиву даних із нульовими елементами?


Я не впевнений, чи повинні бути масиви нульової довжини або теги
stru

@hippietrail, тому що часто, коли хтось запитує, що це за структура, він не знає, що це називається "гнучким членом масиву". Якби вони це зробили, вони могли б легко знайти свою відповідь. Оскільки вони цього не роблять, вони не можуть позначати це питання як таке. Тому у нас немає такої мітки.
Шахбаз

10
Проголосуйте за повторне відкриття. Я погоджуюся, що це не був дублікат, тому що жодна з інших публікацій не стосується комбінації нестандартного "хакерства" з нульовою довжиною та чітко визначеного C99 з гнучким елементом масиву. Я також думаю, що спільноті програмування C завжди вигідно пролити трохи світла на будь-який незрозумілий код з ядра Linux. Головним чином, оскільки у багатьох людей складається враження, що ядро ​​Linux - це якийсь сучасний код C з незрозумілих причин. Хоча насправді це жахливий безлад, залитий нестандартними подвигами, які ніколи не слід розглядати як деякий канон С.
Лундін

5
Не дублікат - це не перший раз, коли я бачив, щоб хтось непотрібно закривав питання. Крім того, я думаю, що це питання додає до бази знань SO.
Aniket Inge

Відповіді:


139

Це спосіб мати змінний розмір даних, не потребуючи виклику malloc( kmallocв даному випадку) двічі. Ви б використовували це так:

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

Це раніше не було стандартним і вважалося хакером (як сказав Анікет), але це було стандартизовано в C99 . Стандартний формат для нього зараз:

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

Зауважте, що ви не згадуєте жодного розміру для dataполя. Зауважте також, що ця спеціальна змінна може мати місце лише в кінці структури.


В C99 це питання пояснено в 6.7.2.1.16 (акцент міна):

Як особливий випадок, останній елемент структури з більш ніж одним названим членом може мати неповний тип масиву; це називається гнучким членом масиву. У більшості ситуацій гнучкий член масиву ігнорується. Зокрема, розмір структури виглядає так, як якщо б гнучкий елемент масиву був опущений, за винятком того, що він може мати більше задніх прокладок, ніж це може означати пропуск. Однак, коли a. (або ->) оператор має лівий операнд, який є (вказівник на) структуру з гнучким членом масиву, а правий операнд називає цей член, він поводиться так, ніби цей член був замінений найдовшим масивом (з тим самим типом елемента ) що не зробить структуру більшою, ніж об'єкт, до якого звертаються; зсув масиву повинен залишатися таким у гнучкого елемента масиву, навіть якщо це буде відрізнятися від замінного масиву. Якщо цей масив не матиме елементів,

Або іншими словами, якщо у вас є:

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

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

Ви можете отримати доступ var->dataз індексами в [0, extra). Зауважте, що sizeof(struct something)буде дано облік розміру лише для інших змінних, тобто дасть dataрозмір 0.


Цікаво також зазначити, як стандарт насправді наводить приклади mallocтакої конструкції (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]));

Ще одна цікава примітка стандарту в тому самому місці: (наголос мій):

припускаючи, що виклик malloc вдається, об'єкт, на який вказує p, веде себе в більшості цілей так, ніби p було оголошено як:

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

(є обставини, коли ця еквівалентність порушена; зокрема, компенсації члена d можуть бути не однаковими ).


Щоб було зрозуміло, оригінальний код у цьому питанні все ще не є стандартним у C99 (ні C11), і все ще вважатиметься злому. Стандартизація C99 повинна опускати пов'язаний масив.
ММ

Що [0, extra)?
СС Енн


36

Насправді це злом, фактично для GCC ( C90 ).

Це також називається Struck hack .

Тож наступного разу я б сказав:

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

Це буде рівнозначно тому, що сказати:

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

І я можу створити будь-яку кількість таких структурних об'єктів.


7

Ідея полягає у тому, щоб дозволити масив змінного розміру в кінці структури. Імовірно, bts_actionце деякий пакет даних із заголовком фіксованого розміру ( typeта sizeполя) та dataчленом змінного розміру . Оголосивши його масивом 0 довжини, його можна індексувати так само, як і будь-який інший масив. Потім ви виділите bts_actionструктуру, наприклад 1024-байтного dataрозміру, наприклад:

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

Дивіться також: http://c2.com/cgi/wiki?StructHack


2
@Aniket: Я не зовсім впевнений, звідки походить ця ідея.
sheu

в C ++ так, в C, не потрібен.
amc

2
@sheu, це випливає з того, що ваш стиль письма mallocзмушує вас повторювати кілька разів, і якщо коли-небудь тип actionзмін, вам доведеться це виправити кілька разів. Порівняйте наступні два для себе, і ви дізнаєтесь: struct some_thing *variable = (struct some_thing *)malloc(10 * sizeof(struct some_thing));vs. struct some_thing *variable = malloc(10 * sizeof(*variable));Другий коротший, чистіший і явно легше змінити.
Шахбаз

5

Код недійсний C ( див. Це ). Ядро Linux, з очевидних причин, не пов'язане з портативністю, тому використовує безліч нестандартних кодів.

Те, що вони роблять, - це нестандартне розширення GCC з розміром масиву 0. Написала б стандартна сумісна програма, u8 data[];і це означало б саме те саме. Автори ядра Linux, мабуть, люблять робити речі непотрібними і нестандартними, якщо варіант для цього виявляється.

У старих стандартах С закінчення структури з порожнім масивом було відоме як "хак структура". Інші вже пояснювали її призначення в інших відповідях. Злама структури, в стандарті C90, була не визначеною поведінкою і могла спричинити збої, головним чином, оскільки компілятор C може вільно додавати будь-яку кількість байтів для підкладки в кінці структури. Такі байти заміщення можуть стикатися з даними, які ви намагалися "зламати" в кінці структури.

GCC на початку зробив нестандартне розширення, щоб змінити це від невизначеного до чітко визначеної поведінки. Тоді стандарт C99 адаптував цю концепцію, і будь-яка сучасна програма C може використовувати цю функцію без ризику. Він відомий як гнучкий член масиву в C99 / C11.


3
Сумніваюся, що "ядро Linux не переймається портативністю". Можливо, ви мали на увазі переносимість інших компіляторів? Це правда, що він досить переплітається з особливостями gcc.
Шахбаз

3
Тим не менш, я думаю, що цей конкретний фрагмент коду не є основним кодом, і, ймовірно, його не вистачає, оскільки його автор не приділяв йому великої уваги. Ліцензія говорить про деякі драйвери інструментів texas, тому навряд чи основні програмісти ядра не звернули на це ніякої уваги. Я впевнений, що розробники ядра постійно оновлюють старий код відповідно до нових стандартів або нових оптимізацій. Він просто занадто великий, щоб переконатися, що все оновлено!
Шахбаз

1
@Shahbaz Під "очевидною" частиною я мав на увазі переносимість інших операційних систем, що, природно, не мало б сенсу. Але вони, схоже, і не приносять прокляття щодо портативності для інших компіляторів, вони використали стільки розширень GCC, що Linux, ймовірно, ніколи не перенесеться до іншого компілятора.
Лундін

3
@Shahbaz Що стосується випадків, що мають маркування Texas Instruments, самі TI відомі тим, що виробляють найкорисніший, хитрий, наївний код C, який коли-небудь бачив, у своїх додатках до додатків для різних мікросхем TI. Якщо код походить від TI, то всі ставки щодо шансу інтерпретувати щось корисне з нього виключаються.
Лундін

4
Це правда, що linux та gcc нероздільні. Ядро Linux також досить важко зрозуміти (в основному тому, що операційна система все одно складна). Я хотів би сказати, що не приємно сказати: "Автори ядра Linux, мабуть, люблять робити речі непотрібними і нестандартними, якщо такий варіант виявляється" через погану практику кодування сторонніх виробників. .
Шахбаз

1

Інше використання масиву нульової довжини є назначеною міткою всередині структури для сприяння компіляції перевірки зміщення структури структури.

Припустимо, у вас є кілька великих структурних визначень (охоплює кілька ліній кеша), які ви хочете переконатися, що вони вирівняні до межі лінії кешу як на початку, так і в середині, де вона перетинає межу.

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

У коді ви можете оголосити їх за допомогою розширень GCC типу:

__attribute__((aligned(CACHE_LINE_BYTES)))

Але ви все ще хочете переконатися, що це виконується під час виконання.

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

Це працювало б для однієї структури, але важко було б охопити багато структур, кожна з яких має інше ім'я члена, яке слід вирівняти. Ви, швидше за все, отримаєте код, як нижче, де ви повинні знайти імена першого члена кожної структури:

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);

Замість того, щоб піти цим шляхом, ви можете оголосити масив нульової довжини в структурі, що виступає як названа мітка з послідовною назвою, але не займає жодного місця.

#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
    ....
};

Тоді код твердження для виконання буде набагато простішим у підтримці:

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);

Цікава ідея. Лише зауважте, що масиви 0 довжини стандартом не дозволені, тому це специфічна для компілятора річ. Крім того, може бути хорошою ідеєю процитувати визначення gcc щодо поведінки масивів 0 довжини в структурному визначенні, щонайменше, щоб показати, чи може вона вводити padding до або після декларування.
Шахбаз
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.