Написание ОС: Многозадачность

    image
    Доброго времени суток, дорогой читатель, скорее всего, ты видел мою предыдущую статью о том, что самому можно написать работоспособную ОС за достаточно короткий срок. Что же, сегодня мы поговорим о реализации многозадачности в моей ОС.

    Что же, вы скорее всего не можете представить себе однозадачную ОС в 2018 году, по этому я решил поговорить о реализации многозадачности в моей ОС. И так, первое — вам надо определиться с типом многозадачности, я выбрал вытесняющую.
    Что она из себя представляет? Вытесняющая многозадачность представляет собой систему распределения вычислительной мощности процессора между процессами: у каждого есть свой квант времени, у кажого есть свой приоритет. И первая проблема — какой квант по длине выбрать, как останавливать выполнение процесса по истечению кванта? На самом деле всё легко как никогда! Мы будем использовать PIT с изначально выставленной частотой в 10026 с копейками прерываний в секунду, тут же мы решаем еще одну проблему: мы уже останавливаем предыдущий процесс. И так, начнем с PIT'а.

    PIT


    PIT — Programmable Interval Timer — счетчик, который по достижению какого-либо запрограммированного количества инкрементов выдаёт сигнал. Так же при помощи этого таймера можно пищать пищалкой в компьютере(той штукой, что пищит после прохождения теста устройств). И так, он считает с частотой 1193182 герц, это значит, что нам надо запрограммировать его на 119(1193182/119 примерно равно 10026). Для этого надо в порт первого генератора отправить 2 байта, сначала младший байт, а потом старший:

    	unsigned short hz = 119;
    	outportb(0x43, 0x34);
    	outportb(0x40, (unsigned char)hz & 0xFF); //Low
    	outportb(0x40, (unsigned char)(hz >> 8) & 0xFF); //Hight, about 10026 times per second


    Теперь стоит приступить к программированию прерывания от PIT, оно имеет IRQ 0, и после ремапа PIC'а будет 0x20м. Для IRQ первого PIC'а я написал вот такой макрос:

    //PIC#0; port 0x20
    #define IRQ_HANDLER(func) char func = 0x90;\
    __asm__(#func ": \npusha \n call __"#func " \n movb $0x20,\
     %al \n outb %al, $0x20 \n popa  \n iret \n");\
    void _## func()


    Структура и процессы


    И так, как вы понимаете, нам надо разработать структуру для каждого процесса, а так же структуру, которая позволяет запомнить все мои выделения памяти.
    Вот, что имею я:
    
    typedef struct _pralloc
    {
    	void * addr;
    	struct _pralloc * next;
    } processAlloc;
    typedef struct
    {
    	void * entry;
    	processAlloc *allocs;
    } ELF_Process;
    typedef struct __attribute__((packed)) _E {
    	unsigned int eax;//4
    	unsigned int ebx;//8
    	unsigned int ecx;//12
    	unsigned int edx;//16
    	unsigned int ebp;//20
    	unsigned int esp;//24
    	unsigned int esi;//28
    	unsigned int edi;//32
    	unsigned int eflags;//36
    	unsigned int state;//40
    	void * startAddr;//44
    	void * currentAddr;//48
    	void * stack;//52
    	unsigned int sse[4 * 8];//
    	unsigned int mmx[2 * 8];//244
    	unsigned int priority;//248
    	unsigned int priorityL;//252
    	void * elf_process;//256
    	char ** argv;//260
    	unsigned int argc;//264
    	unsigned int runnedFrom;//268
    	char * workingDir;//272
    	unsigned int cs;//276 - pop is 4 byte in IRET
    	unsigned int ds;//280
    } Process;
    


    Для начала, нам надо понять следующее: мы можем где-нибудь по глобальному адресу, к примеру, по 0xDEAD положить номер текущего запущенного процесса, тогда при выполнении любого кода мы можем быть уверены: у нас есть номер текущего запущенного процесса, это значит, что при обращении к malloc мы знаем, кому выделяем память, и сразу можем добавить адрес выделенной памяти в список allocs.
    void addProcessAlloc(ELF_Process * p, void * addr)
    {
    	void * z = p->allocs;
    	p->allocs = malloc_wo_adding_to_process(sizeof(processAlloc));
    	p->allocs->addr = addr;
    	p->allocs->next = z;
    }


    Что же, структуру таблицы с описанием процессов мы написали, что дальше, как переключать задачи?
    Для начала хочу заметить, что к примеру, в обработчике локальные переменные хранятся в стеке, а значит после входа в обработчик компилятор портит нам esp. Чтобы такого не произошло создадим переменную с абсолютным адресом, и перед вызовом обработчика будем засовывать ESP туда. В обработчике нам необходимо отослать EOI первому PIC'у и найти процесс, на который нам надо переключиться(не буду описывать механизм приоритетов: он прост, как пробка). Далее — нам надо сохранить все регистры и флаги текущего процесса, по этому сразу перед засовыванием ESP в переменную сохраним все регистры(в том числе сегментные) в стек. В самом обработчике нам очень аккуратно надо их вынуть из стека, так же сохранив флаги и адрес возврата. Хочу заметить, что стек растет вверх(т.е. ESP уменьшается), это значит, что последний регистр, который вы сохранили в стек будет лежать по адресу ESP, предпоследний — ESP +4 и т.п:
    image
    Теперь нам остаётся засунуть в регистры значения регистров процесса, на который мы переключились и выполнить IRET. Profit!

    Запуск процессов


    При запуске процесса нам достаточно выделить стек для процесса, после чего положить в него argc и argv, адрес функции, которая будет отдано управление после завершения процесса. Так же надо установить флаги процессора в нужное вам значение, к примеру, для моей ОС это 0x216, про регистр флагов можно прочитать на википедии.

    Напоследок хочу пожелать успехов, в скором времени я напишу про работу с памятью и другие интересующие вас статьи.
    Удачи, и этичного хакинга!
    Share post

    Comments 20

      0
      А что, AVX не поддерживается?
        0
        Пока что я не храню половину регистров от различных расширений.
          0
          FPU похоже тоже? Это же всё-таки математический сопроцессор, а не просто дополнительный набор регистров.
            0
            Нет, FPU работает, у меня есть статья про написание ОС, там код копирование памяти при помощи SSE, который не работает без включенного сопроцессора.
              0
              Имелось в виду сохранение регистров и статуса FPU при переключении потоков. Не увидел этого в структуре.
            0

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

              0

              Смысл? Calling convention существует много, на ассемблере можно свои делать.
              Зачем ограничивать программы в верчении регистрами?

          +3

          Здорово, если появится еще один Minix и учебник типа «Operating Systems: Design and Implementation».

            0
            Большое спасибо за статью!
              0
              О поддержке HPET/TSC не думали?
                0
                Я правильно понимаю, что ядро и все процессы в этой системе работают в едином адресном пространстве? Если это так, то действительно — заявленный выигрыш по производительности может быть существенным, но становится крайне интересным вопрос о защитных механизмах всего этого хозяйства.
                  –1
                  Хочу заметить, что стек растет вверх(т.е. ESP уменьшается), это значит, что последний регистр, который вы сохранили в стек будет лежать по адресу ESP, предпоследний — ESP +4 и т.п:

                  У стека есть вершина, адрес которой убывает при помещении в стек некоторого значения; таким образом, стек у Вас растет вниз, а не вверх.

                  Для начала хочу заметить, что к примеру, в обработчике локальные переменные хранятся в стеке, а значит после входа в обработчик компилятор портит нам esp. Чтобы такого не произошло создадим переменную с абсолютным адресом, и перед вызовом обработчика будем засовывать ESP туда.

                  Зачем у Вас все так сложно, когда существует регистр EBP; который, в паре с ESP, формирует базовый адрес для локальных переменных.

                  Я понимаю, что Вы только-только начали изучение системного программирования, поэтому «ругать» не хотелось, однако Вашу статью читают и другие начинающие тоже, поэтому как-то так.
                    +1
                    Смотрел группу в ВК про Вашу работу. А что, исходники Вашей оси закрыты?
                      –2
                      Да, но вы можете принять участие в разработки и они станут для Вас доступны:)
                        +8
                        тю… так не интересно. В мире масса других проектов, исходники которых доступны. Если в 2018 году, вопреки упрекам в другой вашей статье в ваш адрес, ещё можно согласится тем, что разрабатывать x86 систему в целях самообразования это вполне себе актуально и нормально, то с закрывать исходники при наличии массы других проектов, более продвинутых и ориентированных на пользователя в 2018 — моветон
                          +2
                          Согласен с предыдущим оратором, однако хотел бы посмотреть исходники
                            0
                            И в правду, зачем писать закрытую ОС и предоставлять код только команде разработчиков, состав которой еще не определен? Лучше дать всем исходники, там BSD или GPL (от злых проприетарщиков) поставить…
                            Хотя, сабж, похоже, не Unix-подобный, а такие системы делать открытыми не принято)
                              0
                              сабж, похоже, не Unix-подобный

                              Судя по тому, что там используется такая ненужная сущность как буква диска, сабж наследует совершенно другие традиции
                        +1
                        Операционные системы: зачем они инженеру? Ответ здесь.
                          +1
                          Где исходники?

                          Only users with full accounts can post comments. Log in, please.