Linux內核分析(五)Linux系統調用中斷處理過程

上一篇博文我們用匯編的方式實現了對系統調用open的引用,這一次我們來深入到系統調用處理的內部,來看看Linux到底是如何處理系統調用的。

 

    系統調用函數system_call的代碼可以在arch/x86/kernel/entry_32.S中,完整的代碼如下:(每行前面的數字是代碼在源文件中的行號)

  490 ENTRY(system_call)

 491         RING0_INT_FRAME                 # can't unwind into user space anyway

 492         ASM_CLAC

 493         pushl_cfi %eax                  # save orig_eax

 494         SAVE_ALL

 495         GET_THREAD_INFO(%ebp)

 496                                         # system call tracing in operation / emulati     on

 497         testl $_TIF_WORK_SYSCALL_ENTRY,TI_flags(%ebp)

 498         jnz syscall_trace_entry

 499         cmpl $(NR_syscalls), %eax

 500         jae syscall_badsys

 501 syscall_call:

 502         call *sys_call_table(,%eax,4)

 503 syscall_after_call:

 504         movl %eax,PT_EAX(%esp)          # store the return value

 505 syscall_exit:

 506         LOCKDEP_SYS_EXIT

 507         DISABLE_INTERRUPTS(CLBR_ANY)    # make sure we don't miss an interrupt

 508                                         # setting need_resched or sigpending

 509                                         # between sampling and the iret

 510         TRACE_IRQS_OFF

 511         movl TI_flags(%ebp), %ecx

 512         testl $_TIF_ALLWORK_MASK, %ecx  # current->work

 513         jne syscall_exit_work

 514

 515 restore_all:

 516         TRACE_IRQS_IRET

 517 restore_all_notrace:

 518 #ifdef CONFIG_X86_ESPFIX32

 519         movl PT_EFLAGS(%esp), %eax      # mix EFLAGS, SS and CS

 520         # Warning: PT_OLDSS(%esp) contains the wrong/random values if we

 521         # are returning to the kernel.

 522         # See comments in process.c:copy_thread() for details.

 523         movb PT_OLDSS(%esp), %ah

 524         movb PT_CS(%esp), %al

 525         andl $(X86_EFLAGS_VM | (SEGMENT_TI_MASK << 8) | SEGMENT_RPL_MASK), %eax

 526         cmpl $((SEGMENT_LDT << 8) | USER_RPL), %eax

 527         CFI_REMEMBER_STATE

 528         je ldt_ss                       # returning to user-space with LDT SS

 529 #endif

 530 restore_nocheck:

 531         RESTORE_REGS 4                  # skip orig_eax/error_code

 532 irq_return:

 533         INTERRUPT_RETURN

================== 引用到的其他函數 ====================

 345 ENTRY(resume_userspace)

 346         LOCKDEP_SYS_EXIT

 347         DISABLE_INTERRUPTS(CLBR_ANY)    # make sure we don't miss an interrupt

 348                                         # setting need_resched or sigpending

 349                                         # between sampling and the iret

 350         TRACE_IRQS_OFF

 351         movl TI_flags(%ebp), %ecx

 352         andl $_TIF_WORK_MASK, %ecx      # is there any work to be done on

 353                                         # int/exception return?

 354         jne work_pending

 355         jmp restore_all

 

 656 syscall_exit_work:

 657         testl $_TIF_WORK_SYSCALL_EXIT, %ecx

 658         jz work_pending

 659         TRACE_IRQS_ON

 660         ENABLE_INTERRUPTS(CLBR_ANY)     # could let syscall_trace_leave() call

 661                                         # schedule() instead

 662         movl %esp, %eax

 663         call syscall_trace_leave

 664         jmp resume_userspace

 665 END(syscall_exit_work)

 

  596 work_resched:

 597         call schedule

 598         LOCKDEP_SYS_EXIT

 599         DISABLE_INTERRUPTS(CLBR_ANY)    # make sure we don't miss an interrupt

 600                                         # setting need_resched or sigpending

 601                                         # between sampling and the iret

 602         TRACE_IRQS_OFF

 603         movl TI_flags(%ebp), %ecx

 604         andl $_TIF_WORK_MASK, %ecx      # is there any work to be done other

 605                                         # than syscall tracing?

 606         jz restore_all

 607         testb $_TIF_NEED_RESCHED, %cl

 608         jnz work_resched

 

先來看system_call函數的整體,基本可以分成四個部分:

1. syscall_call 之前

2. syscall_call

3. syscall_call處理之後,恢復現場並返回

4. 過程中有可能會跳轉到syscall_exit_work部分做點其他的事情

 

下面就分成這四部分分別來看。

1. syscall_call之前:

  491         RING0_INT_FRAME  #將當前的EIP和ESP指針指向內核空間,程序將正式進入內核態運行

 492         ASM_CLAC

 493         pushl_cfi %eax   

#將EAX寄存器的值,也就是中斷調用時傳遞過來的系統調用號保存到當前進程的內核態棧上

 494         SAVE_ALL         

#將所有寄存器的值在內核態棧上保存,也就是所謂的保存現場

 495         GET_THREAD_INFO(%ebp) 

#GET_THREAD_INFO的定義爲:

#define GET_THREAD_INFO(reg) \

 _ASM_MOV PER_CPU_VAR(kernel_stack),reg ; \

 _ASM_SUB $(THREAD_SIZE-KERNEL_STACK_OFFSET),reg ;

  #這段彙編將當前進程的進程描述符中的進程信息結構的地址保存到寄存器EBP中

 496   

 497         testl $_TIF_WORK_SYSCALL_ENTRY,TI_flags(%ebp)

 498         jnz syscall_trace_entry

# EBP中存有當前進程的進程信息結構首地址,這個檢查是看當前進程的進程狀態標記是否設置了,

# 如果設置了,則會跳轉到syscall_trace_entry函數處去執行,不是中斷處理分析的關鍵部分,

#這裏不去進一步分析,僅僅假設標誌沒有設置,程序繼續順序往下執行。

 499         cmpl $(NR_syscalls), %eax

 500         jae syscall_badsys

# 檢查傳入的系統調用號是否合法,如果不是合法的系統調用號,則調用syscall_badsys去做錯誤處理

 

2. syscall_call 就一句代碼:call *sys_call_table(,%eax,4),就是按照eax中指定的向量值,跳轉到相應的入口函數

 

3. syscall_call之後的邏輯被分成了三段,我們也分別來看。

    - syscall_after_call部分,只有一句代碼:movl %eax,PT_EAX(%esp),把被調用的系統調用函數的返回值EAX存放到棧上指定的位置,因爲後面的處理可能還會改變EAX的值,所以先在棧上保存一下函數返回值。

  - syscall_exit部分 部分的處理有些複雜,代碼如下:

 506         LOCKDEP_SYS_EXIT

 507         DISABLE_INTERRUPTS(CLBR_ANY)    # make sure we don't miss an interrupt

 508                                         # setting need_resched or sigpending

 509                                         # between sampling and the iret

 510         TRACE_IRQS_OFF

 511         movl TI_flags(%ebp), %ecx

 512         testl $_TIF_ALLWORK_MASK, %ecx  # current->work

 513         jne syscall_exit_work

    這部分是一個進程切換的時機,內核會在這裏檢查一下有沒有額外的工作要做,額外工作的具體分析留在後面分析,這裏先假設沒有發生任何跳轉。這裏首先關閉中斷,在這段時間內不再相應外部中斷。註釋部分也特別強調了,在iret之前,不要忘記重新打開中斷。

 

  - restore_all部分,顧名思義,就是現場恢復處理,用TRACE_IRQS_IRET重設了上面用TRACE_IRQS_OFF關閉的中斷處理標誌。然後調用了RESTORE_REGS恢復了用save_all保存的所有寄存器的值,最後調用INTERRUPT_RETURN 從中斷處理中返回,程序將回到用戶態進程繼續執行。(518到529行是對某特殊情況的特殊處理,暫時略過)

 

4. 其他處理部分 syscall_exit_work上面說了,是一次進程切換的時機,來看看他的代碼:

 656 syscall_exit_work:

 657         testl $_TIF_WORK_SYSCALL_EXIT, %ecx

 658         jz work_pending

 659         TRACE_IRQS_ON

 660         ENABLE_INTERRUPTS(CLBR_ANY)     # could let syscall_trace_leave() call

 661                                         # schedule() instead

 662         movl %esp, %eax

 663         call syscall_trace_leave

 664         jmp resume_userspace

 665 END(syscall_exit_work)

 

 345 ENTRY(resume_userspace)

 346         LOCKDEP_SYS_EXIT

 347         DISABLE_INTERRUPTS(CLBR_ANY)    # make sure we don't miss an interrupt

 348                                         # setting need_resched or sigpending

 349                                         # between sampling and the iret

 350         TRACE_IRQS_OFF

 351         movl TI_flags(%ebp), %ecx

 352         andl $_TIF_WORK_MASK, %ecx      # is there any work to be done on

 353                                         # int/exception return?

 354         jne work_pending

 355         jmp restore_all

 

 593 work_pending:

 594         testb $_TIF_NEED_RESCHED, %cl

 595         jz work_notifysig

 596 work_resched:

 597         call schedule

 598         LOCKDEP_SYS_EXIT

 599         DISABLE_INTERRUPTS(CLBR_ANY)    # make sure we don't miss an interrupt

 600                                         # setting need_resched or sigpending

 601                                         # between sampling and the iret

 602         TRACE_IRQS_OFF

 603         movl TI_flags(%ebp), %ecx

 604         andl $_TIF_WORK_MASK, %ecx      # is there any work to be done other

 605                                         # than syscall tracing?

 606         jz restore_all

 607         testb $_TIF_NEED_RESCHED, %cl

 608         jnz work_resched

 609

 610 work_notifysig:                         # deal with pending signals and

 611                                         # notify-resume requests

 612 #ifdef CONFIG_VM86

 613         testl $X86_EFLAGS_VM, PT_EFLAGS(%esp)

 614         movl %esp, %eax

 615         jne work_notifysig_v86          # returning to kernel-space or

 616                                         # vm86-space

 617 1:

 618 #else

 619         movl %esp, %eax

 620 #endif

 621         TRACE_IRQS_ON

 622         ENABLE_INTERRUPTS(CLBR_NONE)

 623         movb PT_CS(%esp), %bl

 624         andb $SEGMENT_RPL_MASK, %bl

 625         cmpb $USER_RPL, %bl

 626         jb resume_kernel

 627         xorl %edx, %edx

 628         call do_notify_resume

 629         jmp resume_userspace

 630

 631 #ifdef CONFIG_VM86

 632         ALIGN

 633 work_notifysig_v86:

 634         pushl_cfi %ecx                  # save ti_flags for do_notify_resume

 635         call save_v86_state             # %eax contains pt_regs pointer

 636         popl_cfi %ecx

 637         movl %eax, %esp

 638         jmp 1b

 639 #endif

 640 END(work_pending)

 

    牽扯到的代碼還不止這些,總結起來,這裏面會檢查一些進程標誌,如果滿足條件,就會調用work_pending過程,而在work_pending處理中,會先檢查有沒有發送給該進程的還沒處理的信號,如果有則調用work_notifysig去處理這些信號,比如我們在正在運行的終端程序上按Ctrl+C,則會產生一箇中斷信號,這個信號就是在這個時機被處理的。處理完自己的信號,內核還會調用一次call schedule去進行進程的調度,看是否需要轉去運行優先級更高的進程,如果有,則會進行進程的切換。

    在這段程序裏,我們能看到很多處DISABLE_INTERRUPTS和ENABLE_INTERRUPTS 來處理中斷標誌,這是需要特別小心的地方,一定要確保每一個分支上中斷標誌都被正確的關閉和打開。

 

總結:

    一個系統調用的過程還是相當複雜的,完成請求的功能的代碼只需要call *sys_call_table(,%eax,4),然後給他傳遞合適的參數並接收返回值就行了,但是實際上每一次系統調用,內核還做了大量的其他操作,比如現場的保存和恢復,進程切換的檢查和執行,進程信號的處理,對外部中斷標誌的處理等等。這些額外的工作並不是用戶請求的系統調用功能所必須的,但確是現代操作系統完成多任務處理和進程調度所必須的。

    

    通過本文的分析,我們對Linux的系統調用的理解應該可以更加具體化了一些。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章