內核態(tài)與用戶態(tài)
早期工程師們在操作系統(tǒng)上編寫程序的時候,自己寫個程序可以訪問別人的程序地址,甚至是操作系統(tǒng)占用的地址,這樣就很容易一不小心就直接把操作系統(tǒng)給干掛了,所以那個時候的程序員編寫程序都得小心翼翼的
計算機核心的資源,一般有:內存,I/O端口,特殊機器指令等,這些資源必須得保護起來,規(guī)定哪些程序可以去訪問,哪些程序不能去訪問
所以引入了特權級別的概念,由硬件設備商直接來提供硬件級別的支持,最常見的就是給CPU指令集的權限分級來控制CPU的訪問權限
比如 Intel CPU指令集操作的權限由高到低劃為4級:Ring0、Ring1、Ring2、Ring3,其中Ring0權限最高,可以使用所有CPU指令集,Ring3權限最低,僅能使用部分CPU指令,比如不能使用操作硬件資源的CPU指令:I/O操作、內存分配等操作;另外CPU處于Ring3狀態(tài)不能訪問Ring0的地址空間,包括代碼和數(shù)據(jù)
CPU指令集,就是CPU中用來計算和控制計算機系統(tǒng)的一套指令的集合,實現(xiàn)軟件指揮硬件執(zhí)行的媒介,常見的CPU指令集有X86、ARM、MIPS、Alpha、RISC等
那么CPU是如何記錄這些特權級信息的?
我們這里以80386CPU為例,前文提到過CPU里面有許多段寄存器(CS、DS、SS、ES、FS、GS等)。這些段寄存器里面存放段選擇符(也叫段選擇子)
段選擇符中包含請求特權級RPL(CPL)字段,通過段選擇符可以去查找全局描述符表GDT、局部描述符表LDT中對應的項,需要先進行特權級檢查;這些項中都包含DPL字段(規(guī)定訪問該段的權限級別),只有DPL >= max {CPL, RPL},才允許訪問
CPL很特殊,跟蹤當前CPU正在執(zhí)行的代碼所在段的描述符中DPL的值,總是等于CPU的當前特權級
內核態(tài)與用戶態(tài)都是操作系統(tǒng)的層面的概念,和CPU硬件沒有必然的聯(lián)系;由于硬件已經(jīng)提供了一套特權級使用的相關機制,Linux操作系統(tǒng)沒有必要重新"造輪子",直接使用了硬件的Ring0和Ring3這兩個級別的權限,也就是使用Ring3作為用戶態(tài),Ring0作為內核態(tài)
那么有人會問為什么Linux系統(tǒng)僅使用了Ring0和Ring3這兩個級別?
因為CPU給的權限管理細度不夠,比如Intel CPU中Ring2和Ring3在操作系統(tǒng)里安全情況沒有區(qū)別,Ring1下的系統(tǒng)權限又需要經(jīng)常調用Ring0特權指令,頻繁切換特權級成本過高,操作系統(tǒng)不如將Ring2合并到Ring3,將Ring1劃入Ring0特權級
另一方面不是每種處理器都像x86一樣支持4個權限級別,有些處理器可能只支持2個級別,更少的特權級別,便于移植其他處理器架構上
我們再來看下linux的體系架構圖:
我們可以發(fā)現(xiàn)Linux系統(tǒng)從整體上看,被劃分為用戶態(tài)和內核態(tài)
內核態(tài)
內核態(tài)是處于操作系統(tǒng)的最核心處,Ring0特權級,擁有操作系統(tǒng)的最高權限,能夠控制所有的硬件資源,掌控各種核心數(shù)據(jù),并且能夠訪問內存中的任意地址;由內核態(tài)統(tǒng)一管理這些核心資源,減少有限資源的訪問和使用沖突;在內核里發(fā)生的任何程序異常都是災難性的,會導致整個操作系統(tǒng)的奔潰
用戶態(tài)
用戶態(tài),就是我們通常編寫程序的地方,處于Ring3特權級,權限較低;這一層次的程序沒有對硬件的直接控制權限,也不能直接訪問地址的內存。在這種模式下,即使程序發(fā)生崩潰也不會影響其他程序,可恢復
什么是系統(tǒng)調用
當計算機啟動的時候,CPU處于Ring0狀態(tài),這個時候所有的指令都可以執(zhí)行,通過主引導程序將磁盤扇區(qū)中的操作系統(tǒng)程序加載到內存中,從而啟動操作系統(tǒng)(需要注意一下,本文的操作系統(tǒng) 以Linux0.12為例子)
也就是說當Linux0.12啟動的時候,是在權限最高級別的內核態(tài)運行的;同時對內存進行劃分,劃出一部分(內核區(qū))專門給內核使用,這部分內存只能被內核使用;主內存區(qū)域給其他應用軟件使用。對這部分感興趣地,可以看看筆者之前的文章Linux0.12內核源碼解讀(6)-main.c
當操作系統(tǒng)啟動完成后,CPU就切換到Ring3級別上,操作系統(tǒng)同時進入用戶態(tài),之后的應用程序代碼都運行在權限最低級別的用戶態(tài)上,通常我們能編寫的程序都運行在用戶態(tài)上
需要格外注意一下,CPU特權級其實并不會對操作系統(tǒng)的用戶造成什么影響!有人會和Linux的用戶權限搞混淆,無論是根用戶(root),管理員,訪客還是一般用戶,它們都屬于用戶;而所有的用戶代碼都在用戶態(tài)Ring3上執(zhí)行,所有的內核代碼都在內核態(tài)Ring0上執(zhí)行,和Linux用戶的身份權限并沒有關系!
因為我們編寫的程序都運行在用戶態(tài)上,是無法對內存和I/O端口的訪問,可以說基本上無法與外部世界交互,但是我們平時工作的時候訪問磁盤、寫文件,這些都是必要的需求,怎么辦?
那就需要通過執(zhí)行系統(tǒng)調用system call,操作系統(tǒng)會切換到內核態(tài),由內核去統(tǒng)一執(zhí)行相關操作(大哥幫小弟去執(zhí)行);當執(zhí)行完操作系統(tǒng)再切換回用戶態(tài)。這樣方便集中管理,減少有限資源的訪問和使用沖突
系統(tǒng)調用是操作系統(tǒng)專門為用戶態(tài)運行的進程與硬件設備之間進行交互提供了一組接口,是用戶態(tài)主動要求切換到內核態(tài)的一種方式
系統(tǒng)調用是怎么實現(xiàn)的
接下來我們就結合Linux0.12的源碼一起來看看系統(tǒng)調用是怎么實現(xiàn)的?
庫函數(shù)write
本文以一個常見的庫函數(shù)write函數(shù)為例來,來更方便大家理解,開始發(fā)車:
?
?
//??lib/write.c #define?__LIBRARY__ #include??//頭文件 _syscall3(int,write,int,fd,const?char?*,buf,off_t,count)?//定義write的實現(xiàn),:fd -?文件描述符;buf -?寫緩沖區(qū)指針;count -?寫字節(jié)數(shù)
?
?
write.c這個文件主要是定義write的實現(xiàn),_syscall3(*,write,*)函數(shù)的主要功能是,向文件描述符fd指定的文件寫入count個字節(jié)的數(shù)據(jù)到緩沖區(qū)buf中
需要注意一下#define __LIBRARY__這個宏定義,這里定義直接原因是為了包括在unistd.h中的內嵌匯編代碼
庫函數(shù)擴展匯編宏
因為_syscall3這個函數(shù)定義在/include/unistd.h中,來看下源碼:
?
?
//??/include/unistd.h #ifdef?__LIBRARY__?#?若提前定義__LIBRARY__,則以后內容被包含 ... #define?__NR_write?4?//系統(tǒng)調用號,用作系統(tǒng)調用函數(shù)表中索引值 ... //定義有3個參數(shù)的,?定義系統(tǒng)調用嵌入式匯編宏函數(shù) //%0?- eax(__res),%1 - eax(__NR_name),%2 - ebx(a),%3 - ecx(b),%4 - edx(c)。 #define?_syscall3(type,name,atype,a,btype,b,ctype,c)? type?name(atype?a,btype?b,ctype?c)? {? long?__res;? __asm__?volatile?("int?$0x80"??????????????????????????????????????????????//?調用系統(tǒng)中斷?0x80 ?:?"=a"?(__res)???????????????????????????????????????????????????????????//?返回值eax(__res) ?:?"0"?(__NR_##name),"b"?((long)(a)),"c"?((long)(b)),"d"?((long)(c)));????//輸入為:系統(tǒng)中斷調用號__NR_name,還有另外3個參數(shù) if?(__res>=0)??????????????????????????????????????????????????????????????//?如果返回值>=0,則直接返回該值 ?return?(type)?__res;? errno=-__res;??????????????????????????????????????????????????????????????//?否則置出錯號,并返回-1 return?-1;????????????????????????????????????????????????????????????????? } #endif?/*?__LIBRARY__?*/ ... int?write(int?fildes,?const?char?*?buf,?off_t?count);?//write系統(tǒng)調用的函數(shù)原型定義 ...
?
?
只有在lib/write.c中先定義了#define __LIBRARY__,那么才能在/include/unistd.h中,找到系統(tǒng)調用號和內嵌匯編_syscall3();不然就代表它不需要進行系統(tǒng)調用,這樣就可以忽略unistd.h中和系統(tǒng)調用相關的宏定義,非常的優(yōu)雅
其實我們可以把write.c中的write函數(shù)再重新整合一下:
?
?
int?write(int?fd,const?char*?buf,off_t?count)? {? long?__res;? __asm__?volatile?(?"int?$0x80"? :?"=a"?(__res)? :?""?(__NR_write),?"b"?((long)(fd)),?"c"?((long)(buf)),?"d"?((long)(count)));? if?(__res>=0)? return?(type)?__res;? errno=-__res;? return?-1;? }
?
?
這樣大家就能更容易明白#define __LIBRARY__的作用
上面int $0x80"表示調用系統(tǒng)中斷0x80 ** ,其實系統(tǒng)調用的本質還是通過中斷(0x80)去實現(xiàn)的**!操作系統(tǒng)中真的是處處離不開中斷。中斷相關知識不了解的,可以看看筆者之前寫過的一篇文章圖解計算機中斷
另外由于程序處于用戶態(tài)無法直接操作硬件資源,所以需要進行系統(tǒng)調用,切換到內核態(tài);也就是說用戶程序如果使用庫函數(shù)write,會進行系統(tǒng)調用
而系統(tǒng)調用,其實就是去調用int 0x80中斷,然后把三個參數(shù)fd、buf、count依次存入ebx、ecx、edx寄存器
還有#define __NR_write4 ,定義了系統(tǒng)調用號;_NR_write會被存入eax寄存器;當調用返回后,從eax取出返回值,存入__res,建立了用戶棧和內核棧的聯(lián)系。至于__NR_write的作用下文再講解
int 0x80中斷 調用對應的中斷處理函數(shù)
我們來看下中斷是調用對應的中斷處理函數(shù)的流程圖:
當發(fā)生中斷的時候,CPU獲取到中斷向量號后,通過IDTR,去查找IDT中斷描述符表,得到相應的中斷描述符;然后根據(jù)描述符中的對應中斷處理程序的入口地址,去執(zhí)行中斷處理程序
早在linux0.12啟動時,會進行調度程序初始化main.c/sched_init(),其源碼:
?
?
//?????/kernel/sched.c ... void?sched_init(void) { ?... ?set_system_gate(0x80,&system_call);//設置系統(tǒng)調用中斷門 } ...
?
?
set_system_gate在之前的文章Linux0.12內核源碼解讀(7)-陷阱門初始化講解過,不再贅述
需要注意的是:在用戶態(tài)和內核態(tài)運行的進程使用的棧是不同的,分別叫做用戶棧和內核棧, 兩者各自負責相應特權級別狀態(tài)下的函數(shù)調用;所以當執(zhí)行系統(tǒng)調用中斷int 0x80從用戶態(tài)進入內核態(tài)時,會從用戶棧切換到內核棧,系統(tǒng)調用返回時,還要切換回用戶棧,繼續(xù)完成用戶態(tài)下的函數(shù)調用(這也叫做被中斷進程上下文的保存與恢復)
其中其關鍵作用的是,CPU會可以自動通過TR寄存器找到當前進程的TSS,然后根據(jù)里面ss0和esp0的值找到內核棧的位置,完成用戶棧到內核棧的切換。先了解一下,這塊等進程那塊我們會再詳細聊聊
set_system_gate(0x80,&system_call)這句整體作用是,設置系統(tǒng)調用中斷門,將0x80中斷和函數(shù)system_call綁定在一起,換句話說system_call就是0x80的中斷處理函數(shù)
檢索系統(tǒng)調用函數(shù)表
我們接著去看system_call函數(shù)的源碼:
?
?
//????/kernel/sys_call.s ... //?int?0x80 _system_call: ?push?%ds??????#?壓棧,?保存原段寄存器值 ?push?%es ?push?%fs??? ?pushl?%eax??#?保存eax原值 ?pushl?%edx?? ?pushl?%ecx??#?push?%ebx,%ecx,%edx?as?parameters ?pushl?%ebx??#?to?the?system?call,??ebx,ecx,edx?中放著系統(tǒng)調用對應的C語言函數(shù)的參數(shù) ?movl?$0x10,%edx??#?ds,es?指向內核數(shù)據(jù)段 ?mov?%dx,%ds ?mov?%dx,%es ?movl?$0x17,%edx??#?fs?指向當前局部數(shù)據(jù)段(局部描述符表中數(shù)據(jù)段描述符) ?mov?%dx,%fs ?cmpl?_NR_syscalls,%eax??#?判斷eax是否超過了最大的系統(tǒng)調用號,調用號如果超出范圍的話就跳轉! ?jae?bad_sys_call ?call?_sys_call_table(,%eax,4)???#?間接調用指定功能C函數(shù)! ?pushl?%eax??????????????????????#??把系統(tǒng)調用的返回值入棧! ... ret_from_sys_call:??#當系統(tǒng)調用執(zhí)行完畢之后,會執(zhí)行此處的匯編代碼,從而返回用戶態(tài) ?movl?_current,%eax??#?取當前任務(進程)數(shù)據(jù)結構指針->eax ?cmpl?_task,%eax???#?task[0]?cannot?have?signals ?...
?
?
其中 _sys_call_table(,%eax,4),這里的eax寄存器存放的就是_NR_write系統(tǒng)調用號,_sys_call_table是sys.h中的一個int (*)()類型的數(shù)組,里面存的是所有的系統(tǒng)調用函數(shù)地址,也叫做系統(tǒng)調用函數(shù)表,所以__NR_write也表示系統(tǒng)調用函數(shù)表中的索引值
那為什么%eax * 4乘上4呢?這是因為sys_call_table[]指針每項4 個字節(jié),這樣被調用處理函數(shù)的地址=[_sys_call_table + %eax * 4]
我們再來看下sys_call_table的定義:
?
?
//????/include/linux/sys.h ... extern?int?sys_write(); ... fn_ptr?sys_call_table[]?=?{?sys_setup,?sys_exit,?sys_fork,?sys_read, sys_write,?sys_open,?sys_close,?sys_waitpid,?sys_creat,?sys_link, sys_unlink,?sys_execve,?sys_chdir,?sys_time,?sys_mknod,?sys_chmod, sys_chown,?sys_break,?sys_stat,?sys_lseek,?sys_getpid,?sys_mount, sys_umount,?sys_setuid,?sys_getuid,?sys_stime,?sys_ptrace,?sys_alarm, sys_fstat,?sys_pause,?sys_utime,?sys_stty,?sys_gtty,?sys_access, sys_nice,?sys_ftime,?sys_sync,?sys_kill,?sys_rename,?sys_mkdir, sys_rmdir,?sys_dup,?sys_pipe,?sys_times,?sys_prof,?sys_brk,?sys_setgid, sys_getgid,?sys_signal,?sys_geteuid,?sys_getegid,?sys_acct,?sys_phys, sys_lock,?sys_ioctl,?sys_fcntl,?sys_mpx,?sys_setpgid,?sys_ulimit, sys_uname,?sys_umask,?sys_chroot,?sys_ustat,?sys_dup2,?sys_getppid, sys_getpgrp,?sys_setsid,?sys_sigaction,?sys_sgetmask,?sys_ssetmask, sys_setreuid,sys_setregid,?sys_sigsuspend,?sys_sigpending,?sys_sethostname, sys_setrlimit,?sys_getrlimit,?sys_getrusage,?sys_gettimeofday,? sys_settimeofday,?sys_getgroups,?sys_setgroups,?sys_select,?sys_symlink, sys_lstat,?sys_readlink,?sys_uselib?}; //系統(tǒng)調用總數(shù)目,注意一下:這里相較于linux0.11做了改進,新增系統(tǒng)調用不再需要手動調整該數(shù)目! int?NR_syscalls?=?sizeof(sys_call_table)/sizeof(fn_ptr);
?
?
可以知曉這里的call _sys_call_table(,%eax,4)就是調用系統(tǒng)調用號所對應的內核系統(tǒng)調用函數(shù)sys_write
最終執(zhí)行sys_write
sys_write在fs下的read_write.c:
?
?
//???/fs/read_write.c //?寫文件系統(tǒng)調用 int?sys_write(unsigned?int?fd,char?*?buf,int?count) { ?struct?file?*?file; ?struct?m_inode?*?inode; ??//判斷函數(shù)參數(shù)的有效性 ?if?(fd>=NR_OPEN?||?count?<0?||?!(file=current->filp[fd])) ??return?-EINVAL; ?if?(!count) ??return?0; ??//?取文件相應的i節(jié)點 ?inode=file->f_inode; ??//?若是管道文件,并且是寫管道文件模式,則進行寫管道操作 ?if?(inode->i_pipe) ??return?(file->f_mode&2)?write_pipe(inode,buf,count):-EIO; ??//如果是字符設備文件,則進行寫字符設備操作 ?if?(S_ISCHR(inode->i_mode)) ??return?rw_char(WRITE,inode->i_zone[0],buf,count,&file->f_pos); ??//?如果是塊設備文件,則進行塊設備寫操作 ?if?(S_ISBLK(inode->i_mode)) ??return?block_write(inode->i_zone[0],&file->f_pos,buf,count); ??//?若是常規(guī)文件,則執(zhí)行文件寫操作 ?if?(S_ISREG(inode->i_mode)) ??return?file_write(inode,file,buf,count); ?printk("(Write)inode->i_mode=%06o ",inode->i_mode); ?return?-EINVAL; }
?
?
至此庫函數(shù)write,進行系統(tǒng)調用,最終調用了sys_write這個函數(shù)
我們再通過下圖回顧一下,整個系統(tǒng)調用的過程:
內核態(tài)與用戶態(tài)數(shù)據(jù)交互
到這里我們已經(jīng)了解了系統(tǒng)調用的過程,還遺留一個問題需要去解決一下,就是內核態(tài)與用戶態(tài)如何進行數(shù)據(jù)交互?
回顧系統(tǒng)調用過程中,我們可以發(fā)現(xiàn)寄存器在其中起到了不可或缺的作用,linus在linux0.12中也是采用類似的方法來進行數(shù)據(jù)交互
我們這里繼續(xù)以sys_write函數(shù)為例,來看看里面的file_write(inode,file,buf,count);
?
?
//???/fs/file_dev.c //?寫文件函數(shù)?-?根據(jù)?i?節(jié)點和文件結構信息,將用戶數(shù)據(jù)寫入文件中 int?file_write(struct?m_inode?*?inode,?struct?file?*?filp,?char?*?buf,?int?count) { ?off_t?pos; ?int?block,c; ?struct?buffer_head?*?bh; ?char?*?p; ?int?i=0; /* ?*?ok,?append?may?not?work?when?many?processes?are?writing?at?the?same?time ?*?but?so?what.?That?way?leads?to?madness?anyway. ?*/ ?//如果設置了追加標記位,則更新當前位置指針到文件最后一個字節(jié) ?if?(filp->f_flags?&?O_APPEND) ??pos?=?inode->i_size; ?else ??pos?=?filp->f_pos; ??//?i為已經(jīng)寫入的長度,count為需要寫入的長度 ?while?(ii_dev,block))) ???break; ??c?=?pos?%?BLOCK_SIZE; ??p?=?c?+?bh->b_data;//?開始寫入數(shù)據(jù)的位置 ??bh->b_dirt?=?1;?//標記數(shù)據(jù)需要回寫硬盤 ??c?=?BLOCK_SIZE-c;?//算出能寫的長度 ??if?(c?>?count-i)?c?=?count-i; ??pos?+=?c; ??if?(pos?>?inode->i_size)?{ ???inode->i_size?=?pos; ???inode->i_dirt?=?1; ??} ??i?+=?c; ??while?(c-->0) ???*(p++)?=?get_fs_byte(buf++);//從用戶態(tài)拷貝一個字節(jié)的數(shù)據(jù)到內核態(tài) ??brelse(bh); ?} ??//當數(shù)據(jù)已經(jīng)全部寫入文件或者在寫操作過程中發(fā)生問題時就會退出循環(huán) ?inode->i_mtime?=?CURRENT_TIME; ?if?(!(filp->f_flags?&?O_APPEND))?{ ??filp->f_pos?=?pos; ??inode->i_ctime?=?CURRENT_TIME; ?} ?return?(i?i:-1); }
?
?
我們這里不展開講了,得后面講完磁盤和文件系統(tǒng)再回過頭來講講這塊,把目光聚焦于get_fs_byte函數(shù),我們來看下其源碼:
?
?
//??include/asm/segment.h ? ?//?讀取 fs 段中指定地址處的字節(jié)。 ?//?參數(shù):addr -?指定的內存地址。 ?//?%0?-?(返回的字節(jié)_v);%1 -?(內存地址 addr)。 ?//?返回:返回內存 fs:[addr]處的字節(jié)。 ?//?第 3 行定義了一個寄存器變量_v,該變量將被保存在一個寄存器中,以便于高效訪問和操作。 extern?inline?unsigned?char?get_fs_byte(const?char?*?addr) { ?unsigned?register?char?_v; ?__asm__?("movb?%%fs:%1,%0":"=r"?(_v):"m"?(*addr)); ?return?_v; } ?//?將一字節(jié)存放在 fs 段中指定內存地址處。 ?//?參數(shù):val -?字節(jié)值;addr -?內存地址。 ?//?%0?-?寄存器(字節(jié)值 val);%1 -?(內存地址 addr)。 extern?inline?void?put_fs_byte(char?val,char?*addr) { __asm__?("movb?%0,%%fs:%1"::"r"?(val),"m"?(*addr)); }
?
?
get_fs_byte函數(shù)是從用戶態(tài)拷貝一個字節(jié)的數(shù)據(jù)到內核態(tài),而put_fs_byte則恰恰相反,從內核態(tài)拷貝一個字節(jié)的數(shù)據(jù)到用戶態(tài)
在系統(tǒng)調用運行整個過程中,DS和ES段寄存器指向內核數(shù)據(jù)空間,而FS段寄存器被設置為指向用戶數(shù)據(jù)空間,這可能有人會問為啥?
別忘了在/kernel/sys_call.s中_system_call中的這段:
?
?
_system_call: ... ?movl?$0x10,%edx??#?ds,es?指向內核數(shù)據(jù)段 ?mov?%dx,%ds ?mov?%dx,%es ?movl?$0x17,%edx??#?fs?指向當前局部數(shù)據(jù)段(局部描述符表中數(shù)據(jù)段描述符) ?mov?%dx,%fs ...
?
?
0x10是全局描述符表GDT中內核數(shù)據(jù)段描述符的段值,0x17是局部描述符表LDT中的任務的數(shù)據(jù)段描述符的段值
所以linux這里利用FS寄存器來完成內核數(shù)據(jù)空間與用戶數(shù)據(jù)空間之間的數(shù)據(jù)復制,當進程從中斷調用中退出時,寄存器會自動從內核棧彈出,快捷高效
審核編輯:黃飛
?
評論
查看更多