LINUX.ORG.RU

Дискуссия об использовании языка C++ для разработки ядра Linux

 ,


1

5

В списке рассылки разработчиков ядра Linux возобновилось начатое шесть лет назад обсуждение перспектив использования современного кода на C++ в ядре Linux, помимо нынешнего применения языка Си с ассемблерными вставками и продвижения языка Rust. Изначально тема разработки ядра на C++ была поднята в 2018 году инженером из Red Hat, который первого апреля в качестве шутки опубликовал набор из 45 патчей для использования шаблонов, наследуемых классов и перегрузки функций C++ в коде ядра.

С инициативой продолжения обсуждения выступил Ганс Питер Анвин (Hans Peter Anvin), один из ключевых разработчиков ядра в компании Intel и создатель таких проектов как syslinux, klibc и LANANA, разработавший для ядра Linux систему автомонтирования, реализацию RAID 6, драйвер CPUID и x32 ABI. По мнению Анвина, который является автором многочисленных макросов и ассемблерных вставок в ядре, с 1999 года языки C и C++ значительно продвинулись вперёд в своём развитии и язык C++ стал лучше, чем С, подходить для разработки ядра операционных систем.

Возможности, для которых ещё недавно приходилось привлекать специфичные GCC-расширения, теперь легко реализовать на стандартном C++, и во многих случаях использование C++ позволит улучшить инфраструктуру без глобального изменения кода. В качестве минимальной упоминается использование спецификации C++14, которая включает необходимые средства метапрограммирования, а в качестве желаемой - использование спецификации C++20, в которой появилась поддержка концепций, способных исключить появление многих ошибок.

Анвин считает, что C++ более предпочтителен, чем Rust, так как последний существенно отличается от языка С по синтаксису, непривычен для текущих разработчиков ядра и не позволяет постепенно переписывать код (в случае языка С++ можно по частям переводить код с языка C, так как С-код можно компилировать как C++). В поддержку использования С++ в ядре также выступили Иржи Слаби (Jiri Slaby) из компании SUSE и Дэвид Хауэллс (David Howells) из Red Hat.

>>> Подробности

★★

Проверено: hobbit ()
Последнее исправление: hobbit (всего исправлений: 1)

Ответ на: комментарий от AntonI

Я обожаю когда auto – возвращаемый тип. Тебе нужно взять, и прочитать что же делает функция, вместо того, чтобы увидеть очевидный bool.

Особенно для сложных контейнеров, да-да.

А на что это влияет: контейнер это или нет? На C++98 можно написать любой контейнер. И даже на C++pre98 (например на Turbo C++) тоже можно написать любой контейнер.

И да, если немного подумать, то многие решения можно сделать логичнее и красивее. (А учитывая насколько безграмотно сделан STL, его то уж точно можно переплюнуть по качеству и даже в C++98).

zx_gamer ★★
() автор топика
Ответ на: комментарий от sabacs

Что значит на «нескольких несовместимых диалектах Си»? Есть стандарты ANSI C, C99, C11, C17, и C23. Что мешает в новый стандат внести недостающие плюшки и использовать его при написании ядра?

sadko4u ★★
()
Ответ на: комментарий от sadko4u

Платформозависимость. Невозможно стандартизировать то, что не везде есть или не везде одинаково работает.

zx_gamer ★★
() автор топика
Ответ на: комментарий от zx_gamer

Какая платформозависимость? Что вы мне тут придумываете. У нас signed shift right по-разному на разных архитектурах работает и отнесён к UB, а вы мне тут про какую-то платформозависимость лапшу на уши вешаете.

Чем внедрение defer, например, поломает платформозависимость? Ничем, т.к. это тупо синтаксический сахар, чтобы избавить людей от написания типичного goto-кода для освобождения всех захваченных в функции ресурсов.

sadko4u ★★
()
Ответ на: комментарий от sadko4u

defer в новом стандарте Си.

Как думаешь, когда эту фичу смогут разрешить к повсеместному использованию в ядре и почему через 10 лет?

Ivan_qrt ★★★★★
()

Какая разница, от нас-то все равно неприятно получать патчи.

special-k ★★★
()
Ответ на: комментарий от kvpfs_2

Обработка исключения предполагает радикальные меры - убить/перезапустить/отстегнуть кусок приложухи вместе с данными, которые теперь в хз каком состоянии

Трабла в том, что ради вот таких редчайших случаев, которых может и не случиться, в бинаре компилится куча всякого дерьма, вместе с полезным кодом, и эта куча там лежит мертвым грузом, и кеш инструкций жрет.

slew
()
Ответ на: комментарий от anonmyous

Почему? В случае basic safety, все данные тоже в порядке, и все инварианты выполняются.

Ну вот давайте на примере:

struct Linkable {
   //...private data
   set_link(Linkable_iface &iface);
   // there is no unlink
};
Linkable a, b;
void do() {
   a.set_link(b);
   b.set_link(a); // #1
}

на #1 кидается исключение, операция не закончена. У а линк создан у b нет, пусть каждый из них и остался в валидном состоянии, но в целом программа будет вести себя некорректно.

Другой пример:

struct A {
   //...private data
   process_input_data(vector<char> &src);
}
A a, b;
void fn() {
   vector<char> input;
   //... fill input somehow
   a.process_input_data(input);
   b.process_input_data(input); #1
}

Аналогино - на #1 летит исключение, a учел новый данный, b - нет. Объекты в программе находятся в рассогласованном состоянии. Всё, это не может дальше корректно работать с предсказуемым результатом.

Тут вообще не понял. Гарантии обеспечивает кидающая, а не проверяющая сторона.

Ну а как, вам придется оборачивать всё в try catch любой потенциально кидающий вызов и пытаться вернуть состояние обратно в случае исключения. Вместо if (return …) -> catch (…)

PS: и это ведь игрушечный пример, в реальности это может быть портянкой в сотню строк, и что там кого дергает и как это вернуть в какое-то согласованное состояние в случае исключения где-нибудь в середине портянки - задача со зведздочкой.

kvpfs_2
()
Последнее исправление: kvpfs_2 (всего исправлений: 2)
Ответ на: комментарий от kvpfs_2

Ну а как, вам придется оборачивать всё в try catch любой потенциально кидающий вызов и пытаться вернуть состояние обратно в случае исключения. Вместо if (return …) -> catch (…)

Стандартная практика здесь это сначала вносить изменения во временные копии, а потом делать commit через noexcept swap/move.

Собственно в твоих примерах требуется согласованность данных в a и b, при этом это никак не отражено в коде, в этом первоочередная проблема. Именно поэтому тебе недостаточно basic, т.к. у тебя в логике требование к согласованности есть, а в коде нет.

Если переменные требуют нахождения в согласованном друг с другом состоянии, во-первых сделай их инвариантами класса. А дальше вносишь изменения в копию a, потом в b (или в копию b), потом свопаешь a.

Что характерно, никаких try/catch.

Если вдруг объекты настолько жирные или сложные, что их нельзя копировать, то тогда по ситуации. Но предпочтительный вариант выделять подобъекты, состояние которых меняется, заполнять их копии и дальше свопать уже только их.

Ivan_qrt ★★★★★
()
Ответ на: комментарий от Ivan_qrt

И твой вопрос: «А что, в случае кодов возврата гарантии целостности не нужны?»

Коды возврата известны, ветви выполнения предсказуемы. В случае же «продвинутого» спиписта, который кидает исключения потому что вторник, ход выполнения полностью теряет хоть какую-то предсказуемость, как сказал выше - нужно тупо оборачивать любой вызов в try - catch. Если же исключения используются правильно, лишь для критических ошибок, то я не пытаюсь играться в какие-то там гарантиии, просто знаю на верхнем уровне, что модуль нужно грохнуть.

Стандартная практика здесь это сначала вносить изменения во временные копии, а потом делать commit через noexcept swap/move.

В общем-то я это предлагал в качестве: «ну не будем же мы так делать, правда?». В общем вы сами себе выдумываете проблемы на ровном месте забивая гвозди исключениями.

kvpfs_2
()
Последнее исправление: kvpfs_2 (всего исправлений: 3)
Ответ на: комментарий от kvpfs_2

Я понимаю, что мы друг друга не понимаем и вряд ли договоримся.

Но всё-таки

Если же исключения используются правильно, лишь для критических ошибок, то я не пытаюсь играться в какие-то там гарантиии

Если process_input_data() в #1 возвращает не нулевой код возврата, то что ты будешь делать, чтобы не потерять консистентное состояние? Это чем-то отличается от того, что оно бросает исключение?

Ivan_qrt ★★★★★
()
Ответ на: комментарий от Ivan_qrt

Если process_input_data() в #1 возвращает не нулевой код возврата, то что ты будешь делать, чтобы не потерять консистентное состояние? Это чем-то отличается от того, что оно бросает исключение?

Кину исключение, которое как раз говорит, что караул, все пропало.

kvpfs_2
()
Ответ на: комментарий от kvpfs_2

Лол. Ну и в чём тогда вообще проблема? Всё уже сделано продвинутым «спипистом» за тебя, на что жаловаться-то?

Ivan_qrt ★★★★★
()
Ответ на: комментарий от Ivan_qrt

Ты не понял, наверное. Я кидаю исключения, в случае невосстановимых ошибок. Если в моем коде приходит исключение из process_input_data(), то пусть летит на верхний уровень без перехвата. Если возвращается ошибка (обработка данных не удалась), а я знаю, что обработать нужно в двух местах, иначе теряется консистентность, то это повод кинуть исключение. Но я мог и не кидать, если бы обработать надо было бы в одном лишь месте и консистентность не была бы потеряна.

В случае же васянки спиписта исключения летят по любым поводам, вторник, солнышко, настроение игривое.

kvpfs_2
()
Ответ на: комментарий от kvpfs_2

Но я мог и не кидать, если бы обработать надо было бы в одном лишь месте и консистентность не была бы потеряна.

Собственно это и показывает, что exception safety никак не зависит от того, исключения бросаются, или коды возврата возвращаются, а зависит лишь от наличия ошибок и желания их обработать. С тем, что обеспечить exception safety, в случае исключений, можно без портянок try/catch вроде тоже разобрались.

В случае же васянки спиписта исключения летят по любым поводам, вторник, солнышко, настроение игривое.

Ну а тут мы опять возвращаемся к тому, что это сугубо твоё личное разделение на коды возврата/паники (как в расте/го) и в плюсах обычно так не делают.
Понятно, что тебе такое не нравится, потому что ошибки, которые ты считаешь некритичными тебе приходится ловить и как-то оборачивать в коды возврата, видимо.
Если нравится, то пожалуйста, но если использовать исключения по назначению - для сигнализирования о любых неожиданных ошибках, то таких проблем не возникает.

Ivan_qrt ★★★★★
()
Ответ на: комментарий от Ivan_qrt

Ага, а потом забыть где-нибудь поймать эти исключения и словить kernel panic? Экий вы батенька мазохист, учитывая что у С++ исключений даже stack trace точки, в которой они были сгенерированы, получить нельзя, в отличие от того как, например, это можно сделать в других языках по типу Java.

sadko4u ★★
()
Ответ на: комментарий от sadko4u

забыть где-нибудь поймать эти исключения и словить kernel panic?

Вообще тут речь шла про обработку исключений в целом в прикладных приложениях, а не конкретно в ядре.

Исключения в ядро пока тащить никто не предлагает вроде.

Но даже если рассматривать исключения в случае ядра, то если ты забыл обработать исключение, то точно так же (скорее более вероятно) забудешь обработать код возврата. И kernel panic куда лучше расстрела памяти, она хотя бы точно ничего не повредит.

учитывая что у С++ исключений даже stack trace точки, в которой они были сгенерированы, получить нельзя

Можно. std::stacktrace/boost::stacktrace в помощь.

Ivan_qrt ★★★★★
()
Ответ на: комментарий от kvpfs_2

на #1 кидается исключение, операция не закончена. У а линк создан у b нет, пусть каждый из них и остался в валидном состоянии, но в целом программа будет вести себя некорректно.

Ну так вы же и хотели только «модуль завершить». Вам для этого бейсик сейфти и придумали. Вы можете наконец чётко сформулировать предмет спора? Иначе вы просто спорите с самим собой.

то я не пытаюсь играться в какие-то там гарантиии, просто знаю на верхнем уровне, что модуль нужно грохнуть.

Тьфу ты, да бейсик сейфти для этого и нужен! Если вы даже бейсик сейфти не обеспечиваете, то вы не можете корректно «грохнуть модуль», так как у вас всё, кроме какого-нить _exit(), будет, по сути, давать UB.

Короче. Вы либо чётко описываете предмет спора, либо идёте читать мат часть. Выбирайте. :) А по кругу одно и то же писать - делу не поможет.

anonmyous
()
Ответ на: комментарий от Ivan_qrt

Но даже если рассматривать исключения в случае ядра, то если ты забыл обработать исключение, то точно так же (скорее более вероятно) забудешь обработать код возврата.

Спецификацию исключений удалили из стандарта.

Аттрибуты требующие обработку кодов возврата ввели.

Предупреждения на пропуск в switch возможных вариантов работают.

x86_64 ★★★
()
Последнее исправление: x86_64 (всего исправлений: 1)
Ответ на: комментарий от x86_64

Аттрибуты требующие обработку кодов возврата ввели.

[[nodiscard]] из C23 в ядро попадёт лет через 10 в лучшем случае, так что считай его там нет. А когда попадёт его ещё везде добавить надо будет, что тоже не мало времени займёт.
Но в целом движение хорошее.

Предупреждения на пропуск в switch возможных вариантов работают.

Это подразумевает, что для каждой функции будет создаваться свой enum, содержащий только то, что может вернуть эта функция. При этом она может вернуть что-то сразу из другой функции, т.е. встретить такое в реальном коде практически невозможно.
По сути, если ты обрабатываешь код возврата через switch, то у тебя обязан быть default, иначе что-то пропустишь.

Спецификацию исключений удалили из стандарта.

catch(some_base_exception& ex) перехватит всё, что вылетит (при условии, что все исключения наследуются от some_base_exception). Т.к. это что-то не ожидавшееся тобой, то трактовать его надо как невосстановимое исключение.

Ivan_qrt ★★★★★
()
Ответ на: комментарий от Ivan_qrt

[[nodiscard]]

До этого момента можно пользоваться. [[gnu::warn_unused_result]]

clang то же умеет.

По сути, если ты обрабатываешь код возврата через switch, то у тебя обязан быть default, иначе что-то пропустишь.

Это то же обрабатывается. Да. Для каждой функции с несколькими вариантами отказов придется свой enum писать. Но в них можно константы со сквозной нумерацией. И да. Большинство функций возвращают только один вариант отказа, который и надо обработать.

catch(some_base_exception& ex) перехватит всё, что вылетит (при условии, что все исключения наследуются от some_base_exception). Т.к. это что-то не ожидавшееся тобой, то трактовать его надо как невосстановимое исключение.

Проблема как всегда не в написании кода с исключениями. А в рефакторинге кода, у которого глубоко внутри исключения. Разработчики gcc не зря от использования исключений при разработке gcc отказались. Для больших проектов их применять нецелесообразно, а ядро - большой проект.

x86_64 ★★★
()
Последнее исправление: x86_64 (всего исправлений: 6)

Rust - создали русские силовики, Red Hat и Debian - пяндосские, все программисты мыслят исключительно на C

loveFX
()
Ответ на: комментарий от x86_64

До этого момента можно пользоваться. [[gnu::warn_unused_result]]

Согласен. В ядре для этого __must_check предусмотрен и даже используется. Ок, часть про «скорее более вероятно» снимается.

придется свой enum писать.

Ну вот в ядре все функции, помеченные __must_check возвращают int’ы, да разного рода long’и. Enum’ов я там не видел. Да и вообще в сишном коде возврата ошибок в виде enum’ов я не припомню, т.е. не сильно распространённая практика, судя по-всему.

А рефакторинге кода, у кторого глубоко внутри исключения.

Проблемы с рефакторингом будут только если ты на каком-то этапе был уверен, что откуда-то могут прилететь только конкретные исключения и обработал только их. Не стоит такого предполагать. Хотя бы на каком-то этапе должен быть перехват и обработка всех исключений.

В общем-то есть довольно много проектов, пользующихся исключениями и не испытывающих проблем с рефакторингом. Я уж молчу о том, что есть питон, где на исключениях построено вообще всё.
Так что всё-таки отказ gcc от исключений, это скорее всего особенности их кодовой базы. Переходя с сишки на плюсы я бы тоже не разрешал исключения как минимум до тех пор, пока не будет внедрён повсеместный RAII (т.е. никогда в случае большого проекта).

Ivan_qrt ★★★★★
()
Ответ на: комментарий от x86_64

Это как раз особенность Си.

Да в плюсах так же.

Так а ты имел ввиду, что при обработке кода полученный int к какому-то enum’у кастуется? Или как проверку в switch получить?

Ivan_qrt ★★★★★
()
Ответ на: комментарий от x86_64

Ок, понял.

В принципе вариант интересный, конечно. Но я сомневаюсь, что много кто так делать будет, всё-таки довольно много лишних телодвижений по поддержанию всего этого в актуальном состоянии.

Ну и в случае проброса ошибки из другой функции с другим возвращаемым enum class будет что-то типа:

OtherFuncErrorEnum rc = other_func();
if (rc != OtherFuncErrorEnum::Success) {
    return static_cast<ThisFuncErrorEnum>(rc);
}

Что при рефакторинге и добавлении нового варианта в OtherFuncErrorEnum приведёт к утечке варианта без ошибок/предупреждений.

Ivan_qrt ★★★★★
()
Ответ на: комментарий от Ivan_qrt

Что при рефакторинге и добавлении нового варианта в OtherFuncErrorEnum приведёт к утечке варианта без ошибок/предупреждений.

Тут как раз switch надо. С предупреждением при компиляции. А не if.

x86_64 ★★★
()
Последнее исправление: x86_64 (всего исправлений: 1)
Ответ на: комментарий от x86_64
OtherFuncErrorEnum rc = other_func();
switch (rc) {
    case OtherFuncErrorEnum::Success: break;
    case OtherFuncErrorEnum::Err1:    return ThisFuncErrorEnum::Err1;
    case OtherFuncErrorEnum::Err2:    return ThisFuncErrorEnum::Err2;
    case OtherFuncErrorEnum::Err3:    return ThisFuncErrorEnum::Err3;      
}

Получается так? Ну в принципе вариант, выглядит интересно. Но много бойлерплейта и лишнего кода, да и требует высокой дисциплины со стороны разработчика.

Исключения всё-таки выглядят по надёжнее.

Ivan_qrt ★★★★★
()
Ответ на: комментарий от Ivan_qrt

Можно такой вариант.

OtherFuncErrorEnum rc = other_func();

switch (rc) {

case Success: break;

case Err1:
case Err2:
case Err3:

    return static_cast<ThisFuncErrorEnum>(rc);

}

Но много бойлерплейта и лишнего кода, да и требует высокой дисциплины со стороны разработчика.

Эта дисциплина компилятором внедряется.

x86_64 ★★★
()
Последнее исправление: x86_64 (всего исправлений: 2)
Ответ на: комментарий от x86_64

Можно такой вариант.

Можно, но тогда значения ошибок между enum’ами должны совпадать, т.е. нужен глобальный реестр кодов возврата (предполагаю, что как правило он есть). Ну и он особо ничего не упрощает.

Эта дисциплина компилятором внедряется.

Я имел ввиду, что разработчик должен всё это писать вместо простого if’а и static_cast’а. Должен своевременно обновлять все значения и т.п. Всё это можно обойти менее многословными методами, т.е. требуется дисциплина именно от разработчика/ревьювера.

Ivan_qrt ★★★★★
()
Ответ на: комментарий от zx_gamer

нужно взять, и прочитать что же делает функция, вместо того, чтобы увидеть очевидный bool.

Что делает функция все равно таки надо прочитать.

На C++98 можно написать любой контейнер

Можно. Но с foreach с ним будет работать гораздо удобнее.

если немного подумать, то многие решения можно сделать логичнее и красивее

А если ещё подумать, то с новым стандартом они будут ещё логичнее и ещё красивее.

AntonI ★★★★
()
Последнее исправление: AntonI (всего исправлений: 1)
Ответ на: комментарий от AntonI

foreach реализуется несложным макросом (посмотри на Boost Foreach), добавлять его в язык вовсе не обязательно.

zx_gamer ★★
() автор топика
Ответ на: комментарий от sunjob

… никак не гарантирует отсутствие ошибок.

Если конечно у тебя проект не уровня «Hello world».

WatchCat ★★★★★
()
Ответ на: комментарий от zx_gamer

В стандарте С++98 без явного указания типа итератора, при условии что его нельзя явно получить из контейнера?

AntonI ★★★★
()
Последнее исправление: AntonI (всего исправлений: 1)
Ответ на: комментарий от AntonI

Просто разные стили программирования…

Если мы пишем в процедурном стиле, то объявление функции это ТЗ на ее написание, и конечно, хотелось бы, чтобы ТЗ было максимально четким – что получает, что возвращает, имя функции описывающее ее суть, комментарии если этого всего недостаточно… Тут важно, чтобы все типы были явно определены. Эту часть можно воспринимать как часть проектирования архитектуры программы.

А если я пишу приватный метод, который возвращает итератор объекта, то нафиг мне писать четырех-этажный тип данных, я могу написать и auto, из названия функции будет ясно что она возвращает, да и область видимости такая, что этой функцией буду заниматься только я.

Еще, конечно, есть вариант с написанием очень абстрактного кода, который с какой-то натяжкой можно считать не привязанным к типу данных, но тут могут потом всплыть проблемы, потому что по факту вычисления очень сильно зависят от типа данных… Но, наверное, в некоторых случаях, в очень небольшой области видимости это можно делать.

PS: это к ветке дискуссии «Я обожаю когда auto – возвращаемый тип.»

soomrack ★★★★
()
Последнее исправление: soomrack (всего исправлений: 1)
Ответ на: комментарий от sabacs

Меня всегда безумно радует, когда кто то пытается натянуть правила своего манямирка на весь окружающий мир!

AntonI ★★★★
()
Ответ на: комментарий от soomrack

Все так. Но изначально, в моем примере, про auto как про возвращаемое значение речь вообще не шла:-)

AntonI ★★★★
()
Последнее исправление: AntonI (всего исправлений: 1)
Вы не можете добавлять комментарии в эту тему. Тема перемещена в архив.