上一篇博文我們用匯編的方式實現了對系統調用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的系統調用的理解應該可以更加具體化了一些。