通過分析system_call中斷處理過程來深入理解系統調用


通過分析system_call中斷處理過程來深入理解系統調用

前言說明

本篇為網易雲課堂Linux內核分析課程的第五周作業,上一次作業中我以2個系統調用(getpid, open)作為分析實例來分析系統調用的過程,本篇中我將深入到system_call(匯編級別代碼)中來分析其執行過程.


關鍵詞:system_call, 系統調用


運行環境:

  • Ubuntu 14.04 LTS x64
  • gcc 4.9.2
  • gdb 7.8
  • vim 7.4 with vundle

分析過程

從上一次課后,我們對於系統調用在執行過程中的一些基本情況有了一個比較抽象的認識,一個系統調用的基本過程是用戶態程序通過int 0x80中斷向量指令實現從用戶態進入內核態,系統調用過程中,eax寄存器負責傳遞系統調用號,ebx,ecx等其他寄存器負責傳遞其他參數,但是對於執行完了int 0x80之后,在內核態時:

  • 操作系統到底干了哪些具體的工作?
  • 系統調用在內核態這一階段的過程是什么?

基本概念的總結

  1. 中斷
    中斷分為2種":

    • 可屏蔽中斷: I/O設備發出的所有的中斷請求(IRQ)都產生可屏蔽中斷。可屏蔽中斷產生兩種狀態:屏蔽的(masked)或非屏蔽的(unmasked);當中斷被屏蔽,則CPU控制單元就忽略它。
    • 非可屏蔽中斷:總是由CPU辨認。只有幾個危急事件引起非屏蔽中斷。
  2. 進程上下文
    一般來說,CPU在任何時刻都處於以下三種情況之一:

    • 運行於用戶空間,執行用戶進程;
    • 運行於內核空間,處於進程上下文;
    • 運行於內核空間,處於中斷上下文。

    應用程序通過系統調用陷入內核,此時處於進程上下文。現代幾乎所有的CPU體系結構都支持中斷。當外部設備產生中斷,向CPU發送一個異步信號,CPU調用相應的中斷處理程序來處理該中斷,此時CPU處於中斷上下文。
    在進程上下文中,可以通過current關聯相應的任務。進程以進程上下文的形式運行在內核空間,可以發生睡眠,所以在進程上下文中,可以使作信號量(semaphore)。實際上,內核經常在進程上下文中使用信號量來完成任務之間的同步,當然也可以使用鎖。
    中斷上下文不屬於任何進程,它與current沒有任何關系(盡管此時current指向被中斷的進程)。由於沒有進程背景,在中斷上下文中不能發生睡眠,否則又如何對它進行調度。所以在中斷上下文中只能使用鎖進行同步,正是因為這個原因,中斷上下文也叫做原子上下文(atomic context)(關於同步以后再詳細討論)。在中斷處理程序中,通常會禁止同一中斷,甚至會禁止整個本地中斷,所以中斷處理程序應該盡可能迅速,所以又把中斷處理分成上部和下部。
    相對於進程而言,就是進程執行時的環境。具體來說就是各個變量和數據,包括所有的寄存器變量、進程打開的文件、內存信息等。一個進程的上下文可以分為三個部分:用戶級上下文、寄存器上下文以及系統級上下文

    • 用戶級上下文: 正文、數據、用戶堆棧以及共享存儲區;
    • 寄存器上下文: 通用寄存器、程序寄存器(IP)、處理器狀態寄存器(EFLAGS)、棧指針(ESP);
    • 系統級上下文: 進程控制塊task_struct、內存管理信息(mm_struct、vm_area_struct、pgd、pte)、內核棧。
  3. 中斷上下文與進程上下文
    硬件通過觸發信號,導致內核調用中斷處理程序,進入內核空間。這個過程中,硬件的 一些變量和參數也要傳遞給內核,內核通過這些參數進行中斷處理。所謂的“ 中斷上下文”,其實也可以看作就是硬件傳遞過來的這些參數和內核需要保存的一些其他環境(主要是當前被打斷執行的進程環境)。中斷時,內核不代表任何進程運行,它一般只訪問系統空間,而不會訪問進程空間,內核在中斷上下文中執行時一般不會阻塞.


System_Call

# system call handler stub
ENTRY(system_call)
	RING0_INT_FRAME			# can't unwind into user space anyway
	ASM_CLAC
	pushl_cfi %eax			# save orig_eax
	SAVE_ALL				// 保存系統寄存器信息
	GET_THREAD_INFO(%ebp)   // 獲取thread_info結構的信息
					# system call tracing in operation / emulation
	testl $_TIF_WORK_SYSCALL_ENTRY,TI_flags(%ebp) // 測試是否有系統跟蹤
	jnz syscall_trace_entry   // 如果有系統跟蹤,先執行,然后再回來
	cmpl $(NR_syscalls), %eax // 比較eax中的系統調用號和最大syscall,超過則無效
	jae syscall_badsys  // 無效的系統調用 直接返回
syscall_call:
	call *sys_call_table(,%eax,4) // 調用實際的系統調用程序
syscall_after_call:
	movl %eax,PT_EAX(%esp)		// 將系統調用的返回值eax存儲在棧中
syscall_exit:
	LOCKDEP_SYS_EXIT
	DISABLE_INTERRUPTS(CLBR_ANY)	# make sure we don't miss an interrupt
					# setting need_resched or sigpending
					# between sampling and the iret
	TRACE_IRQS_OFF
	movl TI_flags(%ebp), %ecx
	testl $_TIF_ALLWORK_MASK, %ecx	//檢測是否所有工作已完成
	jne syscall_exit_work  			//工作已經完成,則去進行系統調用推出工作

restore_all:
	TRACE_IRQS_IRET			// iret 從系統調用返回

System_Call的基本處理流程為:

  • 首先保存中斷上下文(SAVE_ALL,也就是CPU狀態,包括各個寄存器),判斷請求的系統調用是否有效
  • 然后call *sys_call_table(,%eax,4)通過系統查詢系統調用查到相應的系統調用程序地址,執行相應的系統調用
  • 系統調用完后,返回系統調用的返回值
  • 關閉中斷響應,檢測系統調用的所有工作是否已經完成,如果完成則進行syscall_exit_work(完成系統調用退出工作)
  • 最后restore_all(恢復中斷請求響應),返回用戶態

接下來對於中間比較關鍵的片段代碼進行重點分析

System_Call中的關鍵部分

syscall_exit_work

syscall_exit_work:
	testl $_TIF_WORK_SYSCALL_EXIT, %ecx //測試syscall的工作完成
	jz work_pending
	TRACE_IRQS_ON  //切換中斷請求響應追蹤可用
	ENABLE_INTERRUPTS(CLBR_ANY)	# could let syscall_trace_leave() call
					//schedule() instead
	movl %esp, %eax
	call syscall_trace_leave //停止追蹤系統調用
	jmp resume_userspace //返回用戶空間,只需要檢查need_resched
END(syscall_exit_work)

該過程為系統調用完成后如何退出調用的過程,其中比較重要的是work_pending,詳見如下:

work_pending:
	testb $_TIF_NEED_RESCHED, %cl  // 判斷是否需要調度
	jz work_notifysig   // 不需要則跳轉到work_notifysig
work_resched:
	call schedule   // 調度進程
	LOCKDEP_SYS_EXIT
	DISABLE_INTERRUPTS(CLBR_ANY)	# make sure we don't miss an interrupt
					# setting need_resched or sigpending
					# between sampling and the iret
	TRACE_IRQS_OFF
	movl TI_flags(%ebp), %ecx
	andl $_TIF_WORK_MASK, %ecx	// 是否所有工作都已經做完
	jz restore_all  			// 是則退出
	testb $_TIF_NEED_RESCHED, %cl // 測試是否需要調度
	jnz work_resched  			// 重新執行調度代碼

work_notifysig:				// 處理未決信號集
#ifdef CONFIG_VM86
	testl $X86_EFLAGS_VM, PT_EFLAGS(%esp) // 判斷是否在虛擬8086模式下
	movl %esp, %eax
	jne work_notifysig_v86		// 返回到內核空間
1:
#else
	movl %esp, %eax
#endif
	TRACE_IRQS_ON  // 啟動跟蹤中斷請求響應
	ENABLE_INTERRUPTS(CLBR_NONE)
	movb PT_CS(%esp), %bl
	andb $SEGMENT_RPL_MASK, %bl
	cmpb $USER_RPL, %bl
	jb resume_kernel        // 恢復內核空間
	xorl %edx, %edx
	call do_notify_resume  // 將信號投遞到進程
	jmp resume_userspace  // 恢復用戶空間

#ifdef CONFIG_VM86
	ALIGN
work_notifysig_v86:
	pushl_cfi %ecx			# save ti_flags for do_notify_resume
	call save_v86_state		// 保存VM86模式下的CPU信息
	popl_cfi %ecx
	movl %eax, %esp
	jmp 1b
#endif
END(work_pending)

首先是work_pending這段匯編邏輯:

  • 檢查是否需要進行調度
  • 如果需要,進行進程調度,然后再次進行判斷
  • 如果無需調度,那就去執行work_notifying,處理信號

然后是work_notifysig的這段匯編邏輯:

  • 先檢查是否是虛擬8086模式,即8086保護模式
  • 如果是,那么需要先保存虛模式下的狀態信息
  • 然后跳轉到之前的代碼繼續執行
  • 將信號投遞到進程
  • 恢復用戶空間

最后返回系統調用


流程圖


實驗截圖



我的總結

系統調用中斷本質上是一個保存當前工作狀態,然后處理,最后返回並且恢復進程的過程.


參考資料

  • Understanding The Linux Kernel, the 3rd edtion
  • Linux內核設計與實現,第三版,Robert Love, 機械工業出版社

署名信息

吳欣偉 原創作品轉載請注明出處:《Linux內核分析》MOOC課程:http://mooc.study.163.com/course/USTC-1000029000


免責聲明!

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



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