大名鼎鼎的進(jìn)程調(diào)度就是從這里開始的
書接上回,上回書咱們說到,time_init 方法通過與 CMOS 端口進(jìn)行讀寫交互,獲取到了年月日時(shí)分秒等數(shù)據(jù),并通過這些計(jì)算出了開機(jī)時(shí)間 startup_time 變量,是從 1970 年 1 月 1 日 0 時(shí)起到開機(jī)當(dāng)時(shí)經(jīng)過的秒數(shù)。
我們繼續(xù)往下看,大名鼎鼎的進(jìn)程調(diào)度初始化,shed_init。
- void main(void) {
- ...
- mem_init(main_memory_start,memory_end);
- trap_init();
- blk_dev_init();
- chr_dev_init();
- tty_init();
- time_init();
- sched_init();
- buffer_init(buffer_memory_end);
- hd_init();
- floppy_init();
- sti();
- move_to_user_mode();
- if (!fork()) {init();}
- for(;;) pause();
- }
這方法可了不起,因?yàn)樗褪嵌噙M(jìn)程的基石!
終于來到了興奮的時(shí)刻,是不是很激動(dòng)?不過先別激動(dòng),這里只是進(jìn)程調(diào)度的初始化,也就是為進(jìn)程調(diào)度所需要用到的數(shù)據(jù)結(jié)構(gòu)做個(gè)準(zhǔn)備,真正的進(jìn)程調(diào)度還需要調(diào)度算法、時(shí)鐘中斷等機(jī)制的配合。
當(dāng)然,對于理解操作系統(tǒng),流程和數(shù)據(jù)結(jié)構(gòu)最為重要了,而這一段作為整個(gè)流程的起點(diǎn),以及建立數(shù)據(jù)結(jié)構(gòu)的地方,就顯得格外重要了。
我們進(jìn)入這個(gè)方法,一點(diǎn)點(diǎn)往后看。
- void sched_init(void) {
- set_tss_desc(gdt+4, &(init_task.task.tss));
- set_ldt_desc(gdt+5, &(init_task.task.ldt));
- ...
- }
兩行代碼初始化了下 TSS 和 LDT。
先別急問這倆結(jié)構(gòu)是啥。還記得之前講的全局描述符表 gdt 么?它在內(nèi)存的這個(gè)位置,并且被設(shè)置成了這個(gè)樣子。
忘了的看一下第八回 | 煩死了又要重新設(shè)置一遍 idt 和 gdt,這就說明之前看似沒用的細(xì)節(jié)有多重要了,大家一定要有耐心。
說回這兩行代碼,其實(shí)就是往后又加了兩項(xiàng),分別是 TSS 和 LDT。
好,那再說說這倆結(jié)構(gòu)是干嘛的,不過本篇先簡單理解,后面會(huì)詳細(xì)講到。
TSS 叫任務(wù)狀態(tài)段,就是保存和恢復(fù)進(jìn)程的上下文的,所謂上下文,其實(shí)就是各個(gè)寄存器的信息而已,這樣進(jìn)程切換的時(shí)候,才能做到保存和恢復(fù)上下文,繼續(xù)執(zhí)行。
由它的數(shù)據(jù)結(jié)構(gòu)你應(yīng)該可以看出點(diǎn)意思。
- struct tss_struct{
- long back_link;
- long esp0;
- long ss0;
- long esp1;
- long ss1;
- long esp2;
- long ss2;
- long cr3;
- long eip;
- long eflags;
- long eax, ecx, edx, ebx;
- long esp;
- long ebp;
- long esi;
- long edi;
- long es;
- long cs;
- long ss;
- long ds;
- long fs;
- long gs;
- long ldt;
- long trace_bitmap;
- struct i387_struct i387;
- };
而 LDT 叫局部描述符表,是與 GDT 全局描述符表相對應(yīng)的,內(nèi)核態(tài)的代碼用 GDT 里的數(shù)據(jù)段和代碼段,而用戶進(jìn)程的代碼用每個(gè)用戶進(jìn)程自己的 LDT 里得數(shù)據(jù)段和代碼段。
先不管它,我這里放一張超綱的圖,你先找找感覺。
我們接著往下看。
- struct desc_struct {
- unsigned long a,b;
- }
- struct task_struct * task[64] = {&(init_task.task), };
- void sched_init(void) {
- ...
- int i;
- struct desc_struct * p;
- p = gdt+6;
- for(i=1;i<64;i++) {
- task[i] = NULL;
- p->a=p->b=0;
- p++;
- p->a=p->b=0;
- p++;
- }
- ...
- }
這段代碼有個(gè)循環(huán),干了兩件事。
一個(gè)是給一個(gè)長度為 64,結(jié)構(gòu)為 task_struct 的數(shù)組 task 附上初始值。
這個(gè) task_struct 結(jié)構(gòu)就是代表每一個(gè)進(jìn)程的信息,這可是個(gè)相當(dāng)相當(dāng)重要的結(jié)構(gòu)了,把它放在心里。
- struct task_struct {
- /* these are hardcoded - don't touch */
- long state; /* -1 unrunnable, 0 runnable, >0 stopped */
- long counter;
- long priority;
- long signal;
- struct sigaction sigaction[32];
- long blocked; /* bitmap of masked signals */
- /* various fields */
- int exit_code;
- unsigned long start_code,end_code,end_data,brk,start_stack;
- long pid,father,pgrp,session,leader;
- unsigned short uid,euid,suid;
- unsigned short gid,egid,sgid;
- long alarm;
- long utime,stime,cutime,cstime,start_time;
- unsigned short used_math;
- /* file system info */
- int tty; /* -1 if no tty, so it must be signed */
- unsigned short umask;
- struct m_inode * pwd;
- struct m_inode * root;
- struct m_inode * executable;
- unsigned long close_on_exec;
- struct file * filp[NR_OPEN];
- /* ldt for this task 0 - zero 1 - cs 2 - ds&ss */
- struct desc_struct ldt[3];
- /* tss for this task */
- struct tss_struct tss;
- };
這個(gè)循環(huán)做的另一件事,是給 gdt 剩下的位置填充上 0,也就是把剩下留給 TSS 和 LDT 的描述符都先附上空值。
往后展望一下的話,就是以后每創(chuàng)建一個(gè)新進(jìn)程,就會(huì)在后面添加一組 TSS 和 LDT 表示這個(gè)進(jìn)程的任務(wù)狀態(tài)段以及局部描述符表信息。
還記得剛剛的超綱圖吧,未來整個(gè)內(nèi)存的規(guī)劃就是這樣的,不過你先不用理解得很細(xì)。
那為什么一開始就先有了一組 TSS 和 LDT 呢?現(xiàn)在也沒創(chuàng)建進(jìn)程呀。錯(cuò)了,現(xiàn)在雖然我們還沒有建立起進(jìn)程調(diào)度的機(jī)制,但我們正在運(yùn)行的代碼就是會(huì)作為未來的一個(gè)進(jìn)程的指令流。
也就是當(dāng)未來進(jìn)程調(diào)度機(jī)制一建立起來,正在執(zhí)行的代碼就會(huì)化身成為進(jìn)程 0 的代碼。所以我們需要提前把這些未來會(huì)作為進(jìn)程 0 的信息寫好。
如果你覺得很疑惑,別急,等后面整個(gè)進(jìn)程調(diào)度機(jī)制建立起來,并且讓你親眼看到進(jìn)程 0 以及進(jìn)程 1 的創(chuàng)建,以及它們后面因?yàn)檫M(jìn)程調(diào)度機(jī)制而切換,你就明白這一切的意義了。
好,收回來,初始化了一組 TSS 和 LDT 后,再往下看兩行。
- #define ltr(n) __asm__("ltr %%ax"::"a" (_TSS(n)))
- #define lldt(n) __asm__("lldt %%ax"::"a" (_LDT(n)))
- void sched_init(void) {
- ...
- ltr(0);
- lldt(0);
- ...
- }
這又涉及到之前的知識(shí)咯。
還記得 lidt 和 lgdt 指令么?一個(gè)是給 idtr 寄存器賦值,以告訴 CPU 中斷描述符表 idt 在內(nèi)存的位置;一個(gè)是給 gdtr 寄存器賦值,以告訴 CPU 全局描述符表 gdt 在內(nèi)存的位置。
那這兩行和剛剛的類似,ltr 是給 tr 寄存器賦值,以告訴 CPU 任務(wù)狀態(tài)段 TSS 在內(nèi)存的位置;lldt 一個(gè)是給 ldt 寄存器賦值,以告訴 CPU 局部描述符 LDT 在內(nèi)存的位置。
這樣,CPU 之后就能通過 tr 寄存器找到當(dāng)前進(jìn)程的任務(wù)狀態(tài)段信息,也就是上下文信息,以及通過 ldt 寄存器找到當(dāng)前進(jìn)程在用的局部描述符表信息。
我們繼續(xù)看。
- void sched_init(void) {
- ...
- outb_p(0x36,0x43); /* binary, mode 3, LSB/MSB, ch 0 */
- outb_p(LATCH & 0xff , 0x40); /* LSB */
- outb(LATCH >> 8 , 0x40); /* MSB */
- set_intr_gate(0x20,&timer_interrupt);
- outb(inb_p(0x21)&~0x01,0x21);
- set_system_gate(0x80,&system_call);
- ...
- }
四行端口讀寫代碼,兩行設(shè)置中斷代碼。
端口讀寫我們已經(jīng)很熟悉了,就是 CPU 與外設(shè)交互的一種方式,之前講硬盤讀寫以及 CMOS 讀寫時(shí),已經(jīng)接觸過了。
而這次交互的外設(shè)是一個(gè)可編程定時(shí)器的芯片,這四行代碼就開啟了這個(gè)定時(shí)器,之后這個(gè)定時(shí)器變會(huì)持續(xù)的、以一定頻率的向 CPU 發(fā)出中斷信號(hào)。
而這段代碼中設(shè)置的兩個(gè)中斷,第一個(gè)就是時(shí)鐘中斷,中斷號(hào)為 0x20,中斷處理程序?yàn)?timer_interrupt。那么每次定時(shí)器向 CPU 發(fā)出中斷后,便會(huì)執(zhí)行這個(gè)函數(shù)。
這個(gè)定時(shí)器的觸發(fā),以及時(shí)鐘中斷函數(shù)的設(shè)置,是操作系統(tǒng)主導(dǎo)進(jìn)程調(diào)度的一個(gè)關(guān)鍵!沒有他們這樣的外部信號(hào)不斷觸發(fā)中斷,操作系統(tǒng)就沒有辦法作為進(jìn)程管理的主人,通過強(qiáng)制的手段收回進(jìn)程的 CPU 執(zhí)行權(quán)限。
第二個(gè)設(shè)置的中斷叫系統(tǒng)調(diào)用 system_call,中斷號(hào)是 0x80,這個(gè)中斷又是個(gè)非常非常非常非常非常非常非常重要的中斷,所有用戶態(tài)程序想要調(diào)用內(nèi)核提供的方法,都需要基于這個(gè)系統(tǒng)調(diào)用來進(jìn)行。
比如 Java 程序員寫一個(gè) read,底層會(huì)執(zhí)行匯編指令 int 0x80,這就會(huì)觸發(fā)系統(tǒng)調(diào)用這個(gè)中斷,最終調(diào)用到 Linux 里的 sys_read 方法。
這個(gè)過程之后會(huì)重點(diǎn)講述,現(xiàn)在只需要知道,在這個(gè)地方,偷偷把這個(gè)極為重要的中斷,設(shè)置好了。
所以你看這一章的內(nèi)容,偷偷設(shè)置了影響進(jìn)程和影響用戶程序調(diào)用系統(tǒng)方法的兩個(gè)重量級中斷處理函數(shù),不簡單呀~
到目前為止,中斷已經(jīng)設(shè)置了不少了,我們現(xiàn)在看看所設(shè)置好的中斷有哪些。
中斷號(hào) | 中斷處理函數(shù) |
---|---|
0 ~ 0x10 |
trap_init 里設(shè)置的一堆 |
0x20 |
timer_interrupt |
0x21 |
keyboard_interrupt |
0x80 |
system_call |
其中 0-0x10 這 17 個(gè)中斷是 trap_init 里初始化設(shè)置的,是一些基本的中斷,比如除零異常等。這個(gè)在 第14回 中斷初始化 trap_init 有講到。
之后,在控制臺(tái)初始化 con_init 里,我們又設(shè)置了 0x21 鍵盤中斷,這樣按下鍵盤就有反應(yīng)了。這個(gè)在 第16回 控制臺(tái)初始化 tty_init 有講到。
現(xiàn)在,我們又設(shè)置了 0x20 時(shí)鐘中斷,并且開啟定時(shí)器。最后又偷偷設(shè)置了一個(gè)極為重要的 0x80 系統(tǒng)調(diào)用中斷。
找到些感覺沒,有沒有越來越發(fā)現(xiàn),操作系統(tǒng)有點(diǎn)靠中斷驅(qū)動(dòng)的意思,各個(gè)模塊不斷初始化各種中斷處理函數(shù),并且開啟指定的外設(shè)開關(guān),讓操作系統(tǒng)自己慢慢“活”了起來,逐漸通過中斷忙碌于各種事情中,無法自拔。
恭喜你,我們已經(jīng)逐漸在接近操作系統(tǒng)的本質(zhì)了。
回顧一下我們今天干了什么,就三件事。
第一,我們往全局描述符表寫了兩個(gè)結(jié)構(gòu),TSS 和 LDT,作為未來進(jìn)程 0 的任務(wù)狀態(tài)段和局部描述符表信息。
第二,我們初始化了一個(gè)結(jié)構(gòu)為 task_struct 的數(shù)組,未來這里會(huì)存放所有進(jìn)程的信息,并且我們給數(shù)組的第一個(gè)位置附上了 init_task.init 這個(gè)具體值,也是作為未來進(jìn)程 0 的信息。
第三,設(shè)置了時(shí)鐘中斷 0x20 和系統(tǒng)調(diào)用 0x80,一個(gè)作為進(jìn)程調(diào)度的起點(diǎn),一個(gè)作為用戶程序調(diào)用操作系統(tǒng)功能的橋梁,非常之重要。
后面,我們將會(huì)逐漸看到,這些重要的事情,是如何緊密且精妙地結(jié)合在一起,發(fā)揮出奇妙的作用。
欲知后事如何,且聽下回分解。
本文轉(zhuǎn)載自微信公眾號(hào)「低并發(fā)編程」,可以通過以下二維碼關(guān)注。轉(zhuǎn)載本文請聯(lián)系低并發(fā)編程公眾號(hào)。本網(wǎng)站已獲得低并發(fā)編程的授權(quán)