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的系统调用的理解应该可以更加具体化了一些。

猜你喜欢

转载自blog.csdn.net/yubo112002/article/details/82527124