Ядро Linux считается ужасно масштабным опенсорсным ПО. Последняя на момент написания этой статьи версия 6.5-rc5 состоит из 36 миллионов строк кода. Само собой, Linux — это плод упорного многолетнего труда множества участников проекта.

Однако первая версия Linux, v0.01, была довольно маленькой. Она состояла всего из 10239 строк кода. Если исключить комментарии и пустые строки, то остаётся всего 8670 строк. Это достаточно малый объём для анализа и хорошее начало для изучения внутренностей ядер UNIX-подобных операционных систем.

Я получил удовольствие от чтения кода v0.01. Это походило на посещение Музея компьютерной истории в Маунтин-Вью — я наконец-то убедился, что легенды верны! Я написал эту статью, чтобы поделиться с вами этим восхитительным опытом.

Примечание: очевидно, я не автор Linux v0.01. Если найдёте в посте ошибки, то дайте мне знать об этом!

Как выглядят системные вызовы?

v0.01 имеет 66 системных вызовов. Вот их список:

access acct alarm break brk chdir chmod
chown chroot close creat dup dup2 execve
exit fcntl fork fstat ftime getegid geteuid
getgid getpgrp setsid getpid getppid
getuid gtty ioctl kill link lock lseek
mkdir mknod mount mpx nice open pause
phys pipe prof ptrace read rename rmdir
setgid setpgid setuid setup signal stat
stime stty sync time times ulimit umask
umount uname unlink ustat utime waitpid write
  • Она поддерживает чтение, запись и удаление файлов и папок. Также поддерживаются другие фундаментальные концепции наподобие chmod(2) (разрешений), chown(2) (владельцев) и pipe(2) (взаимодействие между процессами).

  • fork(2) и execve(2) уже существуют. Не поддерживается только формат исполняемых файлов a.out.

  • Концепция сокетов не реализована, а значит, никакой поддержки сетей.

  • Некоторые функции наподобие mount(2) не реализованы. Они лишь возвращают ENOSYS:

int sys_mount()
{
	return -ENOSYS;
}

Глубокий хардкод под архитектуру Intel 386

У Линуса были знаменитые дебаты с автором MINIX Эндрю Таненбаумом о том, что же лучше подходит для архитектуры операционных систем: монолит или микроядро?

Таненбаум заявил, что Linux не портируем, потому что сильно адаптирован под Intel 386 (i386):

MINIX проектировалась с расчётом на разумную портируемость и была портирована с линейки Intel на 680x0 (Atari, Amiga, Macintosh), SPARC и NS32016. LINUX достаточно сильно привязан к 80x86. Это ошибочный путь.

И это действительно так. Linux v0.01 была сильно адаптирована под i386. Вот реализация strcpy в include/string.h:

extern inline char * strcpy(char * dest,const char *src)
{
__asm__("cld\n"
	"1:\tlodsb\n\t"
	"stosb\n\t"
	"testb %%al,%%al\n\t"
	"jne 1b"
	::"S" (src),"D" (dest):"si","di","ax");
return dest;
}

Она написана на языке ассемблера со строковыми командами i386. Да, её можно найти в качестве оптимизированной реализации strcpy в современном Linux, но она находится в include/string.h, а не где-то типа include/i386/string.h. Более того, там нет никаких #ifdef для переключения реализации под другие архитектуры. Это просто хардкод под Intel 386.

Кроме того, поддерживались только устройства PC/AT:

  • CMOS: часы реального времени (init/main.c).

  • Programmable Interval Timer (PIT): таймер (kernel/sched.c).

  • ATA (PIO): жёсткий диск (kernel/hd.c).

  • VGA (text mode): дисплей (kernel/console.c).

  • Intel 8042: клавиатура PS/2 (kernel/keyboard.s). Да, эта реализация полностью написана на языке ассемблера!

Как можно заметить, что они не находятся в папке drivers, как в современном Linux. Они жёстко прописаны в базовых подсистемах.

«FREAX»

Я где-то читал, что изначально Линус назвал своё ядро «FREAX». В Makefile Linux v0.01 всё ещё есть следующий комментарий:

# Makefile for the FREAX-kernel.

Это действительно был FREAX!

Какая файловая система поддерживалась в v0.01?

Сегодня Linux поддерживает множество файловых систем, например ext4, Btrfs и XFS. А как насчёт v0.01? ext2? Нет. В include/linux/fs.h есть подсказка:

#define SUPER_MAGIC 0x137F

Как правильно предположил GPT-4, это файловая система MINIX!

Забавный факт: источником вдохновения для ext («extended file system», «расширенной файловой системы»), предшественницы ext2/ext3/ext4, стала файловая система MINIX.

«Скорее всего», не будет никаких причин менять планировщик

Вот планировщик Linux v0.01:

	while (1) {
		c = -1;
		next = 0;
		i = NR_TASKS;
		p = &task[NR_TASKS];
		while (--i) {
			if (!*--p)
				continue;
			if ((*p)->state == TASK_RUNNING && (*p)->counter > c)
				c = (*p)->counter, next = i;
		}
		if (c) break;
		for(p = &LAST_TASK ; p > &FIRST_TASK ; --p)
			if (*p)
				(*p)->counter = ((*p)->counter >> 1) +
						(*p)->priority;
	}
	switch_to(next);

В i и p хранятся индекс задачи в таблице задач (не PID!) и указатель на структуру task_struct. Самая важная переменная — это counter в task_struct ((*p)->counter). Планировщик берёт задачу с наибольшим значением counter и переключается на неё. Если все задачи, которые можно выполнять, имеют значения счётчика 0, то он присваивает значению  counter каждой задачи counter = (counter >> 1) + priority и перезапускает цикл. Обратите внимание, что counter >> 1  — это более быстрый способ деления на 2.

Самый важный момент — это обновление счётчика. Оно также обновляет значение счётчика задач, которые нельзя выполнять. Кроме того, это означает, что если задача ожидает ввода-вывода долго, а её приоритет выше 2, то при обновлении счётчика его значение будет увеличиваться до определённого верхнего предела. Это всего лишь моя догадка, но я думаю, что это нужно для повышения приоритета редко выполняемых, но чувствительных к задержкам задач наподобие оболочки, которая бОльшую часть времени ожидает ввода с клавиатуры.

Наконец, switch_to(next) — это макрос, переключающий контекст CPU на выбранную задачу. Он хорошо описан здесь. Если вкратце, он был основан на специфической особенности x86 под названием Task State Segment (TSS), которая больше не используется для управления задачами в архитектуре x86-64.

Кстати, о планировщике в коде есть интересный комментарий:

 *  'schedule()' - это функция планировщика. Это ХОРОШИЙ КОД! Скорее всего,
 * не будет никаких причин менять её, она должна хорошо работать при всех
 * условиях (например, она обеспечивает сильно зависящим от ввода-вывода
 * процессам хорошее время отклика и тому подобное).

Да, это действительно хороший код. К сожалению (или к счастью), это пророчество оказалось ошибочным. Linux стал одним из самых практичных и высокопроизводительных ядер, в которое за много лет внесли множество улучшений планирования и новые алгоритмы, например Completely Fair Scheduler (CFS).

Kernel panic в пяти строках

volatile void panic(const char * s)
{
	printk("Kernel panic: %s\n\r",s);
	for(;;);
}

Сообщаем, что произошла ошибка, и вешаем систему. Точка.

fork(2) в пространстве ядра?

Основная часть инициализации ядра находится в init/main.c (любопытный факт: этот файл по-прежнему находится в современном ядре Linux и инициализирует ядро):

void main(void)		/* Это ДЕЙСТВИТЕЛЬНО void, тут нет никаких ошибок. */
{			/* Процедура запуска подразумевает, что */
/*
 * Прерывания всё ещё отключены. Выполняет необходимую подготовку, затем
 * включаем их
 */
	time_init();
	tty_init();
	trap_init();
	sched_init();
	buffer_init();
	hd_init();
	sti();
	move_to_user_mode();
	if (!fork()) {		/* мы рассчитываем, что это пройдёт без ошибок */
		init();
	}
/*
 *   ПРИМЕЧАНИЕ!! Для любой другой задачи 'pause()' будет означать, что
 * нужно получить сигнал для пробуждения, но task0 - это единственное
 * исключение (см. 'schedule()'), потому что task0 активируется в каждый
 * момент простоя (когда не может выполняться ни одна другая задача)
 * Для task0 'pause()' просто означает, что мы проверяем, может ли выполниться
 * какая-то другая задача, и если нет, то мы возвращаемся сюда.
 */
	for(;;) pause();
}

void init(void)
{
	int i,j;

	setup();
	if (!fork())
		_exit(execve("/bin/update",NULL,NULL));
	(void) open("/dev/tty0",O_RDWR,0);
	(void) dup(0);
	(void) dup(0);
	printf("%d buffers = %d bytes buffer space\n\r",NR_BUFFERS,
		NR_BUFFERS*BLOCK_SIZE);
	printf(" Ok.\n\r");
	if ((i=fork())<0)
		printf("Fork failed in init\r\n");
	else if (!i) {
		close(0);close(1);close(2);
		setsid();
		(void) open("/dev/tty0",O_RDWR,0);
		(void) dup(0);
		(void) dup(0);
		_exit(execve("/bin/sh",argv,envp));
	}
	j=wait(&i);
	printf("child %d died with code %04x\n",j,i);
	sync();
	_exit(0);	/* ПРИМЕЧАНИЕ! _exit, а не exit() */
}

Этот код вызывает функции инициализации каждой подсистемы, всё довольно просто. Но есть и нечто интересное: он вызывает fork(2) в main() ядра. Кроме того, init() выглядит как обычная реализация в пользовательском пространстве, но она жёстко прописана в коде ядра!

Похоже, что она выполняет fork(2) в пространстве ядра, но на самом деле это не так. Хитрость здесь заключается в move_to_user_mode():

#define move_to_user_mode() \
__asm__ ("movl %%esp,%%eax\n\t" \ // EAX = current stack pointer
	"pushl $0x17\n\t" \           // SS (user data seg)
	"pushl %%eax\n\t" \           // ESP
	"pushfl\n\t" \                // EFLAGS
	"pushl $0x0f\n\t" \           // CS (user code seg)
	"pushl $1f\n\t" \             // EIP (return address)
	"iret\n" \                    // switch to user mode
	"1:\tmovl $0x17,%%eax\n\t" \  // IRET returns to this address
	"movw %%ax,%%ds\n\t" \        // Set DS to user data segment
	"movw %%ax,%%es\n\t" \        // Set ES to user data segment
	"movw %%ax,%%fs\n\t" \        // Set FS to user data segment
	"movw %%ax,%%gs" \            // Set GS to user data segment
	:::"ax")                      // No RET instruction here: 
                                  // continue executing following
                                  // lines!

У нас нет необходимости полностью понимать представленный выше ассемблерный код. Достаточно знать, что он переключается в пользовательский режим при помощи команды IRET , но продолжает исполнять следующие строки в коде ядра с текущим указателем стека! Таким образом, последующий if (!fork()) исполняется в пользовательском режиме, а fork(2) на самом деле — это системный вызов.

У Линуса не было машины с 8 МБ ОЗУ

 * Если у вас больше 8 мб памяти, то вам не повезло. Если у меня её
 * нет, почему у вас должна быть :-) Все исходники здесь, измените их.
 * (Серьёзно - это не должно быть слишком сложно. ...

Сегодня довольно широко распространены машины с 8 ГБ ОЗУ. Больше того, 8 ГБ для разработчиков ПО совсем мало ;)

Сложность компиляции современными тулчейнами

В конце я попытался скомпилировать ядро с помощью современных тулчейнов, но мне не удалось. Я думал, что GCC (или сам C) обладает хорошей обратной совместимостью, но её оказалось недостаточно. Даже старый стандартный -std=gnu90 вызывал ошибки компиляции, которые не так уж легко устранить.

Забавно, что Линус использовал собственный GCC с ключом -mstring-insns:

# Если в вашем gcc нет '-mstring-insns' (а он есть только у меня :-)
# удалите его из директив define CFLAGS.

Не совсем понимаю, что это, но, похоже, это функция для поддержки (или оптимизации?) строковых команд x86.

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

Прочитайте сами!

Надеюсь, чтение исходного кода Linux v0.01 понравилось вам так же, как и мне. Если вас интересует v0.01, то скачайте tarball версии v0.01 с kernel.org. Читать код не так сложно, особенно если вы уже читали xv6. Linux v0.01 минималистичен, но очень хорошо написан.

Комментарии (18)


  1. Nurked
    14.08.2023 06:31
    +12

    @PatientZeroКогда прочитал это на hackernews сидел и думал, сколько времени тебе надо будет на перевод всего этого. Я не ошибся. Спасибо за перевод.


    1. PatientZero Автор
      14.08.2023 06:31

      del


    1. PatientZero Автор
      14.08.2023 06:31
      +7


  1. Vanovsky714
    14.08.2023 06:31

    Круто, всегда было интересно, что было в самом начале :)


  1. forthuse
    14.08.2023 06:31
    +3

    К слову,
    По "Хардкору" можно ещё поизучать исходники KolibriOS. :)
    (кто то даже замутил под неё проект в аналогии Wine, но на ассемблере)


    P.S. А, ещё создатель CollapseOS сейчас работает над проектом более старшей ОСИ — DuskOS.
    (вероятно тоже станет нетленным творением в аналах частных достижений IT отрасли)


  1. Johan_Palych
    14.08.2023 06:31
    +5

    Если интересно:
    linux-0.01 скомпилированный в minix-386 и запущенный в эмуляторе 86box
    https://www.linux.org.ru/gallery/screenshots/16983630

    http://www.oldlinux.org/
    http://www.oldlinux.org/Linux.old/qemu-images/

    Linux 0.00, 0.1x images running on Qemu emulator
    https://virtuallyfun.com/2010/08/13/linux-0-00-0-11-on-qemu/
    https://sourceforge.net/projects/bsd42/files/4BSD under Windows/v0.4/Linux 0-11 on qemu-12.5.i386.zip/download
    https://sourceforge.net/projects/bsd42/files/4BSD under Windows/v0.4/linux-0.11 qemu-0.15.0.7z/download


    1. dragonnur
      14.08.2023 06:31

      Спасибо!


    1. strvv
      14.08.2023 06:31

      Действительно, 0.01 версия может собиралась в миниксе версий 1.х, на крайний случай 2.х. позже сам жестко захаркоженный миникс переписывали под gcc. Но код миникс-3 уже сильно раздут. Если учитывать вариации под другие процессорные архитектуры.


    1. OlegSL
      14.08.2023 06:31
      +1

      linux-0.01 скомпилированный в minix-386 и запущенный в эмуляторе 86box
      https://www.linux.org.ru/gallery/screenshots/16983630

      Если кому интересно, скоро, тут на хабре, опубликую очень подробный гайд как сделать себе такой setup.


      1. Johan_Palych
        14.08.2023 06:31

        Нашел Ваш гайд на githab. Интересно и подробно.


  1. ogost
    14.08.2023 06:31

    # Makefile for the FREAX-kernel.

    Возможно за много лет все уже привыкли к названию "Linux", но кмк от "насильной" смены имени ОС только выиграла.


    1. timoxa_dev
      14.08.2023 06:31

      Почему насильной? Я вот например даже и не слышал про то, что ядро изначально называлось FREAX


      1. ogost
        14.08.2023 06:31
        +15

        Администратору FTP сервера, на который его попросил залить Линус, название Freax не понравилось, и он папку назвал Linux без согласования с самим Торвальдсом. Насколько понимаю, это было что-то вроде первого "релиза", до этого исходники ядра не публиковались. Имя прижилось. Сам Торвальдс и ранее подумывал назвать своё детище Линуксом, но посчитал это слишком эгоистичным и назвал его Freax, под этим именем ядро прожило где-то полгода.


  1. saipr
    14.08.2023 06:31
    +5

    первая версия Linux, v0.01, была довольно маленькой. Она состояла всего из 10239 строк кода. Если исключить комментарии и пустые строки, то остаётся всего 8670 строк. Это достаточно малый объём для анализа и хорошее начало для изучения внутренностей ядер UNIX-подобных операционных систем.

    В 1987 году не было ещё Linux, но уже появился Minix и вышеприведённая цитата полностью применима к Minix. Мы тогда задумывали создать на базе Minix операционную систему для первых отечественных ПП ЭВМ тина ЕС 18хх. А книга Эндрю Таненбаум «Operating Systems: Design and Implementation» не потеряла своей актуальности и сегодня.


  1. longhorn_gnu
    14.08.2023 06:31
    +1

    Вау! Наконец-то нашёл хотя-бы один обзор первого Линукса.


  1. Vapaamies
    14.08.2023 06:31
    -1

    А что, уже 25 августа?


  1. OlegSL
    14.08.2023 06:31

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

    В 2008 году Abdel Benamrouche адаптировал под современный (на тот момент) toolchain. Оригинальная статья сейчас не доступна, но на web.archive.org есть ее копия.

    И маленькое занудство, скриншот, который использовался в качестве в русской редакции статья, это как раз скриншот виртуальной машины с qemu с linux-0.01 собранный на современном компиляторе. Образы сейчас тоже, можно скачать только через веб‑архив.


  1. K3nteck
    14.08.2023 06:31

    Сколько сам не пытался читать разобраться не получалось, спасибо за статью!