Linux(内核剖析):13---系统调用的实现与解析

Posted 董哥的黑板报

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Linux(内核剖析):13---系统调用的实现与解析相关的知识,希望对你有一定的参考价值。

一、系统调用概述
  • 系统调用在用户空间进程和硬件设备之间添加了一个中间层
  • 该层主要作用有三个:
    • 第一, 为用户空间提供了一种硬件的抽象接口。举例来说,当需要读写文件的时候,应用程序就可以不去管磁盘类型和介质,甚至不用去管文件所在的文件系统到底是哪种类型
    • 第二,系统调用保证了系统的稳定和安全。作为硬件设备和应用程序之间的中间人,内核可以基于权限、用户类型和其他一些规则对需要进行的访问进行裁决。举例来说,这样可以避免应用程序不正确地使用硬件设备,窃取其他进程的资源,或做出其他危害系统的事情
    • 第三,在前面介绍进程的文章中曾说过,每个进程都运行在虚拟系统中,而在用户空间和系统的其余部分提供这样一层公共接口,也是出于这种考虑。如果应用程序可以随意访问硬件而内核又对此一无所知的话,几乎就没法实现多任务和虚拟内存,当然也不可能实现良好的稳定性和安全性
  • 在Linux中,系统调用是用户空间访问内核的唯一手段;除异常和陷入外,它们是内核唯一的合法入口。实际上,其他的像设备文件和/proc之类的方式,最终也还是要通过系统调用进行访问的。而有趣的是,Linux提供的系统调用却比大部分操作系统都少得多
二、API、POSIX和C库
  • 一般情况下,应用程序通过在用户空间实现的应用编程接口(API)而不是直接通过系统调用来编程。下图给出了POSIX、API、C库以及系统调用之间的关系

POSIX

  • 在Unix中,最流行的应用编程接口是基于POSIX标准的。从纯技术的角度看,POSIX是由IEEE的一组标准组成,其目标是提供一套大体上基于Unix的可移植操作系统标准。在应用场合,Linux尽力与POSIX和SUSv3兼容
  • POSIX是说明API和系统调用之间关系的一个极好例子。在大多数Unix系统上,根据POSIX定义的API函数和系统调用之间有着直接关系。实际上,POSIX标准就是仿照早期Unix系统的接口建立的。另一方面,许多操作系统,像微软的Windows,尽管是非Unix系统,也提供了与POS1X 兼容的库

C库

  • Linux的系统调用像大多数Unix系统一样,作为C库的一部分提供。C库实现了Unix系统的主要API,包括标准C库函数和系统调用接口。所有的C程序都可以使用C库,而由于C 语言本身的特点,其他语言也吋以很方便地把它们封装起来使用。此外,C 库提供了 PO SIX的绝大部分API
  • 关于Unix的接口设计有一句格言“提供机制而不是策略”。换句话说,Unix的系统调用抽象出了用于完成某种确定的目的的函数。至于这些函数怎么用完全不需要内核去关心
三、系统调用的一些性质
  • 访问系统调用(Linux中常称作syscall),通常通过C 库中定义的函数调用来进行
  • 它们通常都需要定义零个、一个或几个参数(输 入)而且可能产生一些副作用,例如,写某个文件或向给定的指针拷贝数据等
  • 系统调用还会通过一个long类型的返回值来表示成功或者错误
    • 通常,但也不绝对,用一个负的返回值来表明错误。
    • 返回一个0值通 常 (当然仍不是绝对的)表明成功
    • 系统调用在出现错误的时候C 库会把错误码写入errno全局变量。通过调用perror()库函数,可以把该变量翻译成用户可以理解的错误字符串
  • 系统调用的性能:
    • Linux系统调用比其他许多操作系统执行得要快。Linux很短的上下文切换时间是一个重要原因,进出内核都被优化得简洁高效。另外一个原因是系统调用处理程序和每个系统调用本身也都非常简洁

系统调用的一般格式

  • 例如getpid()系统调用在内核中实现的形式如下:
SYSCALL_DEFINE0(getpid)
{
    return task_tgid_vnr(current); // returns current->tgid
} 
  • SYSCALL_DEFINE0只是一个宏,它定义了一个无参数的系统调用(因此这里为数字0,展开后的代码如下所示)
asmlinkage long sys_getpid(void)
  • asmlinkage:这是一个编译指令,通知编译器仅从栈中提取该函数的参数。所有的系统调用都需要这个限定词
  • long:函数的返回值。为了保证32位和64位系统的兼容,系统调用在用户空间和内核空间有不同的返回值类型,在用户空间为int,在内核空间为long
  • 所有的系统调用都以sys_开头

系统调用号

  • 概念:
    • 在Linux中,每个系统调用被赋予一个系统调用号。这样,通过这个独一无二的号就可以关联系统调用
    • 当用户空间的进程执行一个系统调用的时候,这个系统调用号就用来指明到底是要执行哪个系统调用;进程不会提及系统调用的名称
  • 特点:
    • 1.一旦分配就不能再有任何变更,否则编译好的应用程序就会崩溃
    • 2.如果一个系统调用被删除,它所占用的系统调用号也不允许被回收利用,如果将这个系统调用号回收给新的系统调用利用,则以前编译好的代码会调用这个新的系统调用,但事实上两个系统调用是不一致的
  • sys_ni_syscall()系统调用:
    • Linux有一个“未实现”系统调用sys_ni_syscall(),它除了返回-ENOSYS外不作任何其他工作,这个错误号就是专门针对无效的系统调用而设的
    • 常用的场景:如果一个系统调用被删除,或者变得不可用,就用这个函数“填补空缺”

sys_call_table(系统调用表)

  • 内核记录了系统调用表中的所有已注册过的系统调用的列表,存储在sys_call_table中
  • 每一种体系结构中,都明确定义了这个表,在x86-64中,它定义于arch/i386/kernel/syscall_64.c文件中
  • 这个表为每一个有效的系统调用指定了唯一的系统调用号
  • 以下代码来自Linux 2.6.22/arch/i386/kernel/syscall_table.S中
ENTRY(sys_call_table)
	.long sys_restart_syscall	/* 0 - old "setup()" system call, used for restarting */
	.long sys_exit
	.long sys_fork
	.long sys_read
	.long sys_write
	.long sys_open		/* 5 */
	.long sys_close
	.long sys_waitpid
	.long sys_creat
	.long sys_link
	.long sys_unlink	/* 10 */
	.long sys_execve
	.long sys_chdir
	.long sys_time
	.long sys_mknod
	.long sys_chmod		/* 15 */
	.long sys_lchown16
	.long sys_ni_syscall	/* old break syscall holder */
	.long sys_stat
	.long sys_lseek
	.long sys_getpid	/* 20 */
	.long sys_mount
	.long sys_oldumount
	.long sys_setuid16
	.long sys_getuid16
	.long sys_stime		/* 25 */
	.long sys_ptrace
	.long sys_alarm
	.long sys_fstat
	.long sys_pause
	.long sys_utime		/* 30 */
	.long sys_ni_syscall	/* old stty syscall holder */
	.long sys_ni_syscall	/* old gtty syscall holder */
	.long sys_access
	.long sys_nice
	.long sys_ni_syscall	/* 35 - old ftime syscall holder */
	.long sys_sync
	.long sys_kill
	.long sys_rename
	.long sys_mkdir
	.long sys_rmdir		/* 40 */
	.long sys_dup
	.long sys_pipe
	.long sys_times
	.long sys_ni_syscall	/* old prof syscall holder */
	.long sys_brk		/* 45 */
	.long sys_setgid16
	.long sys_getgid16
	.long sys_signal
	.long sys_geteuid16
	.long sys_getegid16	/* 50 */
	.long sys_acct
	.long sys_umount	/* recycled never used phys() */
	.long sys_ni_syscall	/* old lock syscall holder */
	.long sys_ioctl
	.long sys_fcntl		/* 55 */
	.long sys_ni_syscall	/* old mpx syscall holder */
	.long sys_setpgid
	.long sys_ni_syscall	/* old ulimit syscall holder */
	.long sys_olduname
	.long sys_umask		/* 60 */
	.long sys_chroot
	.long sys_ustat
	.long sys_dup2
	.long sys_getppid
	.long sys_getpgrp	/* 65 */
	.long sys_setsid
	.long sys_sigaction
	.long sys_sgetmask
	.long sys_ssetmask
	.long sys_setreuid16	/* 70 */
	.long sys_setregid16
	.long sys_sigsuspend
	.long sys_sigpending
	.long sys_sethostname
	.long sys_setrlimit	/* 75 */
	.long sys_old_getrlimit
	.long sys_getrusage
	.long sys_gettimeofday
	.long sys_settimeofday
	.long sys_getgroups16	/* 80 */
	.long sys_setgroups16
	.long old_select
	.long sys_symlink
	.long sys_lstat
	.long sys_readlink	/* 85 */
	.long sys_uselib
	.long sys_swapon
	.long sys_reboot
	.long old_readdir
	.long old_mmap		/* 90 */
	.long sys_munmap
	.long sys_truncate
	.long sys_ftruncate
	.long sys_fchmod
	.long sys_fchown16	/* 95 */
	.long sys_getpriority
	.long sys_setpriority
	.long sys_ni_syscall	/* old profil syscall holder */
	.long sys_statfs
	.long sys_fstatfs	/* 100 */
	.long sys_ioperm
	.long sys_socketcall
	.long sys_syslog
	.long sys_setitimer
	.long sys_getitimer	/* 105 */
	.long sys_newstat
	.long sys_newlstat
	.long sys_newfstat
	.long sys_uname
	.long sys_iopl		/* 110 */
	.long sys_vhangup
	.long sys_ni_syscall	/* old "idle" system call */
	.long sys_vm86old
	.long sys_wait4
	.long sys_swapoff	/* 115 */
	.long sys_sysinfo
	.long sys_ipc
	.long sys_fsync
	.long sys_sigreturn
	.long sys_clone		/* 120 */
	.long sys_setdomainname
	.long sys_newuname
	.long sys_modify_ldt
	.long sys_adjtimex
	.long sys_mprotect	/* 125 */
	.long sys_sigprocmask
	.long sys_ni_syscall	/* old "create_module" */
	.long sys_init_module
	.long sys_delete_module
	.long sys_ni_syscall	/* 130:	old "get_kernel_syms" */
	.long sys_quotactl
	.long sys_getpgid
	.long sys_fchdir
	.long sys_bdflush
	.long sys_sysfs		/* 135 */
	.long sys_personality
	.long sys_ni_syscall	/* reserved for afs_syscall */
	.long sys_setfsuid16
	.long sys_setfsgid16
	.long sys_llseek	/* 140 */
	.long sys_getdents
	.long sys_select
	.long sys_flock
	.long sys_msync
	.long sys_readv		/* 145 */
	.long sys_writev
	.long sys_getsid
	.long sys_fdatasync
	.long sys_sysctl
	.long sys_mlock		/* 150 */
	.long sys_munlock
	.long sys_mlockall
	.long sys_munlockall
	.long sys_sched_setparam
	.long sys_sched_getparam   /* 155 */
	.long sys_sched_setscheduler
	.long sys_sched_getscheduler
	.long sys_sched_yield
	.long sys_sched_get_priority_max
	.long sys_sched_get_priority_min  /* 160 */
	.long sys_sched_rr_get_interval
	.long sys_nanosleep
	.long sys_mremap
	.long sys_setresuid16
	.long sys_getresuid16	/* 165 */
	.long sys_vm86
	.long sys_ni_syscall	/* Old sys_query_module */
	.long sys_poll
	.long sys_nfsservctl
	.long sys_setresgid16	/* 170 */
	.long sys_getresgid16
	.long sys_prctl
	.long sys_rt_sigreturn
	.long sys_rt_sigaction
	.long sys_rt_sigprocmask	/* 175 */
	.long sys_rt_sigpending
	.long sys_rt_sigtimedwait
	.long sys_rt_sigqueueinfo
	.long sys_rt_sigsuspend
	.long sys_pread64	/* 180 */
	.long sys_pwrite64
	.long sys_chown16
	.long sys_getcwd
	.long sys_capget
	.long sys_capset	/* 185 */
	.long sys_sigaltstack
	.long sys_sendfile
	.long sys_ni_syscall	/* reserved for streams1 */
	.long sys_ni_syscall	/* reserved for streams2 */
	.long sys_vfork		/* 190 */
	.long sys_getrlimit
	.long sys_mmap2
	.long sys_truncate64
	.long sys_ftruncate64
	.long sys_stat64	/* 195 */
	.long sys_lstat64
	.long sys_fstat64
	.long sys_lchown
	.long sys_getuid
	.long sys_getgid	/* 200 */
	.long sys_geteuid
	.long sys_getegid
	.long sys_setreuid
	.long sys_setregid
	.long sys_getgroups	/* 205 */
	.long sys_setgroups
	.long sys_fchown
	.long sys_setresuid
	.long sys_getresuid
	.long sys_setresgid	/* 210 */
	.long sys_getresgid
	.long sys_chown
	.long sys_setuid
	.long sys_setgid
	.long sys_setfsuid	/* 215 */
	.long sys_setfsgid
	.long sys_pivot_root
	.long sys_mincore
	.long sys_madvise
	.long sys_getdents64	/* 220 */
	.long sys_fcntl64
	.long sys_ni_syscall	/* reserved for TUX */
	.long sys_ni_syscall
	.long sys_gettid
	.long sys_readahead	/* 225 */
	.long sys_setxattr
	.long sys_lsetxattr
	.long sys_fsetxattr
	.long sys_getxattr
	.long sys_lgetxattr	/* 230 */
	.long sys_fgetxattr
	.long sys_listxattr
	.long sys_llistxattr
	.long sys_flistxattr
	.long sys_removexattr	/* 235 */
	.long sys_lremovexattr
	.long sys_fremovexattr
	.long sys_tkill
	.long sys_sendfile64
	.long sys_futex		/* 240 */
	.long sys_sched_setaffinity
	.long sys_sched_getaffinity
	.long sys_set_thread_area
	.long sys_get_thread_area
	.long sys_io_setup	/* 245 */
	.long sys_io_destroy
	.long sys_io_getevents
	.long sys_io_submit
	.long sys_io_cancel
	.long sys_fadvise64	/* 250 */
	.long sys_ni_syscall
	.long sys_exit_group
	.long sys_lookup_dcookie
	.long sys_epoll_create
	.long sys_epoll_ctl	/* 255 */
	.long sys_epoll_wait
 	.long sys_remap_file_pages
 	.long sys_set_tid_address
 	.long sys_timer_create
 	.long sys_timer_settime		/* 260 */
 	.long sys_timer_gettime
 	.long sys_timer_getoverrun
 	.long sys_timer_delete
 	.long sys_clock_settime
 	.long sys_clock_gettime		/* 265 */
 	.long sys_clock_getres
 	.long sys_clock_nanosleep
	.long sys_statfs64
	.long sys_fstatfs64
	.long sys_tgkill	/* 270 */
	.long sys_utimes
 	.long sys_fadvise64_64
	.long sys_ni_syscall	/* sys_vserver */
	.long sys_mbind
	.long sys_get_mempolicy
	.long sys_set_mempolicy
	.long sys_mq_open
	.long sys_mq_unlink
	.long sys_mq_timedsend
	.long sys_mq_timedreceive	/* 280 */
	.long sys_mq_notify
	.long sys_mq_getsetattr
	.long sys_kexec_load
	.long sys_waitid
	.long sys_ni_syscall		/* 285 */ /* available */
	.long sys_add_key
	.long sys_request_key
	.long sys_keyctl
	.long sys_ioprio_set
	.long sys_ioprio_get		/* 290 */
	.long sys_inotify_init
	.long sys_inotify_add_watch
	.long sys_inotify_rm_watch
	.long sys_migrate_pages
	.long sys_openat		/* 295 */
	.long sys_mkdirat
	.long sys_mknodat
	.long sys_fchownat
	.long sys_futimesat
	.long sys_fstatat64		/* 300 */
	.long sys_unlinkat
	.long sys_renameat
	.long sys_linkat
	.long sys_symlinkat
	.long sys_readlinkat		/* 305 */
	.long sys_fchmodat
	.long sys_faccessat
	.long sys_pselect6
	.long sys_ppoll
	.long sys_unshare		/* 310 */
	.long sys_set_robust_list
	.long sys_get_robust_list
	.long sys_splice
	.long sys_sync_file_range
	.long sys_tee			/* 315 */
	.long sys_vmsplice
	.long sys_move_pages
	.long sys_getcpu
	.long sys_epoll_pwait
	.long sys_utimensat		/* 320 */
	.long sys_signalfd
	.long sys_timerfd
	.long sys_eventfd
四、系统调用处理程序
  • 用户空间的程序无法直接执行内核代码。它们不能直接调用内核空间中的函数,因为内核驻留在受保护的地址空间上。如果进程可以直接在内核的地址空间上读写的话,系统的安全性和稳定性将不复存在
  • 所以,应用程序应该以某种方式通知系统,告诉内核自己需要执行一个系统调用,希望系统切换到内核态,这样内核就可以代表应用程序在内核空间执行系统调用

软中断机制(int $0x80指令、system_call())

  • 通知内核的机制是靠软中断实现的:通过引发一个异常来促使系统切换到内核态去执行异常处理程序。此时的异常处理程序实际上就是系统调用处理程序
  • 软中断的触发:
    • 在x86系统上预定义的软终断是中断号128,通过int $0x80指令触发该中断。这条指令会触发一个异常导致系统切换到内核态并执行第128号异常处理程序,而该程序正是系统调用处理程序。这个处理程序就是system_call()。它与硬件体系结紧密相关 ,x86-64的系统上在entry_64.S文件中用汇编语言编写
  • 最近,x86处理器增加了一条叫做sysenter的指令。与int终断指令相比,这条指令提供了更快、更专业的陷入内核执行系统调用的方式。对这条指令的支持很快被加入内核。且不管系统调用处理程序被如何调用,用户空间引起异常或陷入内核就是一个重要的概念

指定系统调用号

  • 因为所有的系统调用陷入内核的方式都一样,所以仅仅是陷入内核空间是不够的。因此必须把系统调用号一并传给内核
  • 在x86上,系统调用号是通过eax寄存器传递给内核的。在陷入内核之前,用户空间就把相应系统调用所对应的号放入eax中。这样系统调用处理程序一旦运行,就可以从eax中得到数据。其他体系结构上的实现也都类似
  • 系统调用号的有效性检测:system_call()函数通过将给定的系统调用号与NR_syscalls做比较来检査其有效性。如果它大于或者等于NR_syscalls,该函数就返回-ENOSYS。否则,就执行相应的系统调用:
call *sys_call_table(,%rax,8)
  • 由于系统调用表中的表项是以64位(8字节)类型存放的,所以内核需要将给定的系统调用号乘以4,然后用所得的结果在该表中查询其位置。在x86-32系统上,代码很类似,只是用4代替8,参见下图

参数、返回值传递

  • 参数传递:
    • 除了系统调用号以外,大部分系统调用都还需要一些外部的参数输入。所以,在发生陷入的时候,应该把这些参数从用户空间传给内核
    • 最简单的办法就是像传递系统调用号一样,把这些参数也存放在寄存器里。在x86-32系统上,ebx、ecx、edx、esi和edi按照顺序存放前五个参数。 需要六个或六个以上参数的情况不多见,此时,应该用一个单独的寄存器存放指向所有这些参数 在用户空间地址的指针
  • 返回值传递:
    • 给用户空间的返回值也通过寄存器传递。在x86系统上,它存放在eax寄存器中。
五、系统调用的参数验证
  • 系统调用必须仔细检査它们所有的参数是否合法有效。系统调用在内核空间执行,如果任由用户将不合法的输入传递给内核,那么系统的安全和稳定将面临极大的考验
  • 举例来说,与文件I/O相关的系统调用必须检査文件描述符是否有效。与进程相关的函数必须检查提供的PID是否有效。必须检査每个参数,保证它们不但合法有效,而且正确。进程不应当让内核去访问那些它无权访问的资源

检查用户提供的指针

  • 最重要的一种检査就是检査用户提供的指针是否有效。试想,如果一个进程可以给内核传递指针而又无须检査,那么它就可以给出一个它根本就没有访问权限的指针,哄骗内核去为它拷贝本不允许它访问的数据,如原本属于其他进程的数据或者不可读的映射数据
  • 在接收一个用户空间的指针之前,内核必须验证:
    • 1.指针指向的内存区域属于用户空间。进程决不能哄骗内核去读内核空间的数据
    • 2.指针指向的内存区域在进程的地址空间里。进程决不能哄骗内核去读其他进程的数据
    • 3.如果是读,该内核应被标记为可读;如果是写,该内核应被标记为可写;如果是可执行,该内存被标记为可执行。进程决不能绕过内核访问限制

copy_to_user()、copy_from_user()

  • 内核提供了两个方法来完成必须的检杳和内核空间与用户空间之间数据的来回拷贝。注意,内核无论何时都不能轻率地接受来自用户空间的指针!这两个方法中必须经常有一个被使用
  • copy_to_user()
    • 为了向用户空间写入数据,内核提供了copy_to_user()
    • 三个参数:
      • 第一个参数是进程空间中的目的内存地址
      • 第二个是内核空间内的源地址
      • 最后一个参数是需要拷贝的数据长度(字节数)
  • copy_from_user()
    • 为了从用户空间读取数据,内核提供了copy_from_user()
    • 三个参数:
      • 与copy_to_user()类似,该函数把第二个参数指定的位置上的数据拷贝到第一个参数指定的位置上,拷贝的数据长度由第三个参数决定
  • 两个函数的返回值:
    • 失败,返回没能完成拷贝的数据的字节数。系统调用返回标准-EFAULT
    • 成功,返回0
  • 两个函数的阻塞:注意,这两个函数都有可能引起阻塞。当包含用户数据的页被换出到硬盘上而不是在物理内存上的时候,这种情况就会发生。此时,进程就会休眠,直到缺页处理程序将该页从硬盘重新换回物理内存
  • 演示案例:以一个既用了copy_from_user()又用了copy_to_user()的系统调用作例子进行考察。 这个系统调用silly_copy()毫无实际用处,它从第一个参数里拷贝数据到第二个参数。这种用途让人无法理解,它毫无必要地让内核空间作为中转站,把用户空间的数据从一个位置复制到另外 一个位置。但它却能演示出上述函数的用法

检测合法权限(suser()、capable()、capability.h)

  • 最后一项检查针对是否有合法权限
  • suser():
    • 在老版本的Linux内核中,需要超级用户权限的系统调用才可以通过调用suser()函数这个标准动作来完成检査。这个函数只能检査用户是否为超级用户
    • 现在它已经被一个更细粒度的“权能”机制代替(见下)
  • capable():
    • 新的系统允许检査针对特定资源的特殊权限。调用者可以使用capable函数来检査是否有权能对指定的资源进行操作
    • 返回值:
      • 返回非0:调用者有权进行操作
      • 返回0:表示无权
  • 演示案例:
    • capable(CAP_SYS_NICE) 可以检査调用者是否有权改变其他进程的nice值
    • 默认情况下,属于超级用户的进程拥有所有权利而非超级用户没有任何权利
    • 例如,下面是reboot()系统调用,注意,第一步是如何确保调用进程具有CAP_SYS_REBOOT权能。如果那样一个条件语句被删除,任何进程都可以启动系统了

  • capability.h:这个文件包含一份所有这些权能和其对应的权限的列表。例如下面的部分代码来自于linux 2.6.22/include/linux/capaility.h
#define CAP_SYS_RESOURCE     24

/* Allow manipulation of system clock */
/* Allow irix_stime on mips */
/* Allow setting the real-time clock */

#define CAP_SYS_TIME         25

/* Allow configuration of tty devices */
/* Allow vhangup() of tty */

#define CAP_SYS_TTY_CONFIG   26

/* Allow the privileged aspects of mknod() */

#define CAP_MKNOD            27

/* Allow taking of leases on files */

#define CAP_LEASE            28

#define CAP_AUDIT_WRITE      29

#define CAP_AUDIT_CONTROL    30
六、系统调用上下文
  • 在介绍进程管理的文章中我们说过,内核在执行系统调用的时候处于进程上下文。current指针指向当前任务,即引发系统调用的那个进程
  • 在进程上下文中,内核可以休眠(比如在系统调用阻塞或显式调schedule()的时候)并且可以被抢占。这两点都很重要:
    • 首先,能够休眠说明系统调用可以使用内核提供的绝大部分功能。在后面介绍中断的文章中可以看到,休眠的能力会给内核编程带来极大便利
    • 在进程上下文中能够被抢占其实表明,像用户空间内的进程一样,当前的进程同样可以被其他进程抢占。因为新的进程可以使用相同的系统调用,所以必须小心,保证该系统调用是可重入的。当然,这也是在对称多处理中必须同样关心的问题
  • 当系统调用返回的时候,控制权仍然在system_call()中,它最终会负责切换到用户空间,并让用户进程继续执行下去

绑定新的系统调用(asm/unistd.h)

  • 当编写完一个系统调用后,把它注册成一个正式的系统调用是件琐碎的工作:
    • 1.首先,在系统调用表的最后加入一个表项。每种支持该系统调用的硬件体系都必须做这样的工作(大部分的系统调用都针对所有的体系结构)。从0开始算起,系统调用在该表中的位置就是它的系统调用号。如第10个系统调用分配到的系统调用号为9
    • 2.对于所支持的各种体系结构,系统调用号都必须定义于<asm/unistd.h>中
    • 3.系统调用必须被编译进内核映象(不能被编译成模块)。这只要把它放进kernel/下的一个相关文件中就可以了,比如sys.c,它包含了各种各样的系统调用
  • 第一步:下面我们以通过一个虚构的系统调用foo()来观察这些步骤。先把sys_foo加入到系统调用表的末尾。该表位于/arch/i386/kernel/syscall_table.S中文件中(上面也有介绍)
    • 如果没有明确指定编号,则系统会自动分配调用号,例如下面分配的就是338
    • 另外,不同的体系结构不需要对应相同的系统调用号。系统调用号是专属于体系结构ABI(应用程序二进制接口)的部分。通常,你需要让系统调用适应每种体系结构。你可以注意一下,每隔5个表项就加入一个调用号注释的习惯,这样可以方便你查找

  • 第二步:加下来,把系统调用号加入到<asm/unistd.h>的最后,其格式如下

  • 第三步:我们来实现foo()系统调用。无论何种配置,该系统调用都必须编译到核心的内核映象中去,所以在这个例子中我们把它放进kernel/sys.c文件中。你也可以将其放到与其功能联系最紧密的代码中去,假如它的功能与调度相关,那么你也可以把它放到kernel/ sched.c中去

从用户空间访问系统调用

  • 通常,系统调用靠C库支持。用户程序通过包含标准头文件并和C库链接,就可以使用系统调用(或者调用库函数,再由库函数实际调用)。但如果你仅仅写出系统调用,glibc库恐怕并不提供支持
  • 值得庆幸的是,Linux本身提供了一组宏,用于直接对系统调用进行访问。它会设置好寄存器并调用陷入指令。这些宏是_syscalln,其中n的范围0到6,代表需要传递给系统调用的参数个数,这是由于该宏必须了解到底有多少参数按照什么次序压入寄存器
  • 对于每个宏来说,都有2 + 2*n 个参数:
    • 第一个参数对应着系统调用的返回值类型
    • 第二个参数是系统调用的名称
    • 再以后是按照系统调用参数的顺序排列的每个参数的类型和名称
  • 举个例子,open()系统调用的定义是:
long open(const char *filename,int flags,int mode);
  • 而不靠库支持,直接调用此系统调用的宏的形式为,这样应用程序就可以直接使用open()了:
#define NR_open 5
_syscall3(long,open,const char*,filename,int,flags,int,mode);
  • _NR_open在<asm/unistd.h>中定义,是系统调用号。该宏会被扩展成为内嵌汇编的C函数;由汇编语言执行前面内容中所讨论的步骤,将系统调用号和参数压入寄存器并触发软中断来陷入内核。调用open()系统调用直接把上面的宏放置在应用程序中就可以了
  • 现在我们写一个宏来使用上面自己编写的foo()系统调用,然后再写出测试代码:

为什么不通过系统调用的方式实现

  • 虽然实现一个新的系统调用很容易,但是不提倡这么做。通常都会有更好的办法用来代替新建一个系统调用以作实现。让我们看看采用系统调用作为实现方式的利弊和替代的方法
  • 建立一个新的系统调用的好处:
    • 系统调用创建容易且使用方便
    • Linux系统调用的高性能显而易见
  • 缺点:
    • 你需要一个系统调用号,而这需要一个内核在处于开发版本的时候由官方分配给你
    • 系统调用被加入稳定内核后就被固化了,为了避免应用程序的崩溃,它的接口不允许做改动
    • 需要将系统调用分别注册到每个需要支持的体系结构中去
    • 在脚本中不容易调用系统调用,也不能从文件系统直接访问系统调用
    • 由于你需要系统调用号,因此在主内核树之外是很难维护和使用系统调用的 
    • 如果仅仅进行简单的信息交换,系统调用就大材小用了
  • 替代方法:
    • 实现一个设备节点,并对此实现read()和write()。使用 ioctl()对特定的设置进行操作或者对 特定的信息进行检索
    • 像信号量这样的某些接口,可以用文件描述符来表示,因此也就可以按上述方式对其进行操作
    • 把增加的信息作为一个文件放在sysfs的合适位置
  • 总结:
    • 低于许多接口来说,系统调用都被视为正确的解决之道。但Linux系统尽董避免每出现一种新的抽象就简单的加入一个新的系统调用。这使得它的系统调用接口简洁得令人叹为观止,也就避免了许多后悔和反对意见(系统调用再也不被使用或支持)。新系统调用增添频率很低也反映出Linux是一个相对较为稳定并且功能已经较为完善的操作系统

以上是关于Linux(内核剖析):13---系统调用的实现与解析的主要内容,如果未能解决你的问题,请参考以下文章

Linux(内核剖析):21---中断之中断上下文中断处理机制的实现/proc/interrupts

Linux(内核剖析):07---进程调度总体概述(多任务系统策略时间片)

Linux 系统调用 —— fork 内核源码剖析

Linux驱动子系统剖析 | Linux设备与驱动分离思想的代表作——platform总线模型

Linux驱动子系统剖析 | Linux设备与驱动分离思想的代表作——platform总线模型

Linux内核gpio驱动子系统剖析 | 01 - gpio子系统整体实现架构