Как стать автором
Обновить

Комментарии 29

Единственное, чего не хватает - это ссылки с примером. По мне самым наглядным будет пример драйвера для USB-устройства.

/* Structure to hold all of our device specific stuff */
struct usb_skel {
	struct usb_device	*udev;			/* the usb device for this device */
	struct usb_interface	*interface;		/* the interface for this device */
	struct semaphore	limit_sem;		/* limiting the number of writes in progress */
	struct usb_anchor	submitted;		/* in case we need to retract our submissions */
	struct urb		*bulk_in_urb;		/* the urb to read data with */
	unsigned char           *bulk_in_buffer;	/* the buffer to receive data */
	size_t			bulk_in_size;		/* the size of the receive buffer */
	size_t			bulk_in_filled;		/* number of bytes in the buffer */
	size_t			bulk_in_copied;		/* already copied to user space */
	__u8			bulk_in_endpointAddr;	/* the address of the bulk in endpoint */
	__u8			bulk_out_endpointAddr;	/* the address of the bulk out endpoint */
	int			errors;			/* the last request tanked */
	bool			ongoing_read;		/* a read is going on */
	spinlock_t		err_lock;		/* lock for errors */
	struct kref		kref;
	struct mutex		io_mutex;		/* synchronize I/O with disconnect */
	unsigned long		disconnected:1;
	wait_queue_head_t	bulk_in_wait;		/* to wait for an ongoing read */
};

Видим и семафор, и мьютекс и spinlock.

Совсем нет. USB устройство потенциально может быть извлечено в любой момент. В частности в момент выполнения операции write. И чем хорош Linux- так это тем, что даже в совершенно "учебном" варианте драйвера мы этот момент обязательно предусматриваем.

static ssize_t skel_write(struct file *file, const char *user_buffer,
			  size_t count, loff_t *ppos)
{
  ...
    /* this lock makes sure we don't submit URBs to gone devices */
	mutex_lock(&dev->io_mutex);
	if (dev->disconnected) {		/* disconnect() was called */
		mutex_unlock(&dev->io_mutex);
		retval = -ENODEV;
		goto error;
	}
...
	return writesize;
...
error:
	if (urb) {
		usb_free_coherent(dev->udev, writesize, buf, urb->transfer_dma);
		usb_free_urb(urb);
	}
	up(&dev->limit_sem);

exit:
	return retval;
}

А еще здесь как видите вполне себе используется goto. Что давно признак дурного тона в прикладном программировании. Так что не только битовые поля.

Что давно признак дурного тона в прикладном программировании.

… исключительно по причине всеобщего помешательства

У каждого своя правда. Кому-то сильно нравится и ВерблюжийКод и названия функций типа bMutexLockFromISR().

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

Так что если кто из авторов решит осчастливить своим кодом остальное человечество - имейте в виду, что сделать checkpatch перед отправкой в ваших интересах. Как и соблюдение правил оформления кода, принятых при проектировании ядра.

По-моему, если бы в Си был механизм отложенной деинициализации (как деструкторы в плюсах в RAII или контекстные менеджеры в питоне), все эти goto в ядре были бы не нужны

вообще-то, goto - это не дурной тон, а очень мощный оператор.

Он позволяет из любого места перепрыгнуть куда угодно.

Как всякое мощное оружие, goto не рекомендовали применять начинающим программистам.

Поэтому и придумали сказку про дурной тон.

Не, тег <саркамз> надо указывать явно... Но да ладно. Все операторы равны, но некоторые равнее, а некоторые... Что до goto - то ядро самый наглядный пример его правильного использования. Да, это в некоторой степени наследие терминала 80x25 символов (как и практически весь приинятый CodingStyle), но тем не менее.

Простите, а каким образом goto относится к ограничениям терминала?

НЛО прилетело и опубликовало эту надпись здесь

Какой-то очень натянутый аргумент.

НЛО прилетело и опубликовало эту надпись здесь

Я вам минус ставил, но не сейчас и в не в этой теме.

НЛО прилетело и опубликовало эту надпись здесь

Открываем CodyngStyle. Читаем пункт "1) Indentation"

Tabs are 8 characters, and thus indentations are also 8 characters.

Далее смотрим пункт "2) Breaking long lines and strings"

The preferred limit on the length of a single line is 80 columns.

Опять запоминаем. К слову сейчас это "preferred", но некоторое время назад было обязательным. И опускаемся ниже к пункту "3) Placing Braces and Spaces". И вот тут задумываемся... Берем ту же функцию write.

static ssize_t skel_write(struct file *file, const char *user_buffer,
			  size_t count, loff_t *ppos)
{
...  
	/* verify that we actually have some data to write */
	if (count == 0)
		goto exit;
...
	if (!(file->f_flags & O_NONBLOCK)) {
		if (down_interruptible(&dev->limit_sem)) {
			retval = -ERESTARTSYS;
			goto exit;
		}
	} else {
		if (down_trylock(&dev->limit_sem)) {
			retval = -EAGAIN;
			goto exit;
		}
	}
...
	if (retval < 0)
		goto error;
...
	if (retval) {
		dev_err(&dev->interface->dev,
			"%s - failed submitting write urb, error %d\n",
			__func__, retval);
		goto error_unanchor;
	}
...
	return writesize;

error_unanchor:
	usb_unanchor_urb(urb);
error:
	if (urb) {
		usb_free_coherent(dev->udev, writesize, buf, urb->transfer_dma);
		usb_free_urb(urb);
	}
	up(&dev->limit_sem);

exit:
	return retval;
}

А теперь задумаемся - допустим мы хотим отказаться от goto. Как говорят сторонники такого подхода - нет ничего невозможного. И это истинная правда. Но правда еще и в том, что каждый отказ от goto будет на один уровень увеличивать вложенность. А мы помним - каждый уровень вложенности - это 8 колонок. В функции write 9 операторов goto. Т.е. мы имеем 9*8=72 колонки занятые на пустом месте только отступами. При рекомендуемых 80 на код у нас остается 8... И на экране откровенно линейный (при условии отсутствия ошибок) код превращается в "ёлочу".

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

Впрочем, если есть альтернативный взгляд я его с радостью выслушаю.

А откуда в этом примере возьмется столько уровней, если все goto можно заменить на тот код, на который они указывают (только код под error: вынести в отдельную функцию, чтобы не дублировать). Размер функции увеличится на несколько строк, но насколько это критично, учитывая, что она и так на один экран не влезает — не знаю.

Перепишите? Этот код вполне может быть изменен. А уж на более понятный и читаемый - абсолютно точно. Я бы посмотрел.

Пока не очень понимаю, в чём подвох (если он есть), но, если позволите, возьму из этого же файла функцию поменьше (для функции write подход тот же, только, как я выше писал, нужно ещё будет код под error: вынести в отдельную функцию)


функция поменьше
static int skel_open(struct inode *inode, struct file *file)
{
    struct usb_skel *dev;
    struct usb_interface *interface;
    int subminor;
    int retval = 0;

    subminor = iminor(inode);

    interface = usb_find_interface(&skel_driver, subminor);
    if (!interface) {
        pr_err("%s - error, can't find device for minor %d\n",
            __func__, subminor);
        return -ENODEV;
    }

    dev = usb_get_intfdata(interface);
    if (!dev)
        return -ENODEV;

    retval = usb_autopm_get_interface(interface);
    if (!retval) {
        /* increment our usage count for the device */
        kref_get(&dev->kref);

        /* save our object in the file's private structure */
        file->private_data = dev;
    }
    return retval;
}

А нет подвоха.

Можете оформлять патч и получать красивую строчку в резюме (еще и с пруфлинком). По мне так вполне хорошо. Но open() все же сильно проще, чем write(). А если делать, то делать единообразно. Т.е. такой подход должен быть как минимум во всем файле. Ну и у maintainer'ов может быть своя аргументация на тему "почему нет". В первом приближении две точки выхода из функции с одинаковым возвратным значением. Но мотивировать почему это плохо я не возьмусь.

В целом использование оператора goto в таком стиле является общепринятым. Но новый код, оформленный как у вас имеет все шансы быть одобренным.

unsigned long disconnected:1;

битовое поле - какой-то легаси флаг наверно ;)

К слову, что называется. Вопрос для обсуждения - почему для "какого-то легаси флага" размером в один единственный бит выбран такой длинный базовый тип? Т.е. почему unsigned long, а не наличествующие рядом __u8, int или даже bool?

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

__u8 это же не C вроде.

Int в принципе вариативен по длине а bool это c99 и в принципе не базовый.

Но думаю самое главное это то что для 32/64 разрядных систем важна атомароность доступа к памяти, и насколько я понимаю аппаратно доступ к 16бттным вариантам unsigned int не приветствуется...

Интересно почитать, но как не специалисту в ядре не понятно в чем отличие up() от unlock(), почему один можно вызвать из прерывания, а другой нет? Правильно я понимаю, что поток ждущий на семафоре просто посыпается, проверяет и опять делает yield() или что-то такое, а unlock() сигнализирует планировщику что кого-то надо разбудить?

Если действительно интересно, то рекомендую для начала Linux Kerne 2.4 Internals (издавалась на русском, но свой экземпляр увы... потому прошу помощи зала). Там применительно к 2.4 разница между spinlock'ами и семафорами. А затем вооружившись полученными сведениями посмотреть реализацию в современных ядрах. Подробный ответ скорее всего потребует не одной статьи.

Если же совсем кратко то ваше предположение верно.

Условный yield, который делает ожидание семафора, переводит статус текущего процесса в "спящий", что означает что планировщик задач не будет его рассматривать в качестве кандидата на запуск. И далее вызывается планировщик процессов, который выбирает следующий процесс из числа готовых к выполнению. А операция разблокирования семафора обратно переключает статус процесс на "готов к выполению". То есть в какой-то момент планировщик выберет наш первый процесс и он продолжит выполнение с той же точки.

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

Ну и ко всему внутри обработчика прерывания прерывания на этом процессоре выключены. То есть при попытке поспать этот процессор просто зависнет навсегда.

Ну и ко всему внутри обработчика прерывания прерывания на этом процессоре выключены. То есть при попытке поспать этот процессор просто зависнет навсегда.

Тогда получается что up() можно делать, а down() нельзя, я правильно я понял?

Да, up() ведь неблокирующая операция. Она просто записывает значения в поля пары структур.

Хотя зря так написал - в теории up() делать можно, но как это реализовано на практике - не могу сказать.

Да, совсем плох стал...

Вопрос-то явно из FAQ и быть не может чтоб он нигде не задокументирован был. И конечно он довольно серьезно разжеван.

Первое, что стоит посмотреть из старенького Linux Kernel Memory Barriers и чуть более современного Unreliable Guide To Locking. Потом можно глянуть тут описаны механизмы блокировок в ядре (актуальная текстовая версия всегда с исходниками ядра). Опять же - внутренне устройство всегда можно посмотреть непосредственно в исходниках. Настоятельно рекомендую вот этот ресурс - прямо клад для разработчика ядра. В нем достаточно легко отыскиваются нужные функции. Например та же sema_init.

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

Зарегистрируйтесь на Хабре, чтобы оставить комментарий

Публикации

Истории