九色91_成人精品一区二区三区中文字幕_国产精品久久久久一区二区三区_欧美精品久久_国产精品99久久久久久久vr_www.国产视频

Hello! 歡迎來到小浪云!


什么是系統調用機制?結合Linux0.12源碼圖解


avatar
小浪云 2025-01-05 165

內核態與用戶態

早期工程師們在操作系統上編寫程序的時候,自己寫個程序可以訪問別人的程序地址,甚至是操作系統占用的地址,這樣就很容易一不小心就直接把操作系統給干掛了,所以那個時候的程序員編寫程序都得小心翼翼的

什么是系統調用機制?結合Linux0.12源碼圖解

計算機核心的資源,包括內存、I/O端口和特殊機器指令等,是系統運行所必需的關鍵元素。為了確保這些資源的安全性,必須對其進行嚴格的訪問控制,規定哪些程序能夠訪問,哪些程序不能訪問。

為此,引入了特權級別的概念,由硬件設備制造商直接提供硬件級別的支持。其中,最常見的控制方式是通過對 CPU 指令集的權限進行分級。以 intel CPU 指令集為例,操作權限被劃分為四個級別:Ring0、Ring1、Ring2 和 Ring3。在這個分級系統中,Ring0 權限最高,擁有對所有 CPU 指令集的完全訪問權限,而 Ring3 權限最低,僅能執行部分受限的 CPU 指令。例如,Ring3 級別的程序無法執行涉及操作硬件資源的指令,比如 I/O 操作和內存分配。此外,處于 Ring3 狀態的 CPU 也無法訪問 Ring0 級別的地址空間,包括其中的代碼和數據。

CPU 指令集是一套用于計算和控制計算機系統的指令集合,是軟件與硬件之間通信的媒介。常見的 CPU 指令集包括 X86、ARM、MIPS、Alpha 和 RISC 等。

那么,CPU 是如何記錄這些特權級信息的呢?以 80386CPU 為例,前文提及了 CPU 內部有多個段寄存器,如 CS、DS、SS、ES、FS 和 GS 等。這些段寄存器中存放著段選擇符,用于指示對應段的特權級別和訪問權限。

什么是系統調用機制?結合Linux0.12源碼圖解

段選擇符中包含請求特權級RPL(CPL)字段,通過段選擇符可以去查找全局描述符表GDT、局部描述符表LDT中對應的項,需要先進行特權級檢查;這些項中都包含DPL字段(規定訪問該段的權限級別),只有DPL >= max {CPL, RPL},才允許訪問

?

CPL很特殊,跟蹤當前CPU正在執行的代碼所在段的描述符中DPL的值,總是等于CPU的當前特權級

內核態與用戶態都是操作系統的層面的概念,和CPU硬件沒有必然的聯系;由于硬件已經提供了一套特權級使用的相關機制,Linux操作系統沒有必要重新”造輪子”,直接使用了硬件的Ring0和Ring3這兩個級別的權限,也就是使用Ring3作為用戶態,Ring0作為內核態

那么有人會問為什么Linux系統僅使用了Ring0和Ring3這兩個級別?

什么是系統調用機制?結合Linux0.12源碼圖解

因為CPU給的權限管理細度不夠,比如Intel CPU中Ring2和Ring3在操作系統里安全情況沒有區別,Ring1下的系統權限又需要經常調用Ring0特權指令,頻繁切換特權級成本過高,操作系統不如將Ring2合并到Ring3,將Ring1劃入Ring0特權級

另一方面不是每種處理器都像x86一樣支持4個權限級別,有些處理器可能只支持2個級別,更少的特權級別,便于移植其他處理器架構

我們再來看下linux的體系架構圖:

什么是系統調用機制?結合Linux0.12源碼圖解

我們可以發現Linux系統從整體上看,被劃分為用戶態和內核態

  1. 內核態

內核態是處于操作系統的最核心處,Ring0特權級,擁有操作系統的最高權限,能夠控制所有的硬件資源,掌控各種核心數據,并且能夠訪問內存中的任意地址;由內核態統一管理這些核心資源,減少有限資源的訪問和使用沖突;在內核里發生的任何程序異常都是災難性的,會導致整個操作系統的奔潰

  1. 用戶態

用戶態,就是我們通常編寫程序的地方,處于Ring3特權級,權限較低;這一層次的程序沒有對硬件的直接控制權限,也不能直接訪問地址的內存。在這種模式下,即使程序發生崩潰也不會影響其他程序,可恢復

什么是系統調用

當計算機啟動的時候,CPU處于Ring0狀態,這個時候所有的指令都可以執行,通過主引導程序將磁盤扇區中的操作系統程序加載到內存中,從而啟動操作系統(需要注意一下,本文的操作系統 以Linux0.12為例子)

也就是說當Linux0.12啟動的時候,是在權限最高級別的內核態運行的;同時對內存進行劃分,劃出一部分(內核區)專門給內核使用,這部分內存只能被內核使用;主內存區域給其他應用軟件使用。對這部分感興趣地,可以看看筆者之前的文章Linux0.12內核源碼解讀(6)-main.c

什么是系統調用機制?結合Linux0.12源碼圖解

當操作系統啟動完成后,CPU就切換到Ring3級別上,操作系統同時進入用戶態,之后的應用程序代碼都運行在權限最低級別的用戶態上,通常我們能編寫的程序都運行在用戶態上

需要格外注意一下,CPU特權級其實并不會對操作系統的用戶造成什么影響!有人會和Linux的用戶權限搞混淆,無論是根用戶(root),管理員,訪客還是一般用戶,它們都屬于用戶;而所有的用戶代碼都在用戶態Ring3上執行,所有的內核代碼都在內核態Ring0上執行,和Linux用戶的身份權限并沒有關系

因為我們編寫的程序都運行在用戶態上,是無法對內存和I/O端口的訪問,可以說基本上無法與外部世界交互,但是我們平時工作的時候訪問磁盤、寫文件,這些都是必要的需求,怎么辦?

什么是系統調用機制?結合Linux0.12源碼圖解

那就需要通過執行系統調用system call,操作系統會切換到內核態,由內核去統一執行相關操作(大哥幫小弟去執行);當執行完操作系統再切換回用戶態。這樣方便集中管理,減少有限資源的訪問和使用沖突

系統調用是操作系統專門為用戶態運行的進程與硬件設備之間進行交互提供了一組接口,是用戶態主動要求切換到內核態的一種方式

系統調用是怎么實現的

接下來我們就結合Linux0.12的源碼一起來看看系統調用是怎么實現的?

庫函數write

本文以一個常見的庫函數write函數為例來,來更方便大家理解,開始發車:

//??lib/write.c  #define?__LIBRARY__ #include??//頭文件  _syscall3(int,write,int,fd,const?char?*,buf,off_t,count)?//定義write的實現,:fd?-?文件描述符;buf?-?寫緩沖區指針;count?-?寫字節數 

write.c這個文件主要是定義write的實現,_syscall3(*,write,*)函數的主要功能是,向文件描述符fd指定的文件寫入count個字節的數據到緩沖區buf中

需要注意一下#define __LIBRARY__這個宏定義,這里定義直接原因是為了包括在unistd.h中的內嵌匯編代碼

庫函數擴展匯編宏

因為_syscall3這個函數定義在/include/unistd.h中,來看下源碼:

//??/include/unistd.h   #ifdef?__LIBRARY__?#?若提前定義__LIBRARY__,則以后內容被包含  ...  #define?__NR_write?4?//系統調用號,用作系統調用函數表中索引值  ...  //定義有3個參數的,?定義系統調用嵌入式匯編宏函數 //%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"??????????????????????????????????????????????//?調用系統中斷?0x80 ?:?"=a"?(__res)???????????????????????????????????????????????????????????//?返回值eax(__res) ?:?"0"?(__NR_##name),"b"?((long)(a)),"c"?((long)(b)),"d"?((long)(c)));????//輸入為:系統中斷調用號__NR_name,還有另外3個參數 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系統調用的函數原型定義  ... 

只有在lib/write.c中先定義了#define __LIBRARY__,那么才能在/include/unistd.h中,找到系統調用號和內嵌匯編_syscall3();不然就代表它不需要進行系統調用,這樣就可以忽略unistd.h中和系統調用相關的宏定義,非常的優雅

其實我們可以把write.c中的write函數再重新整合一下:

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″表示調用系統中斷0x80 ** ,其實系統調用的本質還是通過中斷(0x80)去實現的**!操作系統中真的是處處離不開中斷。中斷相關知識不了解的,可以看看筆者之前寫過的一篇文章圖解計算機中斷

另外由于程序處于用戶態無法直接操作硬件資源,所以需要進行系統調用,切換到內核態;也就是說用戶程序如果使用庫函數write,會進行系統調用

而系統調用,其實就是去調用int 0x80中斷,然后把三個參數fd、buf、count依次存入ebx、ecx、edx寄存器

還有#define __NR_write 4 ,定義了系統調用號;_NR_write會被存入eax寄存器;當調用返回后,從eax取出返回值,存入__res,建立了用戶和內核的聯系。至于__NR_write的作用下文再講解

int 0x80中斷 調用對應的中斷處理函數

我們來看下中斷是調用對應的中斷處理函數的流程圖:什么是系統調用機制?結合Linux0.12源碼圖解

當發生中斷的時候,CPU獲取到中斷向量號后,通過IDtr,去查找IDT中斷描述符表,得到相應的中斷描述符;然后根據描述符中的對應中斷處理程序的入口地址,去執行中斷處理程序

早在linux0.12啟動時,會進行調度程序初始化main.c/sched_init(),其源碼:

//?????/kernel/sched.c  ...  void?sched_init(void) { ?... ?set_system_gate(0x80,&system_call);//設置系統調用中斷門 }  ... 

set_system_gate在之前的文章Linux0.12內核源碼解讀(7)-陷阱門初始化講解過,不再贅述

需要注意的是:在用戶態和內核態運行的進程使用的是不同的,分別叫做用戶棧和內核棧, 兩者各自負責相應特權級別狀態下的函數調用;所以當執行系統調用中斷int 0x80從用戶態進入內核態時,會從用戶棧切換到內核棧,系統調用返回時,還要切換回用戶棧,繼續完成用戶態下的函數調用(這也叫做被中斷進程上下文的保存與恢復)

其中其關鍵作用的是,CPU會可以自動通過TR寄存器找到當前進程的TSS,然后根據里面ss0和esp0的值找到內核棧的位置,完成用戶棧到內核棧的切換。先了解一下,這塊等進程那塊我們會再詳細聊聊

set_system_gate(0x80,&system_call)這句整體作用是,設置系統調用中斷門,將0x80中斷和函數system_call綁定在一起,換句話說system_call就是0x80的中斷處理函數

檢索系統調用函數表

我們接著去看system_call函數的源碼:

//????/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?中放著系統調用對應的C語言函數的參數 ?movl?$0x10,%edx??#?ds,es?指向內核數據段 ?mov?%dx,%ds ?mov?%dx,%es ?movl?$0x17,%edx??#?fs?指向當前局部數據段(局部描述符表中數據段描述符) ?mov?%dx,%fs ?cmpl?_NR_syscalls,%eax??#?判斷eax是否超過了最大的系統調用號,調用號如果超出范圍的話就跳轉! ?jae?bad_sys_call ?call?_sys_call_table(,%eax,4)???#?間接調用指定功能C函數! ?pushl?%eax??????????????????????#??把系統調用的返回值入棧!  ...  ret_from_sys_call:??#當系統調用執行完畢之后,會執行此處的匯編代碼,從而返回用戶態 ?movl?_current,%eax??#?取當前任務(進程)數據結構指針->eax ?cmpl?_task,%eax???#?task[0]?cannot?have?signals ?... 

其中 _sys_call_table(,%eax,4),這里的eax寄存器存放的就是_NR_write系統調用號,_sys_call_table是sys.h中的一個int (*)()類型的數組,里面存的是所有的系統調用函數地址,也叫做系統調用函數表,所以__NR_write也表示系統調用函數表中的索引值

那為什么%eax * 4乘上4呢?這是因為sys_call_table[]指針每項4 個字節,這樣被調用處理函數的地址=[_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?};  //系統調用總數目,注意一下:這里相較于linux0.11做了改進,新增系統調用不再需要手動調整該數目! int?NR_syscalls?=?sizeof(sys_call_table)/sizeof(fn_ptr); 

可以知曉這里的call _sys_call_table(,%eax,4)就是調用系統調用號所對應的內核系統調用函數sys_write

最終執行sys_write

sys_write在fs下的read_write.c:

//???/fs/read_write.c  //?寫文件系統調用 int?sys_write(unsigned?int?fd,char?*?buf,int?count) { ?struct?file?*?file; ?struct?m_inode?*?inode;  ??//判斷函數參數的有效性 ?if?(fd>=NR_OPEN?||?count?filp[fd])) ??return?-EINVAL; ?if?(!count) ??return?0; ??//?取文件相應的i節點 ?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); ??//?若是常規文件,則執行文件寫操作 ?if?(S_ISREG(inode->i_mode)) ??return?file_write(inode,file,buf,count); ?printk("(Write)inode->i_mode=%06o  ",inode->i_mode); ?return?-EINVAL; } 

至此庫函數write,進行系統調用,最終調用了sys_write這個函數

我們再通過下圖回顧一下,整個系統調用的過程:什么是系統調用機制?結合Linux0.12源碼圖解

內核態與用戶態數據交互

到這里我們已經了解了系統調用的過程,還遺留一個問題需要去解決一下,就是內核態與用戶態如何進行數據交互?

回顧系統調用過程中,我們可以發現寄存器在其中起到了不可或缺的作用,linus在linux0.12中也是采用類似的方法來進行數據交互

我們這里繼續以sys_write函數為例,來看看里面的file_write(inode,file,buf,count);

//???/fs/file_dev.c  //?寫文件函數?-?根據?i?節點和文件結構信息,將用戶數據寫入文件中 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. ?*/ ?//如果設置了追加標記位,則更新當前位置指針到文件最后一個字節 ?if?(filp->f_flags?&?O_APPEND) ??pos?=?inode->i_size; ?else ??pos?=?filp->f_pos; ??//?i為已經寫入的長度,count為需要寫入的長度 ?while?(iif?(!(block?=?create_block(inode,pos/BLOCK_SIZE))) ???break; ??if?(!(bh=bread(inode->i_dev,block))) ???break; ??c?=?pos?%?BLOCK_SIZE; ??p?=?c?+?bh->b_data;//?開始寫入數據的位置 ??bh->b_dirt?=?1;?//標記數據需要回寫硬盤 ??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++);//從用戶態拷貝一個字節的數據到內核態 ??brelse(bh); ?} ??//當數據已經全部寫入文件或者在寫操作過程中發生問題時就會退出循環 ?inode->i_mtime?=?CURRENT_TIME; ?if?(!(filp->f_flags?&?O_APPEND))?{ ??filp->f_pos?=?pos; ??inode->i_ctime?=?CURRENT_TIME; ?} ?return?(i?i:-1); } 

我們這里不展開講了,得后面講完磁盤和文件系統再回過頭來講講這塊,把目光聚焦于get_fs_byte函數,我們來看下其源碼:

//??include/asm/segment.h ? ?//?讀取?fs?段中指定地址處的字節。 ?//?參數:addr?-?指定的內存地址。 ?//?%0?-?(返回的字節_v);%1?-?(內存地址?addr)。 ?//?返回:返回內存?fs:[addr]處的字節。 ?//?第?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; }  ?//?將一字節存放在?fs?段中指定內存地址處。 ?//?參數:val?-?字節值;addr?-?內存地址。 ?//?%0?-?寄存器(字節值?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函數是從用戶態拷貝一個字節的數據到內核態,而put_fs_byte則恰恰相反,從內核態拷貝一個字節的數據到用戶態

在系統調用運行整個過程中,DS和ES段寄存器指向內核數據空間,而FS段寄存器被設置為指向用戶數據空間,這可能有人會問為啥?

別忘了在/kernel/sys_call.s中_system_call中的這段:

_system_call: ... ?movl?$0x10,%edx??#?ds,es?指向內核數據段 ?mov?%dx,%ds ?mov?%dx,%es ?movl?$0x17,%edx??#?fs?指向當前局部數據段(局部描述符表中數據段描述符) ?mov?%dx,%fs ... 

0x10是全局描述符表GDT中內核數據段描述符的段值,0x17是局部描述符表LDT中的任務的數據段描述符的段值

所以linux這里利用FS寄存器來完成內核數據空間與用戶數據空間之間的數據復制,當進程從中斷調用中退出時,寄存器會自動從內核棧彈出,快捷高效

相關閱讀

主站蜘蛛池模板: 日韩中文一区 | 华丽的挑战在线观看 | 久久日韩精品一区二区三区 | 蜜桃精品视频在线 | 91高清视频在线观看 | 少妇av片 | 久久大全 | 日韩高清一区 | 国产高清在线精品一区二区三区 | 精品国产一区二区三区日日嗨 | 国产精品欧美日韩 | 涩涩视频网 | 国产毛片久久久久久久久春天 | 国产精品久久国产精品99 | 久久33 | 91麻豆精品国产91久久久久久 | 亚洲精品乱码久久久久久按摩观 | 久久久久九九九女人毛片 | 综合久久av | 久久久久久久久久久久久九 | 少妇精品久久久久久久久久 | 久草.com | 色综合久久天天综合网 | 一级免费毛片 | 日本不卡免费新一二三区 | 国产成年人视频 | 久久99精品国产99久久6男男 | 爱高潮www亚洲精品 中文字幕免费视频 | av一级久久 | 亚洲成av人片在线观看 | 99精品一区二区三区 | 欧美午夜剧场 | 日本网站免费在线观看 | 九九亚洲精品 | 国产欧美精品一区二区三区 | 日韩精品一区二区三区中文在线 | 国产99久久精品一区二区永久免费 | 日日操av | 国产成人高清在线观看 | 凹凸日日摸日日碰夜夜 | 欧美日韩三级在线观看 |