中斷與系統調用深度分析(以網絡編程接口SocketAPI為例)


1.從計算機CPU與I/O設備的交互方式談起

計算機CPU與I/O設備的交互方式有最早的程序查詢(也叫輪詢)方式,發展到后來的程序中斷方式,DMA方式等。簡單來說,最早的程序查詢方式的機制是,CPU若想和I/O設備交互,首先向I/O設備發出命令,查詢並讀取設備的狀態,如果此時設備可用,則設備開始進行准備工作;CPU每隔一段時間便向設備發出命令,以查詢並讀取設備的當前狀態;當設備准備好后,開始進行數據的傳輸,在傳輸過程中CPU同樣要每隔一段時間就查詢設備發送數據的情況,以防止存儲I/O交互數據的寄存器(也叫數據端口)溢出導致傳輸失敗。程序查詢方式最明顯的特點在於:I/O設備無任何自主性,I/O設備的狀態轉換和數據傳輸的全過程均由CPU全程干預,CPU必須抽出大量的時間用於定期輪詢I/O設備的情況,大大降低了CPU的運算效率。

 

 

上圖是程序查詢方式和程序中斷方式的執行示意圖。

為什么要讓CPU和I/O設備的交互如此頻繁呢?換句話說,為什么要讓I/O設備毫無任何自主性呢?進一步講,如果讓I/O設備有着初步的自主性,允許I/O設備在准備好以及數據傳輸完畢后主動通知CPU,從而打斷了CPU的執行,令CPU轉而服務I該/O設備,這樣做就可以大大提高CPU的執行效率,這就是中斷方式。在著名計算機入門教程《穿越計算機的迷霧》中,作者是這樣講述中斷機制的:“中斷的意思是在做一件事情的時候臨時打個岔,中途去做另外一件事情,然后再回來。這好比拍一下中央處理器的肩膀,告訴它這里有一件事情需要它過來幫個小忙。在有些計算機原理書上,他們把中斷看成你在吃飯,突然電話鈴響了,於是你放下碗筷去接電話,然后再坐下來接着吃。”

中斷機制的執行具體過程如下:

①關中斷,目的是防止其他中斷源前來破壞現場;

②保存斷點,這是為了保證中斷服務程序執行完畢后能正確返回原處;

③引出中斷服務程序,將其於內存中的地址送入CPU的程序計數器PC,這本質上就是一個CPU指令系統的特殊尋址過程;尋址中斷服務程序的入口內存地址有兩種策略:硬件向量法(硬件產生中斷向量,中斷向量由中斷號決定,中斷號的概念在下一段有具體解釋)和軟件查詢法(利用軟件編程的方式事先規定好);

④保存現場狀態;

⑤開中斷,這是為了響應更高級的中斷請求,實現中斷嵌套;

⑥執行中斷服務程序;

⑦關中斷,這是為了保證恢復現場時不被外界打擾;

⑧恢復原來的現場和屏蔽字;

⑨開中斷,中斷返回。(中斷服務程序的最后一條指令)

 其中,①~③由硬件自動完成,該過程也被抽象描述為“中斷隱指令”(這只是一個抽象過程,不是真正的指令);其余步驟由中斷服務程序完成。

如上所述,中斷機制有兩個好處:第一個好處,也是最明顯的好處——通過賦予I/O設備一定的獨立性從而增大CPU執行效率。中斷機制的第二個好處是,不同的外設有不同的中斷信號,因此它們都被CPU分配了各自不同的中斷號,這就意味着計算機內存里可以防止多個不同的程序,而不是像以前那樣每次只能有一個,這也意味着中斷的種類可以有多種多樣,中斷機制不僅可以用在CPU與I/O設備交互上,還可用於軟件應用程序與操作系統的交互上——系統調用。

 

2.從中斷到系統調用

如上所述,中斷分為繁多的類型,因此中斷也有不同的分類方法。

最常用的分類方法是“外中斷”和“內中斷”。該方法可以涵蓋所有的中斷。

外中斷(Interruption,有時直接被稱為“中斷”)指來自CPU和內存以外的部件引起的中斷,如上文所述的I/O設備中斷,如用戶在鍵盤上輸入命令等。外中斷有時直接被稱為“中斷”。

內中斷,又叫“異常”(Exception,這個概念在高級語言編程中經常被提到),則指在CPU和內存內部產生的中斷,最簡單的例子,如“拔電源”,系統突然斷電,CPU確實失去了電能因此無法工作,這是典型的內中斷。此外,如地址非法,除數為0,算數操作溢出,內存頁面失效,用戶程序執行了特權指令等均為內中斷。顯然,系統調用屬於內中斷。

此外,還有“硬件中斷”和“軟件中斷”的分類。硬件中斷是指外部硬件產生的中斷,這顯然屬於外中斷;軟件中斷指的是,通過編程實現的,通過某條指令產生的中斷,顯然系統調用屬於軟件中斷,軟件中斷又屬於內中斷。

 

3.從程序接口到系統調用

操作系統為用戶和應用程序均提供了對計算機硬件系統的接口。前者為命令接口,后者為程序接口,命令接口,如SHELL,腳本等。程序接口,由一組系統調用命令(也叫廣義指令)組成,用戶通過在程序中使用這些系統調用命令來請求操作系統為其提供服務。用戶在程序中可以直接使用這組系統調用命令向系統提出各種服務要求。如當前流行的圖形用戶界面GUI,其本質就是利用系統調用。

 

4.系統調用

系統調用,就是用戶在程序中調用操作系統所提供的一些子功能,系統調用可以被看做特殊的公共子程序。系統中的各種共享資源都由操作系統統一掌管,因此在用戶程序中,凡是與資源有關的操作,如存儲分配,進行I/O傳輸,及管理文件等,都必須通過系統調用方式向操作系統提出夫區請求,並由操作系統代為完成。同城,一個操作系統提供的系統調用命令有數百條。這些系統調用按功能大致可分為如下幾類:

設備管理——完成設備的請求與釋放,以及設備啟動禁用等功能;如多個進程同時爭奪一個聲卡;

文件管理——完成文件的創建,讀寫等;如下載器在下載之前需要用戶設定文件存儲地址;

進程管理——完成對進程的創建,撤銷,阻塞與喚醒等;

進程通信——完成進程之間的消息傳遞或信號傳遞等功能;

內存管理——完成內存的分配,回收以及獲取作業占用內存區大小和初始地址等;

顯然,系統調用運行在系統的核心態,通過系統調用的方式來使用系統功能,可以擺正系統的穩定性和安全性,防止用戶隨意更改或訪問系統的數據或命令。系統調用命令是由操作系統提供的一個或多個子程序模塊實現的。系統調用的運行機制為:用戶通過操作系統運行上層程序,如系統提供的命令解釋程序或用戶自編程序,而上層程序的運行依賴於操作系統的底層管理程序提供服務支持,當需要管理程序服務時,系統則通過硬件中斷機制進入和心態,運行管理程序;也可能是程序運行出現異常情況,被動地需要管理程序的服務,這時就通過異常處理來進入核心態。當管理程序運行結束時,用戶程序需要繼續運行,則通過相應的保存的程序現場退出中斷處理程序或異常處理程序,返回斷點處繼續執行。

操作系統從用戶態轉向核心態的情況有:系統調用——用戶程序要求操作系統的服務,發生一次中斷,用戶程序中產生一個錯誤狀態,用戶程序企圖執行一條特權指令等。如果程序的運行由用戶態轉向核心態,會用到訪管指令,這是一條在用戶態使用的,因此不是特權指令。

 

 上圖是系統調用的執行過程。

 

 上圖顯示了操作系統用戶態和內核態之間的關系。系統調用是二者間重要的橋梁,SocketAPI正是這一點的體現。

 

5.利用gdb跟蹤系統調用

上次實驗已經跟蹤到了Linux內核的start_kernel函數,本次實驗繼續利用gdb跟蹤集成了replyhi的MenuOS系統的Socket相關系統調用。

簡述gdb的使用:

首先啟動一終端,輸入gdb

然后鍵入file 帶路徑的你要測試的文件名(因此建議在該文件所處的目錄下打開用於gdb的終端)

gdb常見命令:

我們在這里對Socket兩個非常常用的系統調用bind()和listen()進行跟蹤,具體步驟如下:

打開Ubuntu虛擬機后,進入上次實驗的裝有MenuOS內核的文件夾,在實驗之前,首先要修改上次的~/MenuOS/menu/Makefile文件:

cd ~/MenuOS/menu
sudo su
# 切換至root用戶以修改Makefile文件
gedit Makefile
# 去掉-S

 

 

 然后,和上次一樣,打開MenuOS,效果如下:

make rootfs

 

 

 此時切不可關閉該終端和QEMU,返回到目錄../linux-5.0.1下,打開另一個終端,輸入如下命令:

gdb
file ./vmlinux
target remote:1234
break __sys_bind
break __sys_listen

此時,gdb已經給__sys_bind和__sys_listen兩個Socket系統調用設定了斷點,gdb響應如下:

 

 

 說明gdb已找到這兩條系統調用的函數定義所在。

然后開始對MenuOS的運行:

c #在gdb終端輸入,接下來兩條命令在QEMU中輸入
replyhi
hello

 

 效果如上圖,gdb已經成功跟蹤到系統調用。

而__sys_bind()系統調用究竟做了哪些事情呢?我們根據gdb給我們函數定義地址的信息,在~/linux-5.0.1/net/socket.c中找到了相應的函數定義,如下圖:

 

 該函數的講解如下:

/*
 *    Bind a name to a socket. Nothing much to do here since it's
 *    the protocol's responsibility to handle the local address.
 *
 *    We move the socket address to kernel space before we call
 *    the protocol layer (having also checked the address is ok).
 */

/*
 *上面一段話的大概意思是,bind()系統調用僅負責將進程的名字與socket綁定;
 *此外,bind()也負責將該socket轉入內核處理;
 *至於處理本地地址,這是網絡協議所需要做的事情。
 */
int __sys_bind(int fd, struct sockaddr __user *umyaddr, int addrlen)
{
    struct socket *sock;
    struct sockaddr_storage address;
    int err, fput_needed;
        /*
         *以fd為索引從當前進程的文件描述符表中,找到對應的file實例,
         *然后從file實例的private_data中,獲取socket實例
         */

    sock = sockfd_lookup_light(fd, &err, &fput_needed);
    if (sock) {
        /*
         * 將用戶空間的地址拷貝到內核空間的緩沖區中
         */
        err = move_addr_to_kernel(umyaddr, addrlen, &address);
        if (!err) {
            /*
             * SELinux相關,不需要關心。
             */
            err = security_socket_bind(sock,
                           (struct sockaddr *)&address,
                           addrlen);
            /*
             * 如果是TCP套接字,sock->ops指向的是inet_stream_ops,
             * sock->ops是在inet_create()函數中初始化,所以bind接口
             * 調用的是inet_bind()函數。
             */

            if (!err)
                err = sock->ops->bind(sock,
                              (struct sockaddr *)
                              &address, addrlen);
        }
        fput_light(sock->file, fput_needed);
    }
    return err;
}

SYSCALL_DEFINE3(bind, int, fd, struct sockaddr __user *, umyaddr, int, addrlen)
{
    return __sys_bind(fd, umyaddr, addrlen);
}    

__sys_listen()的代碼如下圖所示:

 

 

 該函數的講解如下:

/*
 *    Perform a listen. Basically, we allow the protocol to do anything
 *    necessary for a listen, and if that works, we mark the socket as
 *    ready for listening.
 */

/*
 *上述解釋的大致含義是,該系統調用的作用是將端口置為監聽狀態;
 *具體操作視協議而定
*/

int __sys_listen(int fd, int backlog)
{
    struct socket *sock;
    int err, fput_needed;
    int somaxconn;

    sock = sockfd_lookup_light(fd, &err, &fput_needed);
    if (sock) {
        /*
         * sysctl_somaxconn存儲的是服務器監聽時,允許每個套接字連接隊列長度 
         * 的最大值,默認值是128
         */
        somaxconn = sock_net(sock->sk)->core.sysctl_somaxconn;
        /*
         * 如果指定的最大連接數超過系統限制,則使用系統當前允許的連接隊列
         * 中連接的最大數。
         */
        if ((unsigned int)backlog > somaxconn)
            backlog = somaxconn;

        err = security_socket_listen(sock, backlog);
        if (!err)
        /*
         * 從這里開始,socket以后所用的函數將根據TCP/UDP而視協議而定
         */
            err = sock->ops->listen(sock, backlog);

        fput_light(sock->file, fput_needed);
    }
    return err;
}

SYSCALL_DEFINE2(listen, int, fd, int, backlog)
{
    return __sys_listen(fd, backlog);
}

而實際上,在進行Linux系統維護與測試時,一般不需要掌握用gdb跟蹤系統調用的所有深層代碼實現,更重要的是,利用strace命令跟蹤一個進程在運行過程中發生了哪些系統調用,結果如何。

strace命令的常用格式為:

strace +帶路徑的你要檢測的文件名

其常用參數為:

-t 在每行輸出的前面,顯示秒級別的時間
-T 顯示每次系統調用所花費的時間
-v 對於某些相關調用,把完整的環境變量,文件stat結構等打出來。
-f 跟蹤目標進程,以及目標進程創建的所有子進程
-e 控制要跟蹤的事件和跟蹤行為,比如指定要跟蹤的系統調用名稱
-o 把strace的輸出單獨寫到指定的文件
-s 當系統調用的某個參數是字符串時,最多輸出指定長度的內容,默認是32個字節
-p 指定要跟蹤的進程pid, 要同時跟蹤多個pid, 重復多次-p選項即可。

因為很多情況下不知道你想要跟蹤的進程的路徑,因此需要得知其進程PID號,需要使用pidof命令,如查詢火狐瀏覽器的PID:

firefox
pidof firefox

結果如下:

 

 

 然后可以輸入如下命令,開始跟蹤其系統調用:

strace -tt -T -v -f -e trace=network -o ./firefoxlog.txt -p 30589 -p 30544 -p 30494 -p 30443

然后就可以跟蹤到如下內容:(下述內容是我之前跟蹤的結果,因此PID號與上文並不一致)

 

可見,firefox瀏覽器最常調用的三個系統調用分別為:recvmsg(),sendmsg(),socketpair(),它們分別負責發送/接收套接字的創建,數據包的發送和接收。

 對recvmsg(),sendmsg()的深度解析,詳見文末的參考鏈接:

 

 

參考鏈接:

https://github.com/mengning/net/blob/master/doc/systemcall.md

https://blog.csdn.net/weixin_40039738/article/details/81095013

https://blog.csdn.net/u014209688/article/details/71311973


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM