linux系统调用实现机制详解(内核4.14.4)

简介:

linux系统调用实现机制详解(内核4.14.4)前言

1.1     linux系统调用介绍

linux内核中设置了一组用于实现系统功能的子程序,称为系统调用。和普通库函数调用相似,只是系统调用由操作系统核心提供,运行于核心态,而普通的函数调用由函数库或用户自己提供,运行于用户态。

在Linux中,每个系统调用被赋予一个系统调用号。通过这个独一无二的号就可以关联系统调用。当用户空间的进程执行一个系统调用的时候,这个系统调用号就被用来指明到底是要执行哪个系统调用。

系统调用号一旦分配就不能再有任何变更,否则编译好的应用程序就会崩溃。Linux有一个“未实现”系统调用sys_ni_syscall(),它除了返回一ENOSYS外不做任何其他工作,这个错误号就是专门针对无效的系统调用而设的。

结合具体源码来看下实现机制。

1.2     系统调用表和调用号

具体号子分配在文件arch/x86/entry/syscalls/syscall_64.tbl中定义,如下:

0       common  read                    sys_read

1       common  write                   sys_write

2       common  open                    sys_open

3       common  close                   sys_close

………

30      common  shmat                   sys_shmat

31      common  shmctl                  sys_shmctl

32      common  dup                     sys_dup

33      common  dup2                    sys_dup2

34      common  pause                   sys_pause

35      common  nanosleep               sys_nanosleep

36      common  getitimer               sys_getitimer

37      common  alarm                   sys_alarm

38      common  setitimer               sys_setitimer

39      common  getpid                  sys_getpid

40      common  sendfile                sys_sendfile64

41      common  socket                  sys_socket

…….

也可以在arch/x86/include/generated/uapi/asm/unistd_64.h文件中查找到系统调用号。

#define __NR_read 0

#define __NR_write 1

#define __NR_open 2

#define __NR_close 3

#define __NR_stat 4

#define __NR_fstat 5

#define __NR_lstat 6

#define __NR_poll 7

#define __NR_lseek 8

……

 

1.2     系统调用声明

在文件(include/linux/syscalls.h)中定义了系统调用函数声明,函数声明中的asmlinkage限定词,这用于通知编译器仅从栈中提取该函数的参数。所有的系统调用都需要这个限定词。例如系统调用getpid()在内核中被定义成sys_ getpid。这是Linux中所有系统调用都应该遵守的命名规则.

如下:

            asmlinkage long sys_kill(pid_t pid, int sig);

1.3     系统调用实现

不同的系统调用实现在不同的文件中,例如sys_read 系统调用实现在fs/read_write.c文件中,sys_socket定义在net/socket.c中。

例如sys_socket的原型如下:

SYSCALL_DEFINE3(socket, int, family, int, type, int, protocol) 

            其中3表示有3个参数,用于解析参数时候使用。

查看宏SYSCALL_DEFINE3的定义,定义也在include/linux/syscalls.h中,如下:

#define SYSCALL_DEFINE1(name, ...) SYSCALL_DEFINEx(1, _##name, __VA_ARGS__)

#define SYSCALL_DEFINE2(name, ...) SYSCALL_DEFINEx(2, _##name, __VA_ARGS__)

#define SYSCALL_DEFINE3(name, ...) SYSCALL_DEFINEx(3, _##name, __VA_ARGS__)

#define SYSCALL_DEFINE4(name, ...) SYSCALL_DEFINEx(4, _##name, __VA_ARGS__)

#define SYSCALL_DEFINE5(name, ...) SYSCALL_DEFINEx(5, _##name, __VA_ARGS__)

#define SYSCALL_DEFINE6(name, ...) SYSCALL_DEFINEx(6, _##name, __VA_ARGS__)

 

#define SYSCALL_DEFINE_MAXARGS  6

 

#define SYSCALL_DEFINEx(x, sname, ...)                          \

        SYSCALL_METADATA(sname, x, __VA_ARGS__)                 \

        __SYSCALL_DEFINEx(x, sname, __VA_ARGS__)

 

#define __PROTECT(...) asmlinkage_protect(__VA_ARGS__)

#define __SYSCALL_DEFINEx(x, name, ...)                                 \

        asmlinkage long sys##name(__MAP(x,__SC_DECL,__VA_ARGS__))       \

                __attribute__((alias(__stringify(SyS##name))));         \

        static inline long SYSC##name(__MAP(x,__SC_DECL,__VA_ARGS__));  \

        asmlinkage long SyS##name(__MAP(x,__SC_LONG,__VA_ARGS__));      \

        asmlinkage long SyS##name(__MAP(x,__SC_LONG,__VA_ARGS__))       \

        {                                                               \

                long ret = SYSC##name(__MAP(x,__SC_CAST,__VA_ARGS__));  \

                __MAP(x,__SC_TEST,__VA_ARGS__);                         \

                __PROTECT(x, ret,__MAP(x,__SC_ARGS,__VA_ARGS__));       \

                return ret;                                             \

        }                                                               \

        static inline long SYSC##name(__MAP(x,__SC_DECL,__VA_ARGS__))

            我们看到SYSCALL_DEFINE3指向SYSCALL_DEFINEx,而SYSCALL_DEFINEx指向__SYSCALL_DEFINEx,在__SYSCALL_DEFINEx宏中调用真正的原型,如sys_socket(其也定义在syscalls.h)。

            所以SYSCALL_DEFINE3(socket, int, family, int, type, int, protocol)  就是sys_socket函数。具体实现后续会在linux协议栈中进行介绍。

            置于为什么会这么复杂,因为linux发展过程中难免碰到各种漏洞,有些则是因为修改漏洞需要,例如CVE-2009-0029漏洞

https://bugzilla.redhat.com/show_bug.cgi?id=479969

 

1.4     系统调用总接口

之前在arch/x86/kernel/entry_64.S中实现了system_call的系统调用总接口。根据系统参数参数号来执行具体的系统调用。

现在所有socket相关的系统调用,都会使用sys_socketcall的系统调用,如下socketcall的代码片段,根据参数进入switch…case…判断操作码,跳转至对应的系统接口:

SYSCALL_DEFINE2(socketcall, int, call, unsigned long __user *, args)

{

……

        switch (call) {

        case SYS_SOCKET:

                err = sys_socket(a0, a1, a[2]);

                break;

        case SYS_BIND:

                err = sys_bind(a0, (struct sockaddr __user *)a1, a[2]);

                break;

        case SYS_CONNECT:

                err = sys_connect(a0, (struct sockaddr __user *)a1, a[2]);

                break;

        case SYS_LISTEN:

                err = sys_listen(a0, a1);

                break;

        case SYS_ACCEPT:

                err = sys_accept4(a0, (struct sockaddr __user *)a1,

                                  (int __user *)a[2], 0);

                break;

        case SYS_GETSOCKNAME:

                err =

                    sys_getsockname(a0, (struct sockaddr __user *)a1,

                                    (int __user *)a[2]);

                break;

        case SYS_GETPEERNAME:

                err =

                    sys_getpeername(a0, (struct sockaddr __user *)a1,

                                    (int __user *)a[2]);

            这里的变量定义在文件include/uapi/linux/net.h中,如下

#define SYS_SOCKET      1               /* sys_socket(2)                */

#define SYS_BIND        2               /* sys_bind(2)                  */

#define SYS_CONNECT     3               /* sys_connect(2)               */

#define SYS_LISTEN      4               /* sys_listen(2)                */

#define SYS_ACCEPT      5               /* sys_accept(2)                */

#define SYS_GETSOCKNAME 6               /* sys_getsockname(2)           */

#define SYS_GETPEERNAME 7               /* sys_getpeername(2)           */

#define SYS_SOCKETPAIR  8               /* sys_socketpair(2)            */

#define SYS_SEND        9               /* sys_send(2)                  */

#define SYS_RECV        10              /* sys_recv(2)                  */

#define SYS_SENDTO      11              /* sys_sendto(2)                */

#define SYS_RECVFROM    12              /* sys_recvfrom(2)              */

#define SYS_SHUTDOWN    13              /* sys_shutdown(2)              */

#define SYS_SETSOCKOPT  14              /* sys_setsockopt(2)            */

#define SYS_GETSOCKOPT  15              /* sys_getsockopt(2)            */

#define SYS_SENDMSG     16              /* sys_sendmsg(2)               */

#define SYS_RECVMSG     17              /* sys_recvmsg(2)               */

#define SYS_ACCEPT4     18              /* sys_accept4(2)               */

#define SYS_RECVMMSG    19              /* sys_recvmmsg(2)              */

#define SYS_SENDMMSG    20              /* sys_sendmmsg(2)              */

 

1.5     系统调用流程

整体的系统调用的过程如下,由应用程序调用C库提供的API函数,该API实现函数会调用内核的统一入口函数,具体到系统调用。

86340e27c4c2b0fab0bd0667eec165877b893583

            图中逻辑为常用的系统调用。Socket相关的系统调用入口函数为sys_socketcall

如果出现错误,错误码定义在文件:

include/uapi/asm-generic/errno-base.h中。

            具体看下节中的socket系统调用。

1.6     socket具体实现流程例子

Socket 的API函数 socket ()(该函数定义在/usr/include/sys/socket.h文件中

extern int socket (int __domain, int __type, int __protocol) __THROW;

            glibc库对socket系统调用进行了封装。位于文件

sysdeps/unix/sysv/linux/i386/socket.S

            其中定义了#  define __socket socket,调用__socket就是调用socket函数。

            该函数是对socket函数的封装,代码中主要逻辑是调用sys_socketcall系统调用,参数为socket的调用号,然后用socketcall函数来进行调用socket。

整体逻辑看上方图。

可以编译一个使用socket系统调用的应用程序,进行gdb调试,运行到socket时候进行反汇编显示如下,下面标红的一行是移动0x29到eax,而0x29就是41,就是socket系统调用的系统号:

(gdb) disass socket

Dump of assembler code for function socket:

=> 0x00007ffff78f85a0 <+0>: mov    $0x29,%eax

   0x00007ffff78f85a5 <+5>:  syscall

   0x00007ffff78f85a7 <+7>:  cmp    $0xfffffffffffff001,%rax

   0x00007ffff78f85ad <+13>: jae    0x7ffff78f85b0 <socket+16>

   0x00007ffff78f85af <+15>: retq  

   0x00007ffff78f85b0 <+16>: mov    0x2bb8c1(%rip),%rcx        # 0x7ffff7bb3e78

   0x00007ffff78f85b7 <+23>: neg    %eax

   0x00007ffff78f85b9 <+25>: mov    %eax,%fs:(%rcx)

   0x00007ffff78f85bc <+28>: or     $0xffffffffffffffff,%rax

   0x00007ffff78f85c0 <+32>: retq  

End of assembler dump.

 

1.7     系统调用跟踪

编写一个代码如下:

#include <unistd.h>

#include <fcntl.h>

int main(){

    int handle,bytes;

    void * ptr;

    handle=open("tmp/test.txt",O_RDONLY);

    close(handle);

    return 0;

}

编译:gcc -o hell hello.c

使用strace命令进行跟踪:

# strace -o log.txt ./hello

打开log.txt可以看到如下内容:

execve("./hello", ["./hello"], [/* 22 vars */]) = 0

brk(NULL)                               = 0xa1e000

access("/etc/ld.so.nohwcap", F_OK)      = -1 ENOENT (No such file or directory)

mmap(NULL, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7fcc310ad000

access("/etc/ld.so.preload", R_OK)      = -1 ENOENT (No such file or directory)

open("/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 3

fstat(3, {st_mode=S_IFREG|0644, st_size=71985, ...}) = 0

mmap(NULL, 71985, PROT_READ, MAP_PRIVATE, 3, 0) = 0x7fcc3109b000

close(3)                                = 0

access("/etc/ld.so.nohwcap", F_OK)      = -1 ENOENT (No such file or directory)

open("/lib/x86_64-linux-gnu/libc.so.6", O_RDONLY|O_CLOEXEC) = 3

read(3, "\177ELF\2\1\1\3\0\0\0\0\0\0\0\0\3\0>\0\1\0\0\0P\t\2\0\0\0\0\0"..., 832) = 832

fstat(3, {st_mode=S_IFREG|0755, st_size=1868984, ...}) = 0

mmap(NULL, 3971488, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_DENYWRITE, 3, 0) = 0x7fcc30ac0000

mprotect(0x7fcc30c80000, 2097152, PROT_NONE) = 0

mmap(0x7fcc30e80000, 24576, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x1c0000) = 0x7fcc30e80000

mmap(0x7fcc30e86000, 14752, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0) = 0x7fcc30e86000

close(3)                                = 0

mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7fcc3109a000

mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7fcc31099000

mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7fcc31098000

arch_prctl(ARCH_SET_FS, 0x7fcc31099700) = 0

mprotect(0x7fcc30e80000, 16384, PROT_READ) = 0

mprotect(0x600000, 4096, PROT_READ)     = 0

mprotect(0x7fcc310af000, 4096, PROT_READ) = 0

munmap(0x7fcc3109b000, 71985)           = 0

open("tmp/test.txt", O_RDONLY)          = -1 ENOENT (No such file or directory)

close(-1)                               = -1 EBADF (Bad file descriptor)

exit_group(0)                           = ?

+++ exited with 0 +++

注意到最后几行就是我们程序的系统中实现的系统调用。

看到open返回的是-1(ENOENT),因为在tmp目录中不存在test.txt文件。而且我们程序中没有对文件打开与否进行判断,导致出错了应用也不知道,只能通过strace来进行跟踪。

这个也是在(文件include/uapi/asm-generic/errno-base.h中定义的

#define ENOENT           2      /* No such file or directory */)

我们再来看一下之前的一大堆调用,这是为支持我们写的程序运行,系统进行的进程创建、内存映射等等工作。我们在代码中只写了几行,但是系统却在编译链接以及加载到内存的时候做了非常多的事情。

所以,在开发应用程序的时候还会觉得麻烦么?最麻烦的事情底层其实都已经帮我们做好了,实在是找不到借口和老板说应用程序开发很麻烦了哦。

            创建一个tmp/test.txt文件,再调用发现最后三行如下:

open("tmp/test.txt", O_RDONLY)          = 3

close(3)                                = 0

exit_group(0)                           = ?

            说明打开正确了。后续如果要诊断程序的系统调用问题可以使用strace函数。

 

1.8     小结

由于网络上关于系统的调用的介绍代码引用比较分散切老旧对新步入的同学造成不同的费解,因此总结此文。

本文基于内核4.14.14代码介绍了linux系统调用,将系统调用表、调用号所在源码位置标出,同时梳理的系统调用的整个执行逻辑。最后剖析了socket用户接口和sys_socket系统调用之间的关系。针对函数细节没有进行深入,这个未来会有专项课题。

            如有错误欢迎指正,祝大家玩的愉快。

目录
相关文章
|
15天前
|
算法 Linux 调度
深入理解Linux内核调度器:从基础到优化####
本文旨在通过剖析Linux操作系统的心脏——内核调度器,为读者揭开其高效管理CPU资源的神秘面纱。不同于传统的摘要概述,本文将直接以一段精简代码片段作为引子,展示一个简化版的任务调度逻辑,随后逐步深入,详细探讨Linux内核调度器的工作原理、关键数据结构、调度算法演变以及性能调优策略,旨在为开发者与系统管理员提供一份实用的技术指南。 ####
54 4
|
4天前
|
缓存 网络协议 Linux
深入探索Linux操作系统的内核优化策略####
本文旨在探讨Linux操作系统内核的优化方法,通过分析当前主流的几种内核优化技术,结合具体案例,阐述如何有效提升系统性能与稳定性。文章首先概述了Linux内核的基本结构,随后详细解析了内核优化的必要性及常用手段,包括编译优化、内核参数调整、内存管理优化等,最后通过实例展示了这些优化技巧在实际场景中的应用效果,为读者提供了一套实用的Linux内核优化指南。 ####
16 1
|
9天前
|
算法 Linux 开发者
Linux内核中的锁机制:保障并发控制的艺术####
本文深入探讨了Linux操作系统内核中实现的多种锁机制,包括自旋锁、互斥锁、读写锁等,旨在揭示这些同步原语如何高效地解决资源竞争问题,保证系统的稳定性和性能。通过分析不同锁机制的工作原理及应用场景,本文为开发者提供了在高并发环境下进行有效并发控制的实用指南。 ####
|
17天前
|
缓存 资源调度 安全
深入探索Linux操作系统的心脏——内核配置与优化####
本文作为一篇技术性深度解析文章,旨在引领读者踏上一场揭秘Linux内核配置与优化的奇妙之旅。不同于传统的摘要概述,本文将以实战为导向,直接跳入核心内容,探讨如何通过精细调整内核参数来提升系统性能、增强安全性及实现资源高效利用。从基础概念到高级技巧,逐步揭示那些隐藏在命令行背后的强大功能,为系统管理员和高级用户打开一扇通往极致性能与定制化体验的大门。 --- ###
47 9
|
16天前
|
缓存 负载均衡 Linux
深入理解Linux内核调度器
本文探讨了Linux操作系统核心组件之一——内核调度器的工作原理和设计哲学。不同于常规的技术文章,本摘要旨在提供一种全新的视角来审视Linux内核的调度机制,通过分析其对系统性能的影响以及在多核处理器环境下的表现,揭示调度器如何平衡公平性和效率。文章进一步讨论了完全公平调度器(CFS)的设计细节,包括它如何处理不同优先级的任务、如何进行负载均衡以及它是如何适应现代多核架构的挑战。此外,本文还简要概述了Linux调度器的未来发展方向,包括对实时任务支持的改进和对异构计算环境的适应性。
37 6
|
17天前
|
缓存 Linux 开发者
Linux内核中的并发控制机制:深入理解与应用####
【10月更文挑战第21天】 本文旨在为读者提供一个全面的指南,探讨Linux操作系统中用于实现多线程和进程间同步的关键技术——并发控制机制。通过剖析互斥锁、自旋锁、读写锁等核心概念及其在实际场景中的应用,本文将帮助开发者更好地理解和运用这些工具来构建高效且稳定的应用程序。 ####
35 5
|
17天前
|
算法 Unix Linux
深入理解Linux内核调度器:原理与优化
本文探讨了Linux操作系统的心脏——内核调度器(Scheduler)的工作原理,以及如何通过参数调整和代码优化来提高系统性能。不同于常规摘要仅概述内容,本摘要旨在激发读者对Linux内核调度机制深层次运作的兴趣,并简要介绍文章将覆盖的关键话题,如调度算法、实时性增强及节能策略等。
|
17天前
|
Java Linux Android开发
深入探索Android系统架构:从Linux内核到应用层
本文将带领读者深入了解Android操作系统的复杂架构,从其基于Linux的内核到丰富多彩的应用层。我们将探讨Android的各个关键组件,包括硬件抽象层(HAL)、运行时环境、以及核心库等,揭示它们如何协同工作以支持广泛的设备和应用。通过本文,您将对Android系统的工作原理有一个全面的认识,理解其如何平衡开放性与安全性,以及如何在多样化的设备上提供一致的用户体验。
|
17天前
|
缓存 运维 网络协议
深入Linux内核架构:操作系统的核心奥秘
深入Linux内核架构:操作系统的核心奥秘
36 2
|
20天前
|
机器学习/深度学习 负载均衡 算法
深入探索Linux内核调度机制的优化策略###
本文旨在为读者揭开Linux操作系统中至关重要的一环——CPU调度机制的神秘面纱。通过深入浅出地解析其工作原理,并探讨一系列创新优化策略,本文不仅增强了技术爱好者的理论知识,更为系统管理员和软件开发者提供了实用的性能调优指南,旨在促进系统的高效运行与资源利用最大化。 ###