суббота, 4 октября 2008 г.

Трюки в коде ядра Linux.

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

Двойное отрицание
Иногда можно встретить двойное логическое отрицание. Например:

#define likely(x)        __builtin_expect(!!(x), 1)

На самом деле, тут мы еще видим реализацию одного из способов оптимизации, но об этом в другой раз. Пример содержит двойное отрицание. Зачем?

Если x является логическим выражением, то двойное отрицание не нужно, но что будет если в коде мы напишем что-то вроде:

int counter=0x100;
/* ... */
if (likely(counter))

И тут становится понятно, что двойное отрицание выражения всегда дает булевую величину (0 или 1). Его можно назвать приведением к булевому типу, что довольно непривычно звучит в C.

Выравнивание
/* align addr on a size boundary - adjust address up/down if needed */
#define _ALIGN_UP(addr,size) (((addr)+((size)-1))&(~((size)-1)))
#define _ALIGN_DOWN(addr,size) ((addr)&(~((size)-1)))

Ну, тут все понятно.

Комментарии с помощью препроцессора
#if 0
/* XXX: let's do this when we verify it is OK */
if (ret & VM_FAULT_OOM)
ret = -ENOMEM;
#endif

Как видим, удобно для быстрого отключения/включения кода, особенно если внутри уже есть комментарии /* ... */.

Необычное сравнение с константой

if (NULL == siocb->scm)
siocb->scm = &tmp_scm;

Помогает избежать ошибок, когда вместо сравнения (==) по ошибке ставится знак присваивания (=).

Является ли значение степенью 2?

bool is_power_of_2(unsigned long n)
{
return (n != 0 && ((n & (n - 1)) == 0));
}


do { /*...*/ } while(0)

# define cap_clear(c) do { (c) = __cap_empty_set; } while (0)

Очень часто используется при определении макросов, широко известный трюк. Теперь раскрытие макроса может использоваться как вызов функции, так как при раскрытии код будет полностью размещен в своем блоке, который выполнится один раз (while(0)). Внутри блока можно определять переменные. Например:
#ifndef swap
#define swap(x, y) do { typeof(x) z = x; x = y; y = z; } while (0)
#endif


Использование goto

Можно долго говорить, что использование goto в C это плохо, но никакое правило не может быть универсальным. Иначе -- фарисейство или фанатизм... ;)
grep "goto" -R * | wc -l
в дереве исходников ядра дает результат больший чем 50 тысяч.

В большинстве случаев это попытка избежать большей вложенности кода при отработке ошибочных систуаций и освобождении ресурсов. Например:

static long do_sys_truncate(const char __user * path, loff_t length)
{
struct nameidata nd;
struct inode * inode;
int error;

error = -EINVAL;
if (length < 0) /* sorry, but loff_t says... */
goto out;
/* ... */
inode = nd.path.dentry->d_inode;

/* For directories it's -EISDIR, for other non-regulars - -EINVAL */
error = -EISDIR;
if (S_ISDIR(inode->i_mode))
goto dput_and_out;
/* ... */
error = vfs_permission(&nd, MAY_WRITE);
if (error)
goto mnt_drop_write_and_out;
/* ... */
put_write_and_out:
put_write_access(inode);
mnt_drop_write_and_out:
mnt_drop_write(nd.path.mnt);
dput_and_out:
path_put(&nd.path);
out:
return error;
}

Это гораздо проще, чем вложенные друг в друга блоки или излишнее разбиение на функции.

Возвращение кодов ошибок с типом "указатель".

Пример кода:

char *tmp = getname(filename);
int fd = PTR_ERR(tmp);
if (!IS_ERR(tmp)) {
/* ... */
}
return fd;
}

Здесь видно, что getname возвращает указатель, который тем-не менее, может содержать признак ошибки. Здесь, кстати, нужно быть осторожным! Так как это противоречит практике возвращать признак ошибки как NULL. Нужно всегда четко понимать, что возвращает функция ядра в случае ошибки: NULL или PTR_ERR. Например, если написать следующий код:

char *tmp = getname(filename);
if (!tmp)
return NULL;
/* ... do something .. */

Это уже уязвимость, хотя выглядит прилично. Если не знать что возвращает getname...

Реализация механизма крайне проста, и сама по-себе тянет на трюк.
#define MAX_ERRNO       4095
#define IS_ERR_VALUE(x) unlikely((x) >= (unsigned long)-MAX_ERRNO)

static inline long IS_ERR(const void *ptr)
{
return IS_ERR_VALUE((unsigned long)ptr);
}

Как видим, указатель содержит отрицательное значение ошибки. Таким образом, выброшенный диапазон адресов приходится на последние 4Kb виртуального адресного пространства, который функция никогда не вернет. Очевидно, что превращение кода ошибки в указатель -- это приведение типа:

static inline void *ERR_PTR(long error)
{
return (void *) error;
}

Вообще, все это дело терпимо для кода ядра Linux, но совсем нехорошо заниматься такими трюками в пользовательском (предположительно переносимом) коде, так-как все-таки это предполагает, что приведение long в void* и назад -- обратимы.

Вот и все на сегодня. :)

1 комментарий:

mkevac комментирует...

Хорошо...

Архив блога