Linux内核入门

Linux是一套免费使用和自由传播的类Unix操作系统,它最先用于基于x86系列CPU的计算机上。这个系统是由世界各地的成千上万的程序员设计和实现的。其目的是建立不受任何商品化软件的版权制约的、全世界都能自由使用的Unix兼容产品。

我们不去介绍操作系统的历史了,也不管操作系统这门学科上对操作系统的分类了,闲话少说,Linux操作系统只是一个非常新的操作系统。它不拘泥于某种特
定的操作系统类型,从内核上讲,它是一个分时操作系统,但又具备实时操作系统的特性;从体系上讲,它是一个单内核操作系统,但又具备模块化的微内核特征;
它支持各种网际协议,所以又是一个网络操作系统;它支持大规模集群、网格计算,甚至现在还有人在它上面架设云计算、云存储等环境,所以它又是一个分布式操
作系统……

不管怎么说,Linux的开源性质决定了世界上各式各样的人可以按照自己的需求去发展它、完善它,于是乎Linux就具备了高性能、高可用、可扩展、可移植等多种特性。

当今IT业界,小到嵌入式、手机、PC机,大到大规模集群、网格、云,都能看到Linux的身影。本人写一系列疯狂Linux内核博文的目的,就是让广大的中国同胞了解到这一伟大操作系统的内部真实面貌,让你感受到它为什么伟大。

Linux是一个操作系统,也是一个软件。既然一个软件,肯定就要遵循所有软件的特点,那就是其本质=算法+数据结构+文档。本文就从他的架构入手,一步步进入其内部。

1 Linux体系结构

上面就是一幅我认为还比较完美的Linux架构图。从图中我们可以看见,用户使用到的应用程序,最终会通过中断的形式访问内核。详细一点描述就是:
应用程序向内核发出系统调用这一特殊中断,随后包含该程序的进程又用户态进入内核态,就可以访问内核提供的各式各样的函数和数据结构了。更具体的描述我们
随后再细说,先来介绍一些概念吧:

“文件”和“进程”是Linux内核中的两个最基本实体和中心概念,Linux系统的所有操作都是以这两者为基础的。整个系统核心由以下五个部分组成:
① 虚拟文件系统:文件管理和磁盘高速缓存管理(节点和空间管理)
② I/O设备管理:块设备驱动(随机存取设备)、原始设备(raw设备,字符设备,裸设备)
③ 进程控制:进程的调度、同步和通信
④ 存储管理:在主存与CPU二级存储之间对程序进行搬迁
⑤ 网际协议栈:实现各式各样的网络协议。

2 一般程序的执行

一个进程在执行系统调用exec期间(exec("命令名",参数)),就把可执行文件装入本进程的三个区域中:
      ·正文区:对应可执行文件的正文段
      ·数据区:对应可执行文件的数据标识段
      ·堆栈区:新建立的进程工作区

堆栈是一个重要的概念,其主要用于传递参数,保护现场,存放返回地址以及为局部动态变量提供存储区。我们后面的博文将会重点讨论这个,因为堆栈这个东西太重要了。

进程在内核态下运行时的工作区为内核栈,在用户态下运行时的工作区为用户栈。内核栈和用户栈不能交叉使用。

来,我们来看一个程序,用户在标准终端上敲入:copy oldfile newfile。此处,oldfile 是一个现存文件名,而 newfile 是一个新文件名。有:(其中,变量version是初始化数据;数组buffer是未初始化的数据)

#include <fcntl.h>
char buffer[2048];
int version=1;

main(int argc, char *argv[])   /*系统引用main时需要提供argc作为表argv中的 */
{                              /*参数,并且对数组argv的每个成员赋初值,   */
      int fdold, fdnew;        /*对照命令:argv[0]指向字符串copy;        */
      if(argc != 3)            /*argv[1]指向字符串oldfile;               */
      {                        /*argv[2]指向字符串newfile;               */
            printf(“need 2 arguments for copy program/n”);
            exit(1);
      }
      fdold = open(argv[1], O_RDONLY);  /* 打开源文件只读 */
      if (fdold == -1)
      {
            printf(“cannot open file %s/n”, argv[1]);
            exit(1);
      }
      fdnew = creat(argv[2], 0666);     /* 创建可为所有用户读写的目标文件 */
      if(fdnew == -1)
      {
            printf(“cannot create file %s/n”, argv[2]);
            exit(1);
      }
      copy(fdold, fdnew);
      exit(0);
}
copy(int old, int new)
{
      int count;
      while((count = read(old, buffer, sizeof(buffer))) > 0)
            write(new, buffer, count);
}

当main被调用时,main中的参数argc和argv、变量fdold、fdnew及相关函数地址信息就会被压栈;并且无论何时,遇到下一个函
数(本例中是 copy
函数),其参数和变量以及相关地址也会被压栈:(假设程序不进入三个IF程序段中的堆栈过程,其实IF后也有个压栈的过程,我们省略了,但千万别以为没
有)

我们看到:Linux的进程工作在两种状态——内核态(kernel mode)和用户态(user mode)。
所以,Linux系统的内核栈和用户栈是分开的。用户栈保存的是程序中的一般函数和系统调用函数相关信息,对用户是可见的;
内核栈保存的是内核中的函数或数据,如getblk函数等,对用户是透明的。

那么,程序什么时候使用用户栈,什么时候使用内核栈呢?

对,系统调用。也就是执行printf、open、read、write执行C语言库函数时,其最终会用到对应的系统调用,如sys_open、sys_read等。这时候就切换到内核栈。

1 Linux的堆栈切换

我们针对80x86来讨论,其实Linux只在四个地方用了它的堆栈段(由ss+esp指向其栈底地址):
• 系统引导初始化临时实模式下使用的堆栈
• 进入保护模式后提供内核程序始化使用的堆栈,该堆栈也是后来进程0使用的用户态堆栈
• 每个进程通过系统调用,执行内核程序时使用的堆栈,称之为进程的内核态堆栈,每个进程都有自己独立的内核态堆栈
• 进程在用户态执行的堆栈,位于进程逻辑地址空间近末端处

下面简单的介绍一下与普通进程相关的两个堆栈

每个进程都有两个堆栈,分别用于用户态和内核态程序的执行,我们称为用户态堆栈和内核态堆栈。

除了处于不同CPU特权级中,这两个堆栈之间的主要区别还在于任务的内核态堆栈很小,在后面进程管理专题中我们可以看到所保存的数据最多不能超过8096个字节,而进程的用户态堆栈却可以在用户的4GB空间的最底部,并向上延伸。

在用户态运行时,即你看到的那些代码的时候,每个进程(除了进程0和进程1)有自己的4GB地址空间,当一个进程刚被创建时,它的用户态堆栈指针被设置在其地址空间的靠近末端部分,应用程序在用户态下运行时就一直使用这个堆栈,实际物理地址内存则由CPU分页机制确定。

在内核态运行时,每个任务有其自己的内核态堆栈,用于任务在内核代码中执行期间,即执行系统调用以后。其所在的线性地址中位置由该进程TSS段中ss0和esp0 两个字段指定,这两个值来自哪儿呢?

我们的“内存管理”专题中将提到,针对80X86体系,Linux只象征性地使用分段技术,即只是用代码段和数据段。而CPU中的SS寄存器是指向堆栈段
的,但是Linux没有使用专门的堆栈段,而是将数据段中的一部分作为堆栈段。所以,当数据段中的CPL字段为3时,SS寄存器就指向该用户数据段中的用
户栈;如果数据段中的CPL字段为0时,它就指向内核数据段中的内核栈。注意!这一点很重要,特别是我们以后讲解进程切换的时候,这一个知识你不知道的
话,那些内容会让你抓狂的。

除了用户数据段、用户代码段、内核数据段、内核代码段这4个段以外,Linux还使用了其它几个专门的段,下面我们专门来探讨,如图:在单处理器系
统中只有一个GDT,而在多处理器系统中每个CPU对应一个GDT。所有的GDT都存放在cpu_gdt_table
数组中,而所有GDT(当初始化gdtr
寄存器时使用)的地址和它们的大小存放在cpu_gdt_descr数组中,这些符号都在文件arch/i386/kernel/head.S中被定义。

我们再把这个知识扩展一下,80x86体系的286以后出现了一个新段,叫做任务状态段(TSS),主要用来保存处理器中各个寄存器的内容。Linux为
每个处理器都有一个相应的TSS相关的数据结构,每个TSS相应的线性地址空间都是内核数据段相应线性地址空间的一个小子集。所有的任务状态段都顺序地存
放在init_tss数组中;值得特别说明的是,第n个CPU的TSS描述符的Base字段指向init_tss数组的第n个元素。G(粒度)标志被清
0,而Limit字段置为0xeb,因为TSS段是236字节长。Type字段置为9或11(可用的32位TSS),且DPL置为0,因为不允许用户态下
的进程访问TSS段。

好了,回到刚才的问题,我们谈到了,用户进程要想访问内核提供的数据结构和函数时,须进行切换,即由用户态转向内核态,那么内核栈的地址从何而来?

于是乎,当进程由用户态进入内核态时,必发生中断,因为内核态的CPL优先级高,所以要进行栈的切换。那么就会读tr寄存器以访问该进程(现在还是用户
态)的TSS段。随后用TSS中内核态堆栈段ss0和栈指针esp0装载SS和esp寄存器,这样就实现了用户栈到内核栈的切换了。同时,内核用一组
mov指令保存所有寄存器到内核态堆栈上,这也包括用户态中ss和esp这对寄存器的内容。

中断或异常处理结束时,CPU控制单元执行iret命令,重新读取栈中的寄存器内容来更新各个CPU寄存器,以重新开始执行用户态进程,此时将会根据栈中。

这里还要强调一下,内核栈的地址只有一个(如果是多CPU架构,则每个CPU一个),其ss和esp保存在TSS结构中,不允许用户态进程访问,Linux描述TSS的格式的数据结构是tss_struct:

struct tss_struct {
    unsigned short    back_link,__blh;
    unsigned long    esp0;
    unsigned short    ss0,__ss0h;
    unsigned long    esp1;
    unsigned short    ss1,__ss1h;    /* ss1 is used to cache MSR_IA32_SYSENTER_CS */
    unsigned long    esp2;
    unsigned short    ss2,__ss2h;
    unsigned long    __cr3;
    unsigned long    eip;
    unsigned long    eflags;
    unsigned long    eax,ecx,edx,ebx;
    unsigned long    esp;
    unsigned long    ebp;
    unsigned long    esi;
    unsigned long    edi;
    unsigned short    es, __esh;
    unsigned short    cs, __csh;
    unsigned short    ss, __ssh;
    unsigned short    ds, __dsh;
    unsigned short    fs, __fsh;
    unsigned short    gs, __gsh;
    unsigned short    ldt, __ldth;
    unsigned short    trace, io_bitmap_base;
    /*
     * The extra 1 is there because the CPU will access an
     * additional byte beyond the end of the IO permission
     * bitmap. The extra byte must be all 1 bits, and must
     * be within the limit.
     */
    unsigned long    io_bitmap[IO_BITMAP_LONGS + 1];
    /*
     * Cache the current maximum and the last task that used the bitmap:
     */
    unsigned long io_bitmap_max;
    struct thread_struct *io_bitmap_owner;
    /*
     * pads the TSS to be cacheline-aligned (size is 0x100)
     */
    unsigned long __cacheline_filler[35];
    /*
     * .. and then another 0x100 bytes for emergency kernel stack
     */
    unsigned long stack[64];
} __attribute__((packed));

这就是TSS段的全部内容,不多。每次切换时,内核都更新TSS的某些字段以便想要的CPU控制单元可以安全地检索到它需要的信息,这也是Linux安全性的体现之一。所以,TSS只是反映了CPU上当前进程的特性级别,没有必要运行的进程保留TSS。

linux2.4之前的内核有进程最大数的限制,受限制的原因是,每一个进程都有自已的TSS和LDT,而TSS(任务描述符)和LDT(私有描述
符)必须放在GDT中,GDT最大只能存放8192个描述符,除掉系统用的12描述符之外,最大进程数=(8192-12)/2, 总共4090个进程。

从Linux2.4以后,全部进程使用同一个TSS,准确的说是,每个CPU一个TSS,在同一个CPU上的进程使用同一个TSS。TSS的定义在asm-i386/processer.h中,定义如下:

extern struct tss_struct init_tss[NR_CPUS];

在start_kernel()->trap_init()->cpu_init()初始化并加载TSS:

void __init cpu_init (void)
{
 int nr = smp_processor_id();    //获取当前cpu

struct tss_struct * t = &init_tss[nr]; //当前cpu使用的tss

t->esp0 = current->thread.esp0;            //把TSS中esp0更新为当前进程的esp0
 set_tss_desc(nr,t);
 gdt_table[__TSS(nr)].b &= 0xfffffdff;
 load_TR(nr);                                              //加载TSS
 load_LDT(&init_mm.context);                //加载LDT

}

我们知道,任务切换(硬切换)需要用到TSS来保存全部寄存器(2.4以前使用jmp来实现切换),中断发生时也需要从TSS中读取ring0的esp0,那么,进程使用相同的TSS,任务切换怎么办?

其实2.4以后不再使用硬切换,而是使用软切换,寄存器不再保存在TSS中了,而是保存在task->thread中,只用TSS的esp0和IO许可位图,所以,在进程切换过程中,只需要更新TSS中的esp0、io_bitmap,代码在sched.c中:

schedule()->switch_to()->__switch_to(),

void fastcall __switch_to(struct task_struct *prev_p, struct task_struct *next_p)
{
 struct thread_struct *prev = &prev_p->thread,
     *next = &next_p->thread;
 struct tss_struct *tss = init_tss + smp_processor_id(); //当前cpu的TSS

/*
  * Reload esp0, LDT and the page table pointer:
  */
ttss->esp0 = next->esp0; //用下一个进程的esp0更新tss->esp0

//拷贝下一个进程的io_bitmap到tss->io_bitmap

if (prev->ioperm || next->ioperm) {
  if (next->ioperm) {
   /*
    * 4 cachelines copy ... not good, but not that
    * bad either. Anyone got something better?
    * This only affects processes which use ioperm().
    * [Putting the TSSs into 4k-tlb mapped regions
    * and playing VM tricks to switch the IO bitmap
    * is not really acceptable.]
    */
   memcpy(tss->io_bitmap, next->io_bitmap,
     IO_BITMAP_BYTES);
   tss->bitmap = IO_BITMAP_OFFSET;
  } else
   /*
    * a bitmap offset pointing outside of the TSS limit
    * causes a nicely controllable SIGSEGV if a process
    * tries to use a port IO instruction. The first
    * sys_ioperm() call sets up the bitmap properly.
    */
   tss->bitmap = INVALID_IO_BITMAP_OFFSET;
 }
}

2 80x86分段的总结

看晕了吧,我们还是来清理一下80x86段寄存器的知识,这些知识在Linux内核分析中是很重要的,前面已经提到了GDT,这里再把整个段寄存器的知识梳理一下,这样,刚才没看明白的同志应该就有些头绪了。

从80286模式开始,Intel微处理器以两种不同的方式执行地址转换,这两种方式分别称为实模式(real
mode)和保护模式(protected
mode)。一个逻辑地址由两部分组成:一个段标识符(注意,不是我们课堂上学到的什么“段基址”了哈,升级了!)和一个段内相对地址的偏移量。段标识符
是一个16位长的字段,称为段选择符(segment selector),而偏移量是一个32位长的字段。

为了快速方便地找到段选择符,处理器提供段寄存器,段寄存器的唯一目的是存放段选择符的地址(16位,千万要注意,这些段寄存器的内容已经不是什么段基址
了)。这些段寄存器称为cs, ss, ds, es,
fs和gs。尽管只有6个段寄存器,但程序可以把同一个段寄存器用于不同的目的,方法是先将其值保存在存储器中,用完后再恢复。

6个寄存器中3个有专门的用途:
cs——代码段寄存器,指向包含程序指令的段。
ss——栈段寄存器,指向包含当前程序栈的段。
ds——数据段寄存器,指向包含静态数据或者全局数据的段。

其它三个段寄存器作一般用途,可以指向任意的数据段。

cs寄存器还有一个很重要的功能:它含有一个两位的字段,用以指明CPU的当前特权级(Current PrivilegeLevel,CPL)。值为0代表最高优先级,而值为3代表最低优先级。Linux只用0级和3级,分别称之为内核态和用户态。

每个段由一个8字节的段描述符(Segment
Descriptor)表示(参见图),它描述了段的特征(千万要注意,不是段的地址)。段描述符放在全局描述符表(Global Descriptor
Table, GDT)或局部描述符表(Local Descriptor Table,
LDT)中。通常只定义一个GDT,而每个进程除了存放在GDT中的段之外如果还需要创建附加的段,就可以有自己的LDT。GDT在主存中的地址和大小存
放在gdtr处理器寄存器中,当前正被使用的LDT地址和大小放在ldtr处理器寄存器中。

其意义如下:
Base:包含段的首字节的线性地址
G:粒度标志G:如果该位清为0,段大小以字节为单位,否则以4096字节的倍数计。
Limit:存放段中最后一个内存单元的偏移量,从而决定段的长度。如果G被置为0,段的大小在1个字节到1MB之间变化;否则,将在4KB到4GB之间变化。
S:系统标志S,如果它被清0,则这是一个系统段,存储诸如局部描述符表这种关键的数据结构,否则它是一个普通的代码段或数据段。
Type:描述了段的类型特征和它的存取权限(请看表下面的描述)。
DPL:描述符特权级(Descriptor Privilege
Level)字段:用于限制对这个段的存取。它表示为访问这个段而要求的CPU最小的优先级。因此,DPL设为0的段只能当CPL为0时(即在内核态)才
是可访问的,而DPL设为3的段对任何CPL值都是可访问的。
P:SegmentPresent标志:等于0表示段当前不在主存中。Linux总是把这个标志(第 47位)设为1,因为它从来不把整个段交换到磁盘上去。
D或B:称为D或B的标志,取决于是代码段还是数据段。D或B的含义在两种情况下稍微有所区别,但是如果段偏移量的地址是32位长,就基本上把它置为1,如果这个偏移量是16位长,它被清0(更详细描述参见Intel使用手册)。
AVL:AVL标志可以由操作系统使用,但是被Linux忽略。

逻辑地址由16位段选择符和32位偏移量组成,段寄存器仅仅存放段选择符。CPU的分段单元执行以下操作(都是机械地转换,了解一下即可):
• 先检查段选择符的TI字段,以决定段描述符保存在哪一个描述符表中。TI字段指明描述符是在GDT中(在这种情况下,分段单元从gdtr寄存器中得到GDT的线性基地址)还是在激活的LDT中(在这种情况下,分段单元从ldtr寄存器中得到LDT的线性基地址)。
• 从段选择符的index字段计算段描述符的地址,index字段的值乘以8(一个段描述符的大小,其实就是屏蔽掉末尾那三位指示特权级的CPL和指示TI的字段),这个结果与gdtr或ldtr寄存器中的内容相加。
• 把逻辑地址的偏移量与段描述符Base字段的值相加就得到了线性地址。
 
请注意,CPU有一些与段寄存器相关的的寄存器叫做隐Cache,有些书上也叫不可编程寄存器,用来缓存段描述符。于是乎只有当段寄存器中选择子的内容被改变时才需要执行前两个操作。

3 Linux的指针

当对指向指令或者数据结构的指针进行保存时,内核根本不需要为其设置逻辑地址的段选择符,因为cs寄存器就含有当前的段选择符。例如,当内核调用一个函数
时,它执行一条call汇编语言指令,该指令仅指定它逻辑地址的偏移量部分,而段选择符不用设置,其隐含在cs寄存器中了。因为“在内核态执行”
的段只有一种,叫做代码段,由宏_KERNEL_CS定义,所以只要当CPU切换入内核态时足可以将__KERNEL_CS装载入cs。同样的道理也适用
于指向内核数据结构的指针(隐含地使用ds寄存器)以及指向用户数据结构的指针(内核显式地使用es寄存器)。

时间: 2025-01-07 01:10:36

Linux内核入门的相关文章

Linux 内核入门简介

作者 : 韩曙亮 转载请出名出处 : http://blog.csdn.net/shulianghan/article/details/38636827 一. Linux 内核简介 1. 内核功能简介 (1) 操作系统 和 内核 简介 操作系统 : -- 功能 : 完成基本功能 和 系统管理; -- 组成 : 内核(kernel), 设备驱动程序(driver), 启动引导程序(bootloader), 命令行(shell), 用户界面(UI), 文件系统(filesystem), 管理工具;

Unix/Linux环境C编程入门教程(12) openSUSECCPP以及Linux内核驱动开发环境搭建

1. openSUSE是一款优秀的linux. 2.选择默认虚拟机 3.选择稍后安装操作系统 4.选择linux  opensuse 5. 选择默认虚拟机名称 6.设置处理器为双核. 7.内存设置为2G 8. 选择网络地址转换 9.设置IO控制器 10. 选择默认磁盘类型 11.创建一个新的虚拟磁盘 12.设置磁盘大小 13.选择路径保存虚拟磁盘 14. 完成虚拟机创建 15.设置虚拟机 16.选择opensuse镜像 17.开启虚拟机 18.虚拟机启动 19.安装opensuse 20.安装程

linux内核编程入门--系统调用监控文件访问

参考的资料: hello world   https://www.cnblogs.com/bitor/p/9608725.html linux内核监控模块——系统调用的截获  https://www.cnblogs.com/lxw315/p/4773566.html 实现: 实验目的: 内核模块的编写:完成一个Linux/Windows内核/驱动模块的编写, 能够实现对文件访问的监控.或者对键盘设备.USB设备.网络设备. 蓝牙设备等的监控. 实验内容: 通过linux内核模块编程,写一个模块使

如何切入 Linux 内核源代码

Makefile不是Make Love 从前在学校,混了四年,没有学到任何东西,每天就是逃课,上网,玩游戏,睡觉.毕业的时候,人家跟我说Makefile我完全不知,但是一说Make Love我就来劲了,现在想来依然觉得丢人. 毫不夸张地说,Kconfig和Makefile是我们浏览内核代码时最为依仗的两个文件.基本上,Linux内核中每一个目录下边都会有一个 Kconfig文件和一个Makefile文件.对于一个希望能够在Linux内核的汪洋代码里看到一丝曙光的人来说,将它们放在怎么重要的地位都

Linux基础入门

第一节,linux系统简介 (一).Linux 为何物 Linux 也就是系统调用和内核那两层,当然直观的来看,我们使用的操作系统还包含一些在其上运行的应用程序,比如文本编辑器,浏览器,电子邮件. (二).Linux 与 Windows 到底有哪些不同 1.免费与收费 最新正版 Windows8.1 官方售价 ¥888: Linux 免费或少许费用 2. 软件与支持 Windows 平台:数量和质量的优势,不过大部分为收费软件:由微软官方提供重要支持和服务: Linux 平台:大都为开源自由软件

Linux内核经典书籍

1.<Linux内核设计与实现> 本书重在原理.适合入门的最佳图书.作者是为2.6内核加入了抢占的人,对调度部分非常精通,而调度是整个系统的核心,因此本书是很权威的. 2.<深入理解Linux内核> 此书比上一本多了些细节.是Linux内核黑客在推荐图书时的首选.写的比较简单易懂,适合刚刚接触LINUX内核的.此书图表很多,形象地给出了关键数据结构的定义,与<Linux内核源代码情景分析>相比,本书内容紧凑,不会一个问题讲解动辄上百页,有提纲挈领的功用,但是深度上要逊于

Linux内核学习总结

李泽源 原创作品 转载请注明出处 <Linux内核分析>MOOC课程:http://mooc.study.163.com/course/USTC-1000029000 [Linux内核学习总结] 幸福来得很突然,这门课就快结束了…… 是时候,总结下这段时间的坚持了,也给同样对Linux内核有兴趣的你一个指南. 在这门课的学习过程中,按照老师的要求,每次课后都写一篇博文,这是一个很好的学习方式.每当写这些文章的时候,总是要多看几遍视频,再查查相关的资料,才能勉强凑成一个完整的文档:同时也把自己学

如何参与Linux内核开发(转)

本文来源于linux内核代码的Document文件夹下的Hoto文件.Chinese translated version of Documentation/HOWTO If you have any comment or update to the content, please contact theoriginal document maintainer directly.  However, if you have a problemcommunicating in English yo

Linux 基础入门第一次实验笔记

第一节.实验介绍 本节主要介绍 Linux 的历史,Linux 与 Windows 的区别等入门知识.如果你已经有过充分的了解,可以跳过本节,直接进入下一个实验. 一.Linux 为何物 Linux 就是一个操作系统,就像你多少已经了解的 Windows(xp,7,8)和 Max OS ,至于操作系统是什么,就不用过多解释了,如果你学习过前面的入门课程,应该会有个基本概念了,这里简单介绍下操作系统在整个计算机系统中的角色. 我们的 Linux 也就是系统调用和内核那两层,当然直观的来看,我们使用