原來操作系統(tǒng)獲取時間的方式也這么 low
書接上回,上回書咱們說到,通過初始化控制臺的 tty_init 操作,內(nèi)核代碼可以很方便地在控制臺輸出字符啦!
作為用戶也可以通過敲擊鍵盤,或調(diào)用諸如 printf 這樣的庫函數(shù),在屏幕上輸出信息,同時支持換行和滾屏等友好設(shè)計,這些都是 tty_init 初始化,以及其對外封裝的小功能函數(shù),來實(shí)現(xiàn)的。
我們繼續(xù)往下看下一個初始化的倒霉鬼,time_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();
- }
曾經(jīng)我很好奇,操作系統(tǒng)是怎么獲取到當(dāng)前時間的呢?
當(dāng)然,現(xiàn)在都聯(lián)網(wǎng)了,可以從網(wǎng)絡(luò)上實(shí)時同步。那當(dāng)沒有網(wǎng)絡(luò)時,為什么操作系統(tǒng)在啟動之后,可以顯示出當(dāng)前時間呢?難道操作系統(tǒng)在電腦關(guān)機(jī)后,依然不停地在某處運(yùn)行著,勤勤懇懇數(shù)著秒表么?
當(dāng)然不是,那我們今天就打開這個 time_init 函數(shù)一探究竟。
打開這個函數(shù)后我又是很開心,因?yàn)楹芏?,且沒有更深入的方法調(diào)用。
- #define CMOS_READ(addr) ({ \
- outb_p(0x80|addr,0x70); \
- inb_p(0x71); \
- })
- #define BCD_TO_BIN(val) ((val)=((val)&15) + ((val)>>4)*10)
- static void time_init(void) {
- struct tm time;
- do {
- time.tm_sec = CMOS_READ(0);
- time.tm_min = CMOS_READ(2);
- time.tm_hour = CMOS_READ(4);
- time.tm_mday = CMOS_READ(7);
- time.tm_mon = CMOS_READ(8);
- time.tm_year = CMOS_READ(9);
- } while (time.tm_sec != CMOS_READ(0));
- BCD_TO_BIN(time.tm_sec);
- BCD_TO_BIN(time.tm_min);
- BCD_TO_BIN(time.tm_hour);
- BCD_TO_BIN(time.tm_mday);
- BCD_TO_BIN(time.tm_mon);
- BCD_TO_BIN(time.tm_year);
- time.tm_mon--;
- startup_time = kernel_mktime(&time);
- }
夢想的代碼呀!
那主要就是對 CMOS_READ 和 BCD_TO_BIN 都是啥意思展開講一下就明白了了。
首先是 CMOS_READ
- #define CMOS_READ(addr) ({ \
- outb_p(0x80|addr,0x70); \
- inb_p(0x71); \
- })
就是對一個端口先 out 寫一下,再 in 讀一下。
這是 CPU 與外設(shè)交互的一個基本玩法,CPU 與外設(shè)打交道基本是通過端口,往某些端口寫值來表示要這個外設(shè)干嘛,然后從另一些端口讀值來接受外設(shè)的反饋。
至于這個外設(shè)內(nèi)部是怎么實(shí)現(xiàn)的,對使用它的操作系統(tǒng)而言,是個黑盒,無需關(guān)心。那對于我們程序員來說,就更不用關(guān)心了。
對 CMOS 這個外設(shè)的交互講起來可能沒感覺,我們看看與硬盤的交互。
最常見的就是讀硬盤了,我們看硬盤的端口表。
端口 |
讀 |
寫 |
---|---|---|
0x1F0 |
數(shù)據(jù)寄存器 | 數(shù)據(jù)寄存器 |
0x1F1 |
錯誤寄存器 | 特征寄存器 |
0x1F2 |
扇區(qū)計數(shù)寄存器 | 扇區(qū)計數(shù)寄存器 |
0x1F3 |
扇區(qū)號寄存器或 LBA 塊地址 0~7 | 扇區(qū)號或 LBA 塊地址 0~7 |
0x1F4 |
磁道數(shù)低 8 位或 LBA 塊地址 8~15 | 磁道數(shù)低 8 位或 LBA 塊地址 8~15 |
0x1F5 |
磁道數(shù)高 8 位或 LBA 塊地址 16~23 | 磁道數(shù)高 8 位或 LBA 塊地址 16~23 |
0x1F6 |
驅(qū)動器/磁頭或 LBA 塊地址 24~27 | 驅(qū)動器/磁頭或 LBA 塊地址 24~27 |
0x1F7 |
命令寄存器或狀態(tài)寄存器 | 命令寄存器 |
那讀硬盤就是,往除了第一個以外的后面幾個端口寫數(shù)據(jù),告訴要讀硬盤的哪個扇區(qū),讀多少。然后再從 0x1F0 端口一個字節(jié)一個字節(jié)的讀數(shù)據(jù)。這就完成了一次硬盤讀操作。
如果覺得不夠具體,那來個具體的版本。
- 在 0x1F2 寫入要讀取的扇區(qū)數(shù)
- 在 0x1F3 ~ 0x1F6 這四個端口寫入計算好的起始 LBA 地址
- 在 0x1F7 處寫入讀命令的指令號
- 不斷檢測 0x1F7 (此時已成為狀態(tài)寄存器的含義)的忙位
- 如果第四步驟為不忙,則開始不斷從 0x1F0 處讀取數(shù)據(jù)到內(nèi)存指定位置,直到讀完
看,是不是對 CPU 最底層是如何與外設(shè)打交道有點(diǎn)感覺了?是不是也不難?就是按照人家的操作手冊,然后無腦按照要求讀寫端口就行了。
當(dāng)然,讀取硬盤的這個無腦循環(huán),可以 CPU 直接讀取并做寫入內(nèi)存的操作,這樣就會占用 CPU 的計算資源。
也可以交給 DMA 設(shè)備去讀,解放 CPU,但和硬盤的交互,通通都是按照硬件手冊上的端口說明,來操作的,實(shí)際上也是做了一層封裝。
好了,我們已經(jīng)學(xué)會了和一個外設(shè)打交道的基本玩法了。
那我們代碼中要打交道的是哪個外設(shè)呢?就是 CMOS。
它是主板上的一個可讀寫的 RAM 芯片,你在開機(jī)時長按某個鍵就可以進(jìn)入設(shè)置它的頁面。
那我們的代碼,其實(shí)就是與它打交道,獲取它的一些數(shù)據(jù)而已。
我們回過頭看代碼。
- static void time_init(void) {
- struct tm time;
- do {
- time.tm_sec = CMOS_READ(0);
- time.tm_min = CMOS_READ(2);
- time.tm_hour = CMOS_READ(4);
- time.tm_mday = CMOS_READ(7);
- time.tm_mon = CMOS_READ(8);
- time.tm_year = CMOS_READ(9);
- } while (time.tm_sec != CMOS_READ(0));
- BCD_TO_BIN(time.tm_sec);
- BCD_TO_BIN(time.tm_min);
- BCD_TO_BIN(time.tm_hour);
- BCD_TO_BIN(time.tm_mday);
- BCD_TO_BIN(time.tm_mon);
- BCD_TO_BIN(time.tm_year);
- time.tm_mon--;
- startup_time = kernel_mktime(&time);
- }
前面幾個賦值語句 CMOS_READ 就是通過讀寫 CMOS 上的指定端口,依次獲取年月日時分秒等信息。具體咋操作代碼上也寫了,也是按照 CMOS 手冊要求的讀寫指定端口就行了,我們就不展開了。
所以你看,其實(shí)操作系統(tǒng)程序,也是要依靠與一個外部設(shè)備打交道,來獲取這些信息的,并不是它自己有什么魔力。操作系統(tǒng)最大的魅力,就在于它借力完成了一項(xiàng)偉大的事,借 CPU 的力,借硬盤的力,借內(nèi)存的力,以及現(xiàn)在借 CMOS 的力。
至于 CMOS 又是如何知道時間的,這個就不在我們討論范圍了。
接下來 BCD_TO_BIN 就是 BCD 轉(zhuǎn)換成 BIN,因?yàn)閺?CMOS 上獲取的這些年月日都是 BCD 碼值,需要轉(zhuǎn)換成存儲在我們變量上的二進(jìn)制數(shù)值,所以需要一個小算法來轉(zhuǎn)換一下,沒什么意思。
最后一步 kernel_mktime 也很簡單,就是根據(jù)剛剛的那些時分秒數(shù)據(jù),計算從 1970 年 1 月 1 日 0 時起到開機(jī)當(dāng)時經(jīng)過的秒數(shù),作為開機(jī)時間,存儲在 startup_time 這個變量里。
想研究可以仔細(xì)看看這段代碼,不過我覺得這種細(xì)節(jié)不必看。
- startup_time = kernel_mktime(&time);
- // kernel/mktime.c
- long kernel_mktime(struct tm * tm)
- {
- long res;
- int year;
- year = tm->tm_year - 70;
- res = YEAR*year + DAY*((year+1)/4);
- res += month[tm->tm_mon];
- if (tm->tm_mon>1 && ((year+2)%4))
- res -= DAY;
- res += DAY*(tm->tm_mday-1);
- res += HOUR*tm->tm_hour;
- res += MINUTE*tm->tm_min;
- res += tm->tm_sec;
- return res;
- }
就這。
所以今天其實(shí)就是,計算出了一個 startup_time 變量而已,至于這個變量今后會被誰用,怎么用,那就是后話了。
相信你逐漸也體會到了,此時操作系統(tǒng)好多地方都是用外設(shè)要求的方式去詢問,比如硬盤信息、顯示模式,以及今天的開機(jī)時間的獲取等。
所以至少到目前來說,你還不應(yīng)該感覺操作系統(tǒng)有多么的“高端”,很多時候都是繁瑣地,讀人家的硬件手冊,獲取到想要的的信息,拿來給自己用,或者對其進(jìn)行各種設(shè)置。
但你一定要耐得住寂寞,真正體現(xiàn)操作系統(tǒng)的強(qiáng)大設(shè)計之處,還得接著往下讀。
欲知后事如何,且聽下回分解。
本文轉(zhuǎn)載自微信公眾號「低并發(fā)編程」,可以通過以下二維碼關(guān)注。轉(zhuǎn)載本文請聯(lián)系低并發(fā)編程公眾號。本網(wǎng)站已獲得低并發(fā)編程的授權(quán)。