Linux系统调用讲义

Linux系统调用讲义

Linux下系统调用的实现
Linux中的系统调用
Linux中怎样编译和定制内核
 

        

  • Linux下系统调用的实现
        

  1. Unix/Linux操作系统的体系结构及系统调用介绍
        

              

    1. 什么是操作系统和系统调用         

                  操作系统是从硬件抽象出来的虚拟机,在该虚拟机上用户可以运行应用程序。它负责直接与硬件交互,向用户程序提供公共服务,并使它们同硬件特性隔离。因为程序不应该依赖于下层的硬件,只有这样应用程序才能很方便的在各种不同的Unix系统之间移动。系统调用是Unix/Linux操作系统向用户程序提供支持的接口,通过这些接口应用程序向操作系统请求服务,控制转向操作系统,而操作系统在完成服务后,将控制和结果返回给用户程序。
               

              

    2.         

    3. Unix/Linux系统体系结构        

              一个Unix/Linux系统分为三个层次:用户、核心以及硬件。
              
                   其中系统调用是用户程序与核心间的边界,通过系统调用进程可由用户模式转入核心模式,在核心模式下完成一定的服务请求后在返回用户模式。

              

          系统调用接口看起来和C程序中的普通函数调用很相似,它们通常是通过库把这些函数调用映射成进入操作系统所需要的原语。

              

          这些操作原语只是提供一个基本功能集,而通过库对这些操作的引用和封装,可以形成丰富而且强大的系统调用库。这里体现了机制与策略相分离的编程思想——系统调用只是提供访问核心的基本机制,而策略是通过系统调用库来体现。

              

      例:execv, execl, execlv, opendir , readdir…

              

       

              

    4.         

    5. Unix/Linux运行模式,地址空间和上下文
                       

       

              

      运行模式(运行态):

              

          一种计算机硬件要运行Unix/Linux系统,至少需要提供两种运行模式:高优先级的核心模式和低优先级的用户模式。

              

          实际上许多计算机都有两种以上的执行模式。如:intel 80×86体系结构就有四层执行特权,内层特权最高。Unix只需要两层即可以了:核心运行在高优先级,称之为核心态;其它外围软件包括shell,编辑程序,Xwindow等等都是在低优先级运行,称之为用户态。之所以采取不同的执行模式主要原因时为了保护,由于用户进程在较低的特权级上运行,它们将不能意外或故意的破坏其它进程或内核。程序造成的破坏会被局部化而不影响系统中其它活动或者进程。当用户进程需要完成特权模式下才能完成的某些功能时,必须严格按照系统调用提供接口才能进入特权模式,然后执行调用所提供的有限功能。

              

          每种运行态都应该有自己的堆栈。在Linux中,分为用户栈和核心栈。用户栈包括在用户态执行时函数调用的参数、局部变量和其它数据结构。有些系统中专门为全局中断处理提供了中断栈,但是x86中并没有中断栈,中断在当前进程的核心栈中处理。

              

      地址空间:

              

          采用特权模式进行保护的根本目的是对地址空间的保护,用户进程不应该能够访问所有的地址空间:只有通过系统调用这种受严格限制的接口,进程才能进入核心态并访问到受保护的那一部分地址空间的数据,这一部分通常是留给操作系统使用。另外,进程与进程之间的地址空间也不应该随便互访。这样,就需要提供一种机制来在一片物理内存上实现同一进程不同地址空间上的保护,以及不同进程之间地址空间的保护。

              

          Unix/Linux中通过虚存管理机制很好的实现了这种保护,在虚存系统中,进程所使用的地址不直接对应物理的存储单元。每个进程都有自己的虚存空间,每个进程有自己的虚拟地址空间,对虚拟地址的引用通过地址转换机制转换成为物理地址的引用。正因为所有进程共享物理内存资源,所以必须通过一定的方法来保护这种共享资源,通过虚存系统很好的实现了这种保护:每个进程的地址空间通过地址转换机制映射到不同的物理存储页面上,这样就保证了进程只能访问自己的地址空间所对应的页面而不能访问或修改其它进程的地址空间对应的页面。

              

          虚拟地址空间分为两个部分:用户空间和系统空间。在用户模式下只能访问用户空间而在核心模式下可以访问系统空间和用户空间。系统空间在每个进程的虚拟地址空间中都是固定的,而且由于系统中只有一个内核实例在运行,因此所有进程都映射到单一内核地址空间。内核中维护全局数据结构和每个进程的一些对象信息,后者包括的信息使得内核可以访问任何进程的地址空间。通过地址转换机制进程可以直接访问当前进程的地址空间(通过MMU),而通过一些特殊的方法也可以访问到其它进程的地址空间。

              

          尽管所有进程都共享内核,但是系统空间是受保护的,进程在用户态无法访问。进程如果需要访问内核,则必须通过系统调用接口。进程调用一个系统调用时,通过执行一组特殊的指令(这个指令是与平台相关的,每种系统都提供了专门的trap命令,基于x86Linux中是使用int 指令)使系统进入内核态,并将控制权交给内核,由内核替代进程完成操作。当系统调用完成后,内核执行另一组特征指令将系统返回到用户态,控制权返回给进程。

              

      上下文:

              

          一个进程的上下文可以分为三个部分:用户级上下文、寄存器上下文以及系统级上下文。

              

          用户级上下文:正文、数据、用户栈以及共享存储区;

              

          寄存器上下文:程序寄存器(IP),即CPU将执行的下条指令地址,处理机状态寄存器(EFLAGS),栈指针,通用寄存器;

              

          系统级上下文:进程表项(proc结构)U区,在Linux中这两个部分被合成task_struct,区表及页表(mm_struct , vm_area_struct, pgd, pmd, pte),核心栈等。

              

          全部的上下文信息组成了一个进程的运行环境。当发生进程调度时,必须对全部上下文信息进行切换,新调度的进程才能运行。进程就是上下文的集合的一个抽象概念。

              

       

              

    6.         

    7. 系统调用的功能和分类
    8.     

    操作系统核心在运行期间的活动可以分为两个部分:上半部分(top half)和下半部分(bottom half),其中上半部分为应用程序提供系统调用或自陷的服务,是同步服务,由当前执行的进程引起,在当前进程上下文中执行并允许直接访问当前进程的数据结构;而下半部分则是由处理硬件中断的子程序,是属于异步活动,这些子程序的调用和执行与当前进程无关。上半部分允许被阻塞,因为这样阻塞的是当前进程;下半部分不允许被阻塞,因为阻塞下半部分会引起阻塞一个无辜的进程甚至整个核心。

    系统调用可以看作是一个所有Unix/Linux进程共享的子程序库,但是它是在特权方式下运行,可以存取核心数据结构和它所支持的用户级数据。系统调用的主要功能是使用户可以使用操作系统提供的有关设备管理、文件系统、进程控制进程通讯以及存储管理方面的功能,而不必要了解操作系统的内部结构和有关硬件的细节问题,从而减轻用户负担和保护系统以及提高资源利用率。

    系统调用分为两个部分:与文件子系统交互的和进程子系统交互的两个部分。其中和文件子系统交互的部分进一步由可以包括与设备文件的交互和与普通文件的交互的系统调用(open, close, ioctl, create, unlink, . . . );与进程相关的系统调用又包括进程控制系统调用(fork, exit, getpid, . . . ),进程间通讯,存储管理,进程调度等方面的系统调用。
 

2.Linux下系统调用的实现
    (以i386为例说明)
         A.在Linux中系统调用是怎样陷入核心的?
    系统调用在使用时和一般的函数调用很相似,但是二者是有本质性区别的,函数调用不能引起从用户态到核心态的转换,而正如前面提到的,系统调用需要有一个状态转换。

    在每种平台上,都有特定的指令可以使进程的执行由用户态转换为核心态,这种指令称作操作系统陷入(operating system trap)。进程通过执行陷入指令后,便可以在核心态运行系统调用代码。

    在Linux中是通过软中断来实现这种陷入的,在x86平台上,这条指令是int 0x80。也就是说在Linux中,系统调用的接口是一个中断处理函数的特例。具体怎样通过中断处理函数来实现系统调用的入口将在后面详细介绍。

    这样,就需要在系统启动时,对INT 0x80进行一定的初始化,下面将描述其过程:

1.使用汇编子程序setup_idtlinux/arch/i386/kernel/head.S)初始化idt表(中断描述符表),这时所有的入口函数偏移地址都被设为ignore_int

        

              ( setup_idt:        

      lea ignore_int,%edx

              

      movl $(__KERNEL_CS << 16),%eax

              

      movw %dx,%ax /* selector = 0x0010 = cs */

              

      movw $0x8E00,%dx /* interrupt gate – dpl=0, present */

              

      lea SYMBOL_NAME(idt_table),%edi

              

      mov $256,%ecx

              

      rp_sidt:

              

      movl %eax,(%edi)

              

      movl %edx,4(%edi)

              

      addl $8,%edi

              

      dec %ecx

              

      jne rp_sidt

              

      ret

              

      selector = __KERNEL_CS, DPL = 0, TYPE = E, P = 1;

              

      2.Start_kernel()(linux/init/main.c)调用trap_init()(linux/arch/i386/kernel/trap.c)函数设置中断描述符表。在该函数里,实际上是通过调用函数set_system_gate(SYSCALL_VECTOR,&system_call)来完成该项的设置的。其中的SYSCALL_VECTOR就是0x80,而system_call则是一个汇编子函数,它即是中断0x80的处理函数,主要完成两项工作a. 寄存器上下文的保存;b. 跳转到系统调用处理函数。在后面会详细介绍这些内容。

          

 

(补充说明:门描述符

    set_system_gate()是在linux/arch/i386/kernel/trap.S中定义的,在该文件中还定义了几个类似的函数set_intr_gate(), set_trap_gate, set_call_gate()。这些函数都调用了同一个汇编子函数__set_gate(),该函数的作用是设置门描述符。IDT中的每一项都是一个门描述符。

#define _set_gate(gate_addr,type,dpl,addr)

set_gate(idt_table+n,15,3,addr);

    门描述符的作用是用于控制转移,其中会包括选择子,这里总是为__KERNEL_CS(指向GDT中的一项段描述符)、入口函数偏移地址、门访问特权级(DPL)以及类型标识(TYPE)。Set_system_gateDPL3,表示从特权级3(最低特权级)也可以访问该门,type15,表示为386中断门。)

 
 

        

      B.与系统调用相关的数据结构        

      1.系统调用处理函数的函数名的约定

              

          函数名都以“sys_”开头,后面跟该系统调用的名字。例如,系统调用fork()的处理函数名是sys_fork()

              

      asmlinkage int sys_fork(struct pt_regs regs);

              

      (补充关于asmlinkage的说明)

              

       
              2.系统调用号(System Call Number

              

          核心中为每个系统调用定义了一个唯一的编号,这个编号的定义在linux/include/asm/unistd.h中,编号的定义方式如下所示:

              

      #define __NR_exit 1

              

      #define __NR_fork 2

              

      #define __NR_read 3

              

      #define __NR_write 4

              

      . . . . . .

              

          用户在调用一个系统调用时,系统调用号号作为参数传递给中断0x80,而该标号实际上是后面将要提到的系统调用表(sys_call_table)的下标,通过该值可以找到相映系统调用的处理函数地址。

              

       
              3.系统调用表

          

系统调用表的定义方式如下:(linux/arch/i386/kernel/entry.S

ENTRY(sys_call_table)

.long SYMBOL_NAME(sys_ni_syscall)

.long SYMBOL_NAME(sys_exit)

.long SYMBOL_NAME(sys_fork)

.long SYMBOL_NAME(sys_read)

.long SYMBOL_NAME(sys_write)

. . . . . .

系统调用表记录了各个系统调用处理函数的入口地址,以系统调用号为偏移量很容易的能够在该表中找到对应处理函数地址。在linux/include/linux/sys.h中定义的NR_syscalls表示该表能容纳的最大系统调用数,NR_syscalls = 256

 
C.系统调用函数接口是如何转化为陷入命令
 

        

                  如前面提到的,系统调用是通过一条陷入指令进入核心态,然后根据传给核心的系统调用号为索引在系统调用表中找到相映的处理函数入口地址。这里将详细介绍这一过程。        

          我们还是以x86为例说明:

              

          由于陷入指令是一条特殊指令,而且依赖与操作系统实现的平台,如在x86中,这条指令是int 0x80,这显然不是用户在编程时应该使用的语句,因为这将使得用户程序难于移植。所以在操作系统的上层需要实现一个对应的系统调用库,每个系统调用都在该库中包含了一个入口点(如我们看到的fork, open, close等等),这些函数对程序员是可见的,而这些库函数的工作是以对应系统调用号作为参数,执行陷入指令int 0x80,以陷入核心执行真正的系统调用处理函数。当一个进程调用一个特定的系统调用库的入口点,正如同它调用任何函数一样,对于库函数也要创建一个栈帧。而当进程执行陷入指令时,它将处理机状态转换到核心态,并且在核心栈执行核心代码。

              

          这里给出一个示例(linux/include/asm/unistd.h):

              

      #define _syscallN(type, name, type1, arg1, type2, arg2, . . . ) \

              

      type name(type1 arg1,type2 arg2) \

              

      { \

              

      long __res; \

              

      __asm__ volatile ("int $0x80" \

              

      : "=a" (__res) \

              

      : "" (__NR_##name),"b" ((long)(arg1)),"c" ((long)(arg2))); \

              

      . . . . . .

              

      __syscall_return(type,__res); \

              

      }

              

          在执行一个系统调用库中定义的系统调用入口函数时,实际执行的是类似如上的一段代码。这里牵涉到一些gcc的嵌入式汇编语言,不做详细的介绍,只简单说明其意义:

              

          其中__NR_##name是系统调用号,如name == ioctl,则为__NR_ioctl,它将被放在寄存器eax中作为参数传递给中断0x80的处理函数。而系统调用的其它参数arg1, arg2, …则依次被放入ebx, ecx, . . .等通用寄存器中,并作为系统调用处理函数的参数,这些参数是怎样传入核心的将会在后面介绍。

              

          下面将示例说明:

              

      int func1()

              

      {

              

      int fd, retval;

              

      fd = open(filename, ……);

              

      ……

              

      ioctl(fd, cmd, arg);

              

      . . .

              

      }

              

       

              

      func2()

              

      {

              

      int fd, retval;

              

      fd = open(filename, ……);

              

      ……

              

      __asm__ __volatile__(\

              

      "int $0x80\n\t"\

              

      :"=a"(retval)\

              

      :"0"(__NR_ioctl),\

              

      "b"(fd),\

              

      "c"(cmd),\

              

      "d"(arg));

              

      }

              

          这两个函数在Linux/x86上运行的结果应该是一样的。

              

          若干个库函数可以映射到同一个系统调用入口点。系统调用入口点对每个系统调用定义其真正的语法和语义,但库函数通常提供一个更方便的接口。如系统调用exec有集中不同的调用方式:execl, execle,等,它们实际上只是同一系统调用的不同接口而已。对于这些调用,它们的库函数对它们各自的参数加以处理,来实现各自的特点,但是最终都被映射到同一个核心入口点。

              

       D.系统调用陷入内核后作何初始化处理

          

    当进程执行系统调用时,先调用系统调用库中定义某个函数,该函数通常被展开成前面提到的_syscallN的形式通过INT 0x80来陷入核心,其参数也将被通过寄存器传往核心。

    在这一部分,我们将介绍INT 0x80的处理函数system_call

    思考一下就会发现,在调用前和调用后执行态完全不相同:前者是在用户栈上执行用户态程序,后者在核心栈上执行核心态代码。那么,为了保证在核心内部执行完系统调用后能够返回调用点继续执行用户代码,必须在进入核心态时保存时往核心中压入一个上下文层;在从核心返回时会弹出一个上下文层,这样用户进程就可以继续运行。

    那么,这些上下文信息是怎样被保存的,被保存的又是那些上下文信息呢?这里仍以x86为例说明。

    在执行INT指令时,实际完成了以下几条操作:

1.由于INT指令发生了不同优先级之间的控制转移,所以首先从TSS(任务状态段)中获取高优先级的核心堆栈信息(SSESP);2.把低优先级堆栈信息(SSESP)保留到高优先级堆栈(即核心栈)中;
3.把EFLAGS,外层CSEIP推入高优先级堆栈(核心栈)中。
4.通过IDT加载CSEIP(控制转移至中断处理函数)

然后就进入了中断0x80的处理函数system_call了,在该函数中首先使用了一个宏SAVE_ALL,该宏的定义如下所示:

#define SAVE_ALL \

cld; \

pushl %es; \

pushl %ds; \

pushl %eax; \

pushl %ebp; \

pushl %edi; \

pushl %esi; \

pushl %edx; \

pushl %ecx; \

pushl %ebx; \

movl $(__KERNEL_DS),%edx; \

movl %edx,%ds; \

movl %edx,%es;

    该宏的功能一方面是将寄存器上下文压入到核心栈中,对于系统调用,同时也是系统调用参数的传入过程,因为在不同特权级之间控制转换时,INT指令不同于CALL指令,它不会将外层堆栈的参数自动拷贝到内层堆栈中。所以在调用系统调用时,必须先象前面的例子里提到的那样,把参数指定到各个寄存器中,然后在陷入核心之后使用SAVE_ALL把这些保存在寄存器中的参数依次压入核心栈,这样核心才能使用用户传入的参数。下面给出system_call的源代码:

ENTRY(system_call)

pushl %eax # save orig_eax

SAVE_ALL

GET_CURRENT(%ebx)

cmpl $(NR_syscalls),%eax

jae badsys

testb $0x20,flags(%ebx) # PF_TRACESYS

jne tracesys

call *SYMBOL_NAME(sys_call_table)(,%eax,4)

. . . . . .

          在这里所做的所有工作是:
           1.保存EAX寄存器,因为在SAVE_ALL中保存的EAX寄存器会被调用的返回值所覆盖;
           2.调用SAVE_ALL保存寄存器上下文;
           3.判断当前调用是否是合法系统调用(EAX是系统调用号,它应该小于NR_syscalls);
           4.如果设置了PF_TRACESYS标志,则跳转到syscall_trace,在那里将会把当前进程挂起并向其父进程发送SIGTRAP,这主要是为了设              置调试断点而设计的;
           5.如果没有设置PF_TRACESYS标志,则跳转到该系统调用的处理函数入口。这里是以EAX(即前面提到的系统调用号)作为偏移,在系             统调用表sys_call_table中查找处理函数入口地址,并跳转到该入口地址。
 

补充说明:
1.GET_CURRENT

        

              #define GET_CURRENT(reg)        

      movl %esp, reg;

              

      andl $-8192, reg;

              

          其作用是取得当前进程的task_struct结构的指针返回到reg中,因为在Linux中核心栈的位置是task_struct之后的两个页面处(8192bytes),所以此处把栈指针与-8192则得到的是task_struct结构指针,而task_struct中偏移为4的位置是成员flags,在这里指令testb $0x20,flags(%ebx)检测的就是task_struct->flags

              


              2.堆栈中的参数

              

          正如前面提到的,SAVE_ALL是系统调用参数的传入过程,当执行完SAVE_ALL并且再由CALL指令调用其处理函数时,堆栈的结构应该如上图所示。这时的堆栈结构看起来和执行一个普通带参数的函数调用是一样的,参数在堆栈中对应的顺序是(arg1 ebx),(arg2, ecx,arg3, edx. . . . . .,这正是SAVE_ALL压栈的反顺序,这些参数正是用户在使用系统调用时试图传送给核心的参数。下面是在核心的调用处理函数中使用参数的两种典型方法:

              

      asmlinkage int sys_fork(struct pt_regs regs)

              

      asmlinkage int sys_open(const char * filename, int flags, int mode)

              

          在sys_fork中,把整个堆栈中的内容视为一个struct pt_regs类型的参数,该参数的结构和堆栈的结构是一致的,所以可以使用堆栈中的全部信息。而在sys_open中参数filename, flags, mode正好对应与堆栈中的ebx, ecx, edx的位置,而这些寄存器正是用户在通过C库调用系统调用时给这些参数指定的寄存器。

              

      __asm__ __volatile__(\

              

      "int $0x80\n\t"\

              

      :"=a"(retval)\

              

      :"0"(__NR_open),\

              

      "b"(filename),\

              

      "c"(flags),\

              

      "d"(mode));

              

       
              3.核心如何使用用户空间的参数

          

        

    

在使用系统调用时,有些参数是指针,这些指针所指向的是用户空间DS寄存器的段选择子所描述段中的地址,而在2.2之前的版本中,核心态的DS段寄存器的中的段选择子和用户态的段选择子描述的段地址不同(前者为0xC0000000, 后者为0x00000000),这样在使用这些参数时就不能读取到正确的位置。所以需要通过特殊的核心函数(如:memcpy_fromfs, mencpy_tofs)来从用户空间数据段读取参数,在这些函数中,是使用FS寄存器来作为读取参数的段寄存器的,FS寄存器在系统调用进入核心态时被设成了USER_DSDS被设成了KERNEL_DS)。在2.2之后的版本用户态和核心态使用的DS中段选择子描述的段地址是一样的(都是0x00000000),所以不需要再经过上面那样烦琐的过程而直接使用参数了。2.2及以后的版本linux/arch/i386/head.S    

ENTRY(gdt_table)

    

.quad 0x0000000000000000/* NULL descriptor */    

.quad 0x0000000000000000/* not used */

    

.quad 0x00cf9a000000ffff /* 0x10 kernel 4GB code at 0x00000000 */

    

.quad 0x00cf92000000ffff /* 0x18 kernel 4GB data at 0x00000000 */

    

.quad 0x00cffa000000ffff /* 0x23 user 4GB code at 0x00000000 */

    

.quad 0x00cff2000000ffff /* 0x2b user 4GB data at 0x00000000 */

    

                           2.0 linux/arch/i386/head.S ENTRY(gdt) .quad 0x0000000000000000 /* NULL descriptor */    

.quad 0x0000000000000000 /* not used */

    

.quad 0xc0c39a000000ffff /* 0x10 kernel 1GB code at 0xC0000000 */

    

.quad 0xc0c392000000ffff /* 0x18 kernel 1GB data at 0xC0000000 */

    

.quad 0x00cbfa000000ffff /* 0x23 user 3GB code at 0x00000000 */

    

.quad 0x00cbf2000000ffff /* 0x2b user 3GB data at 0x00000000 *

    

 

    

2.0版的内核中SAVE_ALL宏定义还有这样几条语句:

    

"movl $" STR(KERNEL_DS) ",%edx\n\t"

    

"mov %dx,%ds\n\t"

    

"mov %dx,%es\n\t"

    

"movl $" STR(USER_DS) ",%edx\n\t"

    

"mov %dx,%fs\n\t"

    

"movl $0,%edx\n\t"

    

 
     

    

E.调用返回
    调用返回的过程要做的工作比其响应过程要多一些,这些工作几乎是每次从核心态返回用户态都需要做的,这里将简要的说明:    

1.判断有没有软中断,如果有则跳转到软中断处理;
    2.判断当前进程是否需要重新调度,如果需要则跳转到调度处理;
    3.如果当前进程有挂起的信号还没有处理,则跳转到信号处理;
    4.使用用RESTORE_ALL来弹出所有被SAVE_ALL压入核心栈的内容并且使用iret返回用户态。

    

F.实例介绍

    

    前面介绍了系统调用相关的数据结构以及在Linux中使用一个系统调用的过程中每一步是怎样处理的,下面将把前面的所有概念串起来,说明怎样在Linux中增加一个系统调用。    

这里实现的系统调用hello仅仅是在控制台上打印一条语句,没有任何功能。

    

1.修改linux/include/i386/unistd.h,在里面增加一条语句:

    

    

            

                  #define __NR_hello ???(这个数字可能因为核心版本不同而不同)
                  2.在某个合适的目录中(如:linux/kernel)增加一个hello.c,修改该目录下的Makefile(把相映的.o文件列入Makefile中就可以了)。
                  3.编写hello.c            

      . . . . . .

                  

      asmlinkage int sys_hello(char * str)

                  

      {

                  

      printk(“My syscall: hello, I know what you say to me: %s ! \n”, str);

                  

      return 0;

                  

      }

                  

       
                  4.修改linux/arch/i386/kernel/entry.S,在里面增加一条语句:

                  

      ENTRY(sys_call_table)

                  

      . . . . . .

                  

      .long SYMBOL_NAME(sys_hello)

                  

      并且修改:

                  

      .rept NR_syscalls-??? /* ??? = ??? +1 */

                  

      .long SYMBOL_NAME(sys_ni_syscall)
                  5.在linux/include/i386/中增加hello.h,里面至少应包括这样几条语句:

              

        

    

#include <linux/unistd.h>    

 

    

#ifdef __KERNEL

    

#else

    

inline _syscall1(int, hello, char *, str);

    

#endif

    

这样就可以使用系统调用hello

    

 
     

    

                                                                           Back

    

 

    

    

            

  • Linux中的系统调用
  •     

    

    1. 进程相关的系统调用
            Fork & vfork & clone        

        进程是一个指令执行流及其执行环境,其执行环境是一个系统资源的集合,这些资源在Linux中被抽象成各种数据对象:进程控制块、虚存空间、文件系统,文件I/O、信号处理函数。所以创建一个进程的过程就是这些数据对象的创建过程。

            

        在调用系统调用fork创建一个进程时,子进程只是完全复制父进程的资源,这样得到的子进程独立于父进程,具有良好的并发性,但是二者之间的通讯需要通过专门的通讯机制,如:pipefifoSystem V IPC机制等,另外通过fork创建子进程系统开销很大,需要将上面描述的每种资源都复制一个副本。这样看来,fork是一个开销十分大的系统调用,这些开销并不是所有的情况下都是必须的,比如某进程fork出一个子进程后,其子进程仅仅是为了调用exec执行另一个执行文件,那么在fork过程中对于虚存空间的复制将是一个多余的过程(由于Linux中是采取了copy-on-write技术,所以这一步骤的所做的工作只是虚存管理部分的复制以及页表的创建,而并没有包括物理也面的拷贝);另外,有时一个进程中具有几个独立的计算单元,可以在相同的地址空间上基本无冲突进行运算,但是为了把这些计算单元分配到不同的处理器上,需要创建几个子进程,然后各个子进程分别计算最后通过一定的进程间通讯和同步机制把计算结果汇总,这样做往往有许多格外的开销,而且这种开销有时足以抵消并行计算带来的好处。

            

     

            

        这说明了把计算单元抽象到进程上是不充分的,这也就是许多系统中都引入了线程的概念的原因。在讲述线程前首先介绍以下vfork系统调用,vfork系统调用不同于fork,用vfork创建的子进程共享地址空间,也就是说子进程完全运行在父进程的地址空间上,子进程对虚拟地址空间任何数据的修改同样为父进程所见。但是用vfork创建子进程后,父进程会被阻塞直到子进程调用execexit。这样的好处是在子进程被创建后仅仅是为了调用exec执行另一个程序时,因为它就不会对父进程的地址空间有任何引用,所以对地址空间的复制是多余的,通过vfork可以减少不必要的开销。

            

        在Linux中, forkvfork都是调用同一个核心函数

            

        do_fork(unsigned long clone_flag, unsigned long usp, struct pt_regs)

            

        其中clone_flag包括CLONE_VM, CLONE_FS, CLONE_FILES, CLONE_SIGHAND, CLONE_PIDCLONE_VFORK等等标志位,任何一位被置1了则表明创建的子进程和父进程共享该位对应的资源。所以在vfork的实现中,cloneflags = CLONE_VFORK | CLONE_VM | SIGCHLD,这表示子进程和父进程共享地址空间,同时do_fork会检查CLONE_VFORK,如果该位被置1了,子进程会把父进程的地址空间锁住,直到子进程退出或执行exec时才释放该锁。

            

     

            

        在讲述clone系统调用前先简单介绍线程的一些概念。

            

        线程是在进程的基础上进一步的抽象,也就是说一个进程分为两个部分:线程集合和资源集合。线程是进程中的一个动态对象,它应该是一组独立的指令流,进程中的所有线程将共享进程里的资源。但是线程应该有自己的私有对象:比如程序计数器、堆栈和寄存器上下文。

            

        线程分为三种类型:

            

        内核线程、轻量级进程和用户线程。

            

    内核线程:

            

        它的创建和撤消是由内核的内部需求来决定的,用来负责执行一个指定的函数,一个内核线程不需要一个用户进程联系起来。它共享内核的正文段核全局数据,具有自己的内核堆栈。它能够单独的被调度并且使用标准的内核同步机制,可以被单独的分配到一个处理器上运行。内核线程的调度由于不需要经过态的转换并进行地址空间的重新映射,因此在内核线程间做上下文切换比在进程间做上下文切换快得多。

            

    轻量级进程:

            

        轻量级进程是核心支持的用户线程,它在一个单独的进程中提供多线程控制。这些轻量级进程被单独的调度,可以在多个处理器上运行,每一个轻量级进程都被绑定在一个内核线程上,而且在它的生命周期这种绑定都是有效的。轻量级进程被独立调度并且共享地址空间和进程中的其它资源,但是每个LWP都应该有自己的程序计数器、寄存器集合、核心栈和用户栈。

            

    用户线程:

            

        用户线程是通过线程库实现的。它们可以在没有内核参与下创建、释放和管理。线程库提供了同步和调度的方法。这样进程可以使用大量的线程而不消耗内核资源,而且省去大量的系统开销。用户线程的实现是可能的,因为用户线程的上下文可以在没有内核干预的情况下保存和恢复。每个用户线程都可以有自己的用户堆栈,一块用来保存用户级寄存器上下文以及如信号屏蔽等状态信息的内存区。库通过保存当前线程的堆栈和寄存器内容载入新调度线程的那些内容来实现用户线程之间的调度和上下文切换。

            

        内核仍然负责进程的切换,因为只有内核具有修改内存管理寄存器的权力。用户线程不是真正的调度实体,内核对它们一无所知,而只是调度用户线程下的进程或者轻量级进程,这些进程再通过线程库函数来调度它们的线程。当一个进程被抢占时,它的所有用户线程都被抢占,当一个用户线程被阻塞时,它会阻塞下面的轻量级进程,如果进程只有一个轻量级进程,则它的所有用户线程都会被阻塞。

            

     

            

        明确了这些概念后,来讲述Linux的线程和clone系统调用。

            

        在许多实现了MT的操作系统中(如:SolarisDigital Unix等), 线程和进程通过两种数据结构来抽象表示: 进程表项和线程表项,一个进程表项可以指向若干个线程表项, 调度器在进程的时间片内再调度线程。  但是在Linux中没有做这种区分,  而是统一使用task_struct来管理所有进程/线程,只是线程与线程之间的资源是共享的,这些资源可是是前面提到过的:虚存、文件系统、文件I/O以及信号处理函数甚至PID中的几种。

            


                也就是说Linux中,每个线程都有一个task_struct,所以线程和进程可以使用同一调度器调度。其实Linux核心中,轻量级进程和进程没有质上的差别,因为Linux中进程的概念已经被抽象成了计算状态加资源的集合,这些资源在进程间可以共享。如果一个task独占所有的资源,则是一个HWP,如果一个task和其它task共享部分资源,则是LWP

            

        clone系统调用就是一个创建轻量级进程的系统调用:

            

        int clone(int (*fn)(void * arg), void *stack, int flags, void * arg);

            

        其中fn是轻量级进程所执行的过程,stack是轻量级进程所使用的堆栈,flags可以是前面提到的CLONE_VM, CLONE_FS, CLONE_FILES, CLONE_SIGHAND,CLONE_PID的组合。Clone forkvfork在实现时都是调用核心函数do_fork

            

        do_fork(unsigned long clone_flag, unsigned long usp, struct pt_regs)

            

        和forkvfork不同的是,forkclone_flag = SIGCHLD

            

        vforkclone_flag = CLONE_VM | CLONE_VFORK | SIGCHLD

            

        而在clone中,clone_flag由用户给出。

            

        下面给出一个使用clone的例子。

            

        Void * func(int arg)

            

        {

            

        . . . . . .

            

        }

            

        int main()

            

        {

            

    int clone_flag, arg;

            

    . . . . . .

            

    clone_flag = CLONE_VM | CLONE_SIGHAND | CLONE_FS |

            

    CLONE_FILES;

            

    stack = (char *)malloc(STACK_FRAME);

            

    stack += STACK_FRAME;

            

    retval = clone((void *)func, stack, clone_flag, arg);

            

    . . . . . .

            

    }

            

        看起来clone的用法和pthread_create有些相似,两者的最根本的差别在于clone是创建一个LWP,对核心是可见的,由核心调度,而pthread_create通常只是创建一个用户线程,对核心是不可见的,由线程库调度。

            

     

            

    Nanosleep & sleep

            

        sleep和nanosleep都是使进程睡眠一段时间后被唤醒,但是二者的实现完全不同。

            

        Linux中并没有提供系统调用sleepsleep是在库函数中实现的,它是通过调用alarm来设定报警时间,调用sigsuspend将进程挂起在信号SIGALARM上,sleep只能精确到秒级上。

            

        nanosleep则是Linux中的系统调用,它是使用定时器来实现的,该调用使调用进程睡眠,并往定时器队列上加入一个time_list型定时器,time_list结构里包括唤醒时间以及唤醒后执行的函数,通过nanosleep加入的定时器的执行函数仅仅完成唤醒当前进程的功能。系统通过一定的机制定时检查这些队列(比如通过系统调用陷入核心后,从核心返回用户态前,要检查当前进程的时间片是否已经耗尽,如果是则调用schedule()函数重新调度,该函数中就会检查定时器队列,另外慢中断返回前也会做此检查),如果定时时间已超过,则执行定时器指定的函数唤醒调用进程。当然,由于系统时间片可能丢失,所以nanosleep精度也不是很高。

            

        alarm也是通过定时器实现的,但是其精度只精确到秒级,另外,它设置的定时器执行函数是在指定时间向当前进程发送SIGALRM信号。

            

     
            2.存储相关的系统调用

        

    

mmap:文件映射    

    在讲述文件映射的概念时,不可避免的要牵涉到虚存(SVR 4VM)。实际上,文件映射是虚存的中心概念,文件映射一方面给用户提供了一组措施,似的用户将文件映射到自己地址空间的某个部分,使用简单的内存访问指令读写文件;另一方面,它也可以用于内核的基本组织模式,在这种模式种,内核将整个地址空间视为诸如文件之类的一组不同对象的映射。

    

    Unix中的传统文件访问方式是,首先用open系统调用打开文件,然后使用readwrite以及lseek等调用进行顺序或者随即的I/O。这种方式是非常低效的,每一次I/O操作都需要一次系统调用。另外,如果若干个进程访问同一个文件,每个进程都要在自己的地址空间维护一个副本,浪费了内存空间。而如果能够通过一定的机制将页面映射到进程的地址空间中,也就是说首先通过简单的产生某些内存管理数据结构完成映射的创建。当进程访问页面时产生一个缺页中断,内核将页面读入内存并且更新页表指向该页面。而且这种方式非常方便于同一副本的共享。

    

    下面给出以上两种方式的对比图:

    

 

    

    VM是面向对象的方法设计的,这里的对象是指内存对象:内存对象是一个软件抽象的概念,它描述内存区与后备存储之间的映射。系统可以使用多种类型的后备存储,比如交换空间,本地或者远程文件以及帧缓存等等。VM系统对它们统一处理,采用同一操作集操作,比如读取页面或者回写页面等。每种不同的后备存储都可以用不同的方法实现这些操作。这样,系统定义了一套统一的接口,每种后备存储给出自己的实现方法。

    

    这样,进程的地址空间就被视为一组映射到不同数据对象上的的映射组成。所有的有效地址就是那些映射到数据对象上的地址。这些对象为映射它的页面提供了持久性的后备存储。映射使得用户可以直接寻址这些对象。

    

    值得提出的是,VM体系结构独立于Unix系统,所有的Unix系统语义,如正文,数据及堆栈区都可以建构在基本VM系统之上。同时,VM体系结构也是独立于存储管理的,存储管理是由操作系统实施的,如:究竟采取什么样的对换和请求调页算法,究竟是采取分段还是分页机制进行存储管理,究竟是如何将虚拟地址转换成为物理地址等等(Linux中是一种叫Three Level Page Table的机制),这些都与内存对象的概念无关。

    

    下面介绍LinuxVM的实现。

    

    如下图所示,一个进程应该包括一个mm_structmemory manage struct),该结构是进程虚拟地址空间的抽象描述,里面包括了进程虚拟空间的一些管理信息:start_code, end_code, start_data, end_data, start_brk, end_brk等等信息。另外,也有一个指向进程虚存区表(vm_area_struct virtual memory area)的指针,该链是按照虚拟地址的增长顺序排列的。

    


        在Linux进程的地址空间被分作许多区(vma),每个区(vma)都对应虚拟地址空间上一段连续的区域,vma是可以被共享和保护的独立实体,这里的vma就是前面提到的内存对象。这里给出vm_area_struct的结构,其中,前半部分是公共的,与类型无关的一些数据成员,如:指向mm_struct的指针,地址范围等等,后半部分则是与类型相关的成员,其中最重要的是一个指向vm_operation_struct向量表的指针vm_opsvm_pos向量表是一组虚函数,定义了与vma类型无关的接口。每一个特定的子类,即每种vma类型都必须在向量表中实现这些操作。这里包括了:open, close, unmap, protect, sync, nopage, wppage, swapout这些操作。

    

struct vm_area_struct {

    

/*公共的,与vma类型无关的 */

    

struct mm_struct * vm_mm;    

unsigned long vm_start;

    

unsigned long vm_end;

    

struct vm_area_struct *vm_next;

    

pgprot_t vm_page_prot;

    

unsigned long vm_flags;

    

short vm_avl_height;

    

struct vm_area_struct * vm_avl_left;

    

struct vm_area_struct * vm_avl_right;

    

struct vm_area_struct *vm_next_share;

    

struct vm_area_struct **vm_pprev_share;

    

/* 与类型相关的 */

    

struct vm_operations_struct * vm_ops;

    

unsigned long vm_pgoff;

    

struct file * vm_file;

    

unsigned long vm_raend

    

void * vm_private_data;

    

};    

vm_ops: open, close, no_page, swapin, swapout . . . . . .

    

    介绍完VM的基本概念后,我们可以讲述mmap, munmap系统调用了。mmap调用实际上就是一个内存对象vma的创建过程,mmap的调用格式是: void * mmap(void *start, size_t length, int prot , int flags, int fd, off_t offset);其中start是映射地址,length是映射长度,如果flagsMAP_FIXED不被置位,则该参数通常被忽略,而查找进程地址空间中第一个长度符合的空闲区域;Fd是映射文件的文件句柄,offset是映射文件中的偏移地址;prot是映射保护权限,可以是PROT_EXEC, PROT_READ, PROT_WRITE, PROT_NONEflags则是指映射类型,可以是MAP_FIXED, MAP_PRIVATE, MAP_SHARED,该参数必须被指定为MAP_PRIVATEMAP_SHARED其中之一,MAP_PRIVATE是创建一个写时拷贝映射(copy-on-write),也就是说如果有多个进程同时映射到一个文件上,映射建立时只是共享同样的存储页面,但是某进程企图修改页面内容,则复制一个副本给该进程私用,它的任何修改对其它进程都不可见。而MAP_SHARED则无论修改与否都使用同一副本,任何进程对页面的修改对其它进程都是可见的。    

Mmap系统调用的实现过程是:

    

    1.先通过文件系统定位要映射的文件;
        2.权限检查,映射的权限不会超过文件打开的方式,也就是说如果文件是以只读方式打开,那么则不允许建立一个可写映射;
        3.创建一个vma对象,并对之进行初始化;
        4.调用映射文件的mmap函数,其主要工作是给vm_ops向量表赋值;
        5.把该vma链入该进程的vma链表中,如果可以和前后的vma合并则合并;
        6.如果是要求VM_LOCKED(映射区不被换出)方式映射,则发出缺页请求,把映射页面读入内存中;

    

munmap(void * start, size_t length)    

    该调用可以看作是mmap的一个逆过程。它将进程中从start开始length长度的一段区域的映射关闭,如果该区域不是恰好对应一个vma,则有可能会分割几个或几个vma

    

Msync(void * start, size_t length, int flags)

    

    把映射区域的修改回写到后备存储中。因为munmap时并不保证页面回写,如果不调用msync,那么有可能在munmap后丢失对映射区的修改。其中flags可以是MS_SYNC, MS_ASYNC, MS_INVALIDATEMS_SYNC要求回写完成后才返回,MS_ASYNC发出回写请求后立即返回,MS_INVALIDATE使用回写的内容更新该文件的其它映射。

    

    该系统调用是通过调用映射文件的sync函数来完成工作的。

    

brk(void * end_data_segement):

    

    将进程的数据段扩展到end_data_segement指定的地址,该系统调用和mmap的实现方式十分相似,同样是产生一个vma,然后指定其属性。不过在此之前需要做一些合法性检查,比如该地址是否大于mm->end_codeend_data_segementmm->brk之间是否还存在其它vma等等。通过brk产生的vma映射的文件为空,这和匿名映射产生的vma相似,关于匿名映射不做进一步介绍。我们使用的库函数malloc就是通过brk实现的,通过下面这个例子很容易证实这点:

    

main()

    

{

    

char * m, * n;

    

int size;

    

 

    

m = (char *)sbrk(0);

    

printf("sbrk addr = %08lx\n", m);

    

do {

    

n = malloc(1024);

    

printf("malloc addr = %08lx\n", n);

    

}w hile(n < m);    

m = (char *)sbrk(0);

    

printf("new sbrk addr = %08lx\n", m);    

}

    

 

    

       sbrk addr = 0804a000 malloc addr = 080497d8    

malloc addr = 08049be0

    

malloc addr = 08049fe8

    

malloc addr = 0804a3f0

    

new sbrk addr = 0804b000

    

3.进程间通信(IPC

    

    

                进程间通讯可以通过很多种机制,包括signal, pipe, fifo, System V IPC, 以及socket等等,前几种概念都比较好理解,这里着重介绍关于System V IPC        

        System V IPC包括三种机制:message(允许进程发送格式化的数据流到任意的进程)、shared memory(允许进程间共享它们虚拟地址空间的部分区域)和semaphore(允许进程间同步的执行)。

            

        操作系统核心中为它们分别维护着一个表,这三个表是系统中所有这三种IPC对象的集合,表的索引是一个数值ID,进程通过这个ID可以查找到需要使用的IPC资源。进程每创建一个IPC对象,系统中都会在相应的表中增加一项。之后其它进程(具有许可权的进程)只要通过该IPC对象的ID则可以引用它。

            

        IPC对象必须使用IPC_RMID命令来显示的释放,否则这个对象就处于活动状态,甚至所有的使用它的进程都已经终止。这种机制某些时候十分有用,但是也正因为这种特征,使得操作系统内核无法判断IPC对象是被用户故意遗留下来供将来其它进程使用还是被无意抛弃的。

            

        Linux中只提供了一个系统调用接口ipc()来完成所有System V IPC操作,我们常使用的是建立在该调用之上的库函数接口。对于这三种IPC,都有很相似的三种调用:xxxget, (msgsnd, msgrcv)semopt | (shmat, shmdt), xxxctl

            

        Xxxget:获取调用,在系统中申请或者查询一个IPC资源,返回值是该IPC对象的ID,该调用类似于文件系统的open, create调用;

            

        Xxxctl:控制调用,至少包括三种操作:XXX_RMID(释放IPC对象), XXX_STAT(查询状态), XXX_SET(设置状态信息);

            

        (msgsnd, msgrcv) | Semopt | (shmat, shmdt)|:操作调用,这些调用的功能随IPC对象的类型不同而有较大差异。

            

    4.文件系统相关的调用

            

        文件是用来保存数据的,而文件系统则可以让用户组织,操纵以及存取不同的文件。内核允许用户通过一个严格定义的过程性接口与文件系统进行交互,这个接口对用户屏蔽了文件系统的细节,同时指定了所有相关系统调用的行为和语义。Linux支持许多中文件系统,如ext2msdos, ntfs, proc, dev, ufs, nfs等等,这些文件系统都实现了相同的接口,因此给应用程序提供了一致性的视图。但每种文件系统在实现时可能对某个方面加以了一定的限制。如:文件名的长度,是否支持所有的文件系统接口调用。

            

        为了支持多文件系统,sun提出了一种vnode/vfs接口,SVR4中将之实现成了一种工业标准。而Linux作为一种Unixclone体,自然也实现了这种接口,只是它的接口定义和SVR4的稍有不同。Vnode/Vfs接口的设计体现了面向对象的思想,Vfs(虚拟文件系统)代表内核中的一个文件系统,Vnode(虚拟节点)代表内核中的一个文件,它们都可以被视为抽象基类,并可以从中派生出不同的子类以实现不同的文件系统。

            

        由于篇幅原因,这里只是大概的介绍一下怎样通过Vnode/Vfs结构来实现文件系统和访问文件。

            

        在Linux中支持的每种文件系统必须有一个file_system_type结构,此结构的核心是read_super函数,该函数将读取文件系统的超级块。Linux中支持的所有文件系统都会被注册在一条file_system_type结构链中,注册是在系统初始化时调用regsiter_filesystem()完成,如果文件系统是以模块的方式实现,则是在调用init_module时完成。

            


                当mount某种块设备时,将调用系统调用mount,该调用中将会首先检查该类文件系统是否注册在系统种中,如果注册了则先给该文件系统分配一个super_block,并进行初始化,最后调用这种文件系统的read_super函数来完成super_block结构私有数据的赋值。其中最主要的工作是给super_blocks_ops赋值,s_ops是一个函数向量表,由文件系统各自实现了一组操作。

            

    struct super_operations {

            

    void (*read_inode) (struct inode *);

            

    void (*write_inode) (struct inode *);

            

    void (*put_inode) (struct inode *);

            

    void (*delete_inode) (struct inode *);

            

    void (*put_super) (struct super_block *);

            

    void (*write_super) (struct super_block *);

            

    int (*statfs) (struct super_block *, struct statfs *);

            

    int (*remount_fs) (struct super_block *, int *, char *);

            

    void (*clear_inode) (struct inode *);

            

    void (*umount_begin) (struct super_block *);

            

    };

            

        由于这组操作中定义了文件系统中对于inode的操作,所以是之后对于文件系统中文件所有操作的基础。

            

        在给super_blocks_ops赋值后,再给该文件系统分配一个vfsmount结构,将该结构注册到系统维护的另一条链vfsmntlist中,所有mount上的文件系统都在该链中有一项。在umount时,则从链中删除这一项并且释放超级块。

            

        对于一个已经mount的文件系统中任何文件的操作首先应该以产生一个inode实例,即根据文件系统的类型生成一个属于该文件系统的内存i节点。这首先调用文件定位函数lookup_dentry查找目录缓存看是否使用过该文件,如果还没有则缓存中找不到,于是需要的i接点则依次调用路径上的所有目录I接点的lookup函数,在lookup函数中会调用iget函数,该函数中最终调用超级块的s_ops->read_inode读取目标文件的磁盘I节点(这一步再往下就是由设备驱动完成了,通过调用驱动程序的read函数读取磁盘I节点),read_inode函数的主要功能是初始化inode的一些私有数据(比如数据存储位置,文件大小等等)以及给inode_operations函数开关表赋值,最终该inode被绑定在一个目录缓存结构dentry中返回。

            

        在获得了文件的inode之后,对于该文件的其它一切操作都有了根基。因为可以从inode 获得文件操作函数开关表file_operatoins,该开关表里给出了标准的文件I/O接口的实现,包括read, write, lseek, mmap, ioctl等等。这些函数入口将是所有关于文件的系统调用请求的最终处理入口,通过这些函数入口会向存储该文件的硬设备驱动发出请求并且由驱动程序返回数据。当然这中间还会牵涉到一些关于buffer的管理问题,这里就不赘述了。

            

        通过讲述这些,我们应该明白了为什么可以使用统一的系统调用接口来访问不同文件系统类型的文件了:因为在文件系统的实现一层,都把低层的差异屏蔽了,用户可见的只是高层可见的一致的系统调用接口。

            

     
            5.与module相关的系统调用

        

    

    Linux中提供了一种动态加载或卸载内核组件的机制——模块。通过这种机制Linux用户可以为自己可以保持一个尽量小的内核映像文件,另外,往内核中加载和卸载模块不需要重新编译整个内核以及引导机器。可以通过一定的命令或者调用在一个运行的系统中加载模块,在不需要时卸载模块。模块可以完成许多功能,比如文件系统、设备驱动,系统支持的执行文件格式,甚至系统调用和中断处理都可以用模块来更新。    

    Linux中提供了往系统中添加和卸载模块的接口,create_module()init_module (), delete_module(),这些系统调用通常不是直接为程序员使用的,它们仅仅是为实现一些系统命令而提供的接口,如insmod, rmmod,(在使用这些系统调用前必须先加载目标文件到用户进程的地址空间,这必须由目标文件格式所特定的库函数(如:libobj.a中的一些函数)来完成)。

    

    Linux的核心中维护了一个module_list列表,每个被加载到核心中的模块都在其中占有一项,系统调用create_module()就是在该列表里注册某个指定的模块,而init_module则是使用模块目标文件内容的映射来初始化核心中注册的该模块,并且调用该模块的初始化函数,初始化函数通常完成一些特定的初始化操作,比如文件系统的初始化函数就是在操作系统中注册该文件系统。delete_module则是从系统中卸载一个模块,其主要工作是从module_list中删除该模块对应的module结构并且调用该模块的cleanup函数卸载其它私有信息。
     
     

    

                                                                                                       Back

    

    

            

  • Linux中怎样编译和定制内核
  •     

    

    1.编译内核前注意的事项        

        检查系统上其它资源是否符合新内核的要求。在linux/Document目录下有一个叫Changes的文件,里面列举了当前内核版本所需要的其它软件的版本号,

            

    – Kernel modutils             2.1.121                           ; insmod -V

            

    – Gnu C                       2.7.2.3                           ; gcc –version

            

    – Binutils                    2.8.1.0.23                        ; ld -v

            

    – Linux libc5 C Library       5.4.46                            ; ls -l /lib/libc*

            

    – Linux libc6 C Library       2.0.7pre6                         ; ls -l /lib/libc*

            

    – Dynamic Linker (ld.so)      1.9.9                             ; ldd –version or ldd -v

            

    – Linux C++ Library           2.7.2.8                           ; ls -l /usr/lib/libg++.so.*

            

    . . . . . .

            

    其中最后一项是列举该软件版本号的命令,如果不符合要求先给相应软件升级,这一步通常可以忽略。

            

    2.配置内核

            

        使用make config或者make menuconfig, make xconfig配置新内核。其中包括选择块设备驱动程序、网络选项、网络设备支持、文件系统等等,用户可以根据自己的需求来进行功能配置。每个选项至少有“y”和“n”两种选择,选择“y”表示把相应的支持编译进内核,选“n”表示不提供这种支持,还有的有第三种选择“m”,则表示把该支持编译成可加载模块,即前面提到的module,怎样编译和安装模块在后面会介绍。

            

        这里,顺便讲述一下如何在内核中增加自己的功能支持。

            

        假如我们现在需要在自己的内核中加入一个文件系统tfile,在完成了文件系统的代码后,在linux/fs下建立一个tfile目录,把源文件拷贝到该目录下,然后修改linux/fs下的Makefile,把对应该文件系统的目标文件加入目标文件列表中,最后修改linux/fs/Config.in文件,加入

            

    bool ‘tfile fs support’ CONFIG_TFILE_FS或者

            

    tristate ‘tfile fs support’ CONFIG_TFILE_FS

            

    这样在Make menuconfig时在filesystem选单下就可以看到

            

    < > tfile fs support一项了

            

    3.编译内核

            

        在配置好内核后就是编译内核了,在编译之前首先应该执行make dep命令建立好依赖关系,该命令将会修改linux中每个子目录下的.depend文件,该文件包含了该目录下每个目标文件所需要的头文件(绝对路径的方式列举)。

            

        然后就是使用make bzImage命令来编译内核了。该命令运行结束后将会在linux/arch/asm/boot/产生一个名叫bzImage的映像文件。

            

    4.使用新内核引导

            

        把前面编译产生的映像文件拷贝到/boot目录下(也可以直接建立一个符号连接,这样可以省去每次编译后的拷贝工作),这里暂且命名为vmlinuz-new,那么再修改/etc/lilo.conf,在其中增加这么几条:

            

    image = /boot/vmlinuz-new

            

    root = /dev/hda1

            

    label = new

            

    read-only

            

    并且运行lilo命令,那么系统在启动时就可以选用新内核引导了。

            

    5.编译模块和使用模块

        

    

linux/目录下执行make modules编译模块,然后使用命令make modules_install来安装模块(所有的可加载模块的目标文件会被拷贝到/lib/modules/2.2.12/),这样之后就可以通过执行insmod 〈模块名〉和rmmod〈模块名〉命令来加载或卸载功能模块了。

php.ini 中的几个参数设置

设置                                   描述                                                                                           建议值
max_execution_time     一个脚本可使用多少 CPU 秒                                                 30
max_input_time              一个脚本等待输入数据的时间有多长(秒)                        60
memory_limit                  在被取消之前,一个脚本可使用多少内存(字节)            32M
output_buffering             数据发送给客户机之前,有多少数据(字节)需要缓存    4096

具体数字主要取决于您的应用程序。如果要从用户处接收大文件,那么 max_input_time 可能必须增加,可以在 php.ini 中修改,也可以通过代码重写它。与之类似,CPU 或内存占用较多的程序也可能需要更大的设置值。目标就是缓解超标程序的影响,因此不建议全局禁用这些设置。关于 max_execution_time,还有一点需要注意:它表示进程的 CPU 时间,而不是绝对时间。因此一个进行大量 I/O 和少量计算的程序的运行时间可能远远超过 max_execution_time。这也是 max_input_time 可以大于 max_execution_time 的原因所在。

关于script 标签的 defer 属性

DEFER Attribute | defer Property

Sets or retrieves the status of the script.

Syntax

HTML             <SCRIPT DEFER … >
Scripting        SCRIPT.defer [ = bDefer ]

Possible Values

    

        

            

            

        

    

bDefer Boolean that specifies or receives one of the following values.            

                

                    

                        

                        

                    

                    

                        

                        

                    

                

            

false Default. Inline executable function is not deferred.
true Inline executable function is deferred.

            

The property is read/write. The property has a default value of false.

Remarks

Using the attribute at design time can improve the download performance of a page because the browser does not need to parse and execute the script and can continue downloading and parsing the page instead.

需要注意的几点:

1.使用了defer="true"属性的script标签应该放在<head></head>之间。如果放在Body之间可能会得不到预期的效果(我试过如果把script放body里,在页面加载的第一次不会起defer的作用,但之后的刷新defer却起作用)。

2.使用了defer="true"属性的script标签里面包含的脚本在运行的过程中,不能使用document.write()方法向页面输出内容。因为设置了defer="true"的脚本是页面加载之后才加载并渲染的.如果这时候使用document.write()方法,会把之前的页面内容都清掉(当然,如果有这个需要的情况除外)。

PHP生成PDF文档的FPDF类

PHP生成PDF文档的FPDF类
以前在PHP4的早期版本中用PDFlib生成PDF文档比较容易,现在升级到PHP5了,发现更麻烦了,装的PHP 5.2.4默认没有PHPlib,从php.net上找了一个,装上竟一直报错,开始以为是版本兼容问题,后来在租来的服务器上(PHP 4.3.11)也是不行,在网上搜索,看到PHPlib居然还是非免费的,算了吧,放弃!
继续搜索其他的解决方案,phpMyAdmin用的有生成PDF的功能,是TCPDF,测试发现不支持中文,所有的汉字都只显示为方格,戒烟如你初步判断为字库问题,网上也没有合适的解决办法,只好再放弃!
最后才找到一个叫FPDF的东西,简单、实用、支持中文,在PHP 5.2.4和PHP 4.3.11上运行均正常,完全符合戒烟如你的要求,吼吼~~就是你了!

  下载:http://www.fpdf.org/
中文包:http://www.fpdf.org/download/chinese.zip
中文手册:http://www.fpdf.org/en/dl.php?id=72
你也可以直接从本文的附件里下载,提供了1.5.2和1.5.3两个版本的FPDF,但中文版的手册只有1.5.2的,说的还满详细,基本可以指导操作了。

  把中文包里的chinese.php和ex.php解压到下载的FPDF包里,运行ex.php,就可以看到繁体中文显示的东西。

  把下面的代码:
$pdf->AddBig5Font();
$pdf->SetFont(‘Big5’,”,20);
替换为:
$pdf->AddGBFont();
$pdf->SetFont(‘GB’,”,20);

  再把$pdf->Write(5,’*’);的*替换成你想输出的中文,就一切OK了!再运行下ex.php试试看?

VIM 相关资源

vim 下载地址:
Linux版:wget ftp://ftp.vim.org/pub/vim/unix/vim-7.1.tar.bz2

帮助文档地址:
http://vimcdoc.sourceforge.net/
http://vimcdoc.sourceforge.net/doc/help.html

VIM帮助手册pdf
VIM在线手册

手把手教你把Vim改装成一个IDE编程环境(图文)
http://blog.csdn.net/wooin/archive/2007/12/30/2004470.aspx

Vim颜色设置
http://zywangyan54.blog.163.com/blog/static/31810358200752993227703/

Vim程序调试
http://www.wangchao.net.cn/bbsdetail_69434.html

Vim使用经验
http://blog.csdn.net/camry_camry/archive/2004/09/23/114188.aspx

Vim自动给脚本加注释
http://blog.chinaunix.net/u/6542/showart.php?id=357716

VIM 插件大全 及 不错介绍
http://hi.baidu.com/00%C6%F3%B6%EC/blog/item/fd456c03a2d40f8bd53f7c29.html

Vim即学即用
http://blog.linuxpk.com/3973/viewspace-2644

小技巧:

全文档代码格式化,命令模式下: gg=G 即可搞定;
当前行代码格式化,命令模式下: == 即可搞定;
代码块格式化,命令模式下: =a{ ; 格式化{}里面的代码
更多的还要看上面的手册呢

删除所有空行: :g /^$/d #注意不要 :% s/^$//

 

linux内核网络栈代码的准备知识

一.linux内核网络栈代码的准备知识
 
1. linux内核ipv4网络部分分层结构
 
BSD socket层: 这一部分处理BSD socket相关操作,每个socket在内核中以struct socket结构体现。这一部分的文件
 
主要有:/net/socket.c /net/protocols.c etc

INET socket层:BSD socket是个可以用于各种网络协议的接口,而当用于tcp/ip,即建立了AF_INET形式的socket时,

 
还需要保留些额外的参数,于是就有了struct sock结构。文件主要
 
有:/net/ipv4/protocol.c /net/ipv4/af_inet.c /net/core/sock.c etc

TCP/UDP层:处理传输层的操作,传输层用struct inet_protocol和struct proto两个结构表示。文件主要

 
有:/net/ipv4/udp.c /net/ipv4/datagram.c /net/ipv4/tcp.c /net/ipv4/tcp_input.c /net/ipv4//tcp_output.c /net/ipv4/tcp_minisocks.c /net/ipv4/tcp_output.c /net/ipv4/tcp_timer.c
 
etc  
     
IP层:处理网络层的操作,网络层用struct packet_type结构表示。文件主要有:/net/ipv4/ip_forward.c
ip_fragment.c ip_input.c ip_output.c etc.

数据链路层和驱动程序:每个网络设备以struct net_device表示,通用的处理在dev.c中,驱动程序都在/driver/net目

 
录下。
 
2. 两台主机建立udp通信所走过的函数列表
 
^
|       sys_read                fs/read_write.c
|       sock_read               net/socket.c
|       sock_recvmsg            net/socket.c
|       inet_recvmsg            net/ipv4/af_inet.c
|       udp_recvmsg             net/ipv4/udp.c
|       skb_recv_datagram       net/core/datagram.c
|       ——————————————-
|       sock_queue_rcv_skb      include/net/sock.h
|       udp_queue_rcv_skb       net/ipv4/udp.c
|       udp_rcv                 net/ipv4/udp.c
|       ip_local_deliver_finish net/ipv4/ip_input.c
|       ip_local_deliver        net/ipv4/ip_input.c
|       ip_recv                 net/ipv4/ip_input.c
|       net_rx_action           net/dev.c
|       ——————————————-
|       netif_rx                net/dev.c
|       el3_rx                  driver/net/3c309.c
|       el3_interrupt           driver/net/3c309.c

==========================

|       sys_write               fs/read_write.c
|       sock_writev             net/socket.c                    
|       sock_sendmsg            net/socket.c
|       inet_sendmsg            net/ipv4/af_inet.c
|       udp_sendmsg             net/ipv4/udp.c
|       ip_build_xmit           net/ipv4/ip_output.c
|       output_maybe_reroute    net/ipv4/ip_output.c
|       ip_output               net/ipv4/ip_output.c
|       ip_finish_output        net/ipv4/ip_output.c
|       dev_queue_xmit          net/dev.c
|       ——————————————–
|       el3_start_xmit          driver/net/3c309.c
V

 
 
3. 网络路径图、重要数据结构sk_buffer及路由介绍
 
    linux-net.pdf 第2.1章 第2.3章 第2.4章
    
4. 从连接、发送、到接收数据包的过程
 
    linux-net.pdf 第4、5、6章详细阐述
 
 
二.linux的tcp-ip栈代码的详细分析
 
1.数据结构(msghdr,sk_buff,socket,sock,proto_ops,proto)
 
bsd套接字层,操作的对象是socket,数据存放在msghdr这样的数据结构:
 
创建socket需要传递family,type,protocol三个参数,创建socket其实就是创建一个socket实例,然后创建一个文件描述符结构,并且互相建立一些关联,即建立互相连接的指针,并且初始化这些对文件的写读操作映射到socket的read,write函数上来。
 
同时初始化socket的操作函数(proto_ops结构),如果传入的type参数是STREAM类型,那么就初始化为SOCKET->ops为inet_stream_ops,如果是DGRAM类型,则SOCKET-ops为inet_dgram_ops。对于inet_stream_ops其实是一个结构体,包含了stream类型的socket操作的一些入口函数,在这些函数里主要做的是对socket进行相关的操作,同时通过调用下面提到的sock中的相关操作完成socket到sock层的传递。比如在inet_stream_ops里有个inet_release的操作,这个操作除了释放socket的类型空间操作外,还通过调用socket连接的sock的close操作,对于stream类型来说,即tcp_close来关闭sock
释放sock。
 
创建socket同时还创建sock数据空间,初始化sock,初始化过程主要做的事情是初始化三个队列,receive_queue(接收到的数据包sk_buff链表队列),send_queue(需要发送数据包的sk_buff链表队列),backlog_queue(主要用于tcp中三次握手成功的那些数据包,自己猜的),根据family、type参数,初始化sock的操作,比如对于family为inet类型的,type为stream类型的,sock->proto初始化为tcp_prot.其中包括stream类型的协议sock操作对应的入口函数。
 
在一端对socket进行write的过程中,首先会把要write的字符串缓冲区整理成msghdr的数据结构形式(参见linux内核2.4版源代码分析大全),然后调用sock_sendmsg把msghdr的数据传送至inet层,对于msghdr结构中数据区中的每个数据包,创建sk_buff结构,填充数据,挂至发送队列。一层层往下层协议传递。一下每层协议不再对数据进行拷贝。而是对sk_buff结构进行操作。
 
inet套接字及以下层 数据存放在sk_buff这样的数据结构里:
 
路由:
    
    在linux的路由系统主要保存了三种与路由相关的数据,第一种是在物理上和本机相连接的主机地址信息表,第二种是保存了在网络访问中判断一个网络地址应该走什么路由的数据表;第三种是最新使用过的查询路由地址的缓存地址数据表。
    1.neighbour结构  neighbour_table{ }是一个包含和本机所连接的所有邻元素的信息的数据结构。该结构中有个元素是neighbour结构的数组,数组的每一个元素都是一个对应于邻机的neighbour结构,系统中由于协议的不同,会有不同的判断邻居的方式,每种都有neighbour_table{}类型的实例,这些实例是通过neighbour_table{}中的指针next串联起来的。在neighbour结构中,包含有与该邻居相连的网络接口设备net_device的指针,网络接口的硬件地址,邻居的硬件地址,包含有neigh_ops{}指针,这些函数指针是直接用来连接传输数据的,包含有queue_xmit(struct * sk_buff)函数入口地址,这个函数可能会调用硬件驱动程序的发送函数。
 
    2.FIB结构 在FIB中保存的是最重要的路由规则,通过对FIB数据的查找和换算,一定能够获得路由一个地址的方法。系统中路由一般采取的手段是:先到路由缓存中查找表项,如果能够找到,直接对应的一项作为路由的规则;如果不能找到,那么就到FIB中根据规则换算传算出来,并且增加一项新的,在路由缓存中将项目添加进去。
    3.route结构(即路由缓存中的结构)
 
 
 
数据链路层:
  
   net_device{}结构,对应于每一个网络接口设备。这个结构中包含很多可以直接获取网卡信息的函数和变量,同时包含很多对于网卡操作的函数,这些直接指向该网卡驱动程序的许多函数入口,包括发送接收数据帧到缓冲区等。当这些完成后,比如数据接收到缓冲区后便由netif_rx(在net/core/dev.c各种设备驱动程序的上层框架程序)把它们组成sk_buff形式挂到系统接收的backlog队列然后交由上层网络协议处理。同样,对于上层协议处理下来的那些sk_buff。便由dev_queue_xmit函数放入网络缓冲区,交给网卡驱动程序的发送程序处理。
 
   在系统中存在一张链表dev_base将系统中所有的net_device{}结构连在一起。对应于内核初始化而言,系统启动时便为每个所有可能支持的网络接口设备申请了一个net_device{}空间并串连起来,然后对每个接点运行检测过程,如果检测成功,则在dev_base链表中保留这个接点,否则删除。对应于模块加载来说,则是调用register_netdev()注册net_device,在这个函数中运行检测过程,如果成功,则加到dev_base链表。否则就返回检测不到信息。删除同理,调用
unregister_netdev。
 
 
2.启动分析
 
    2.1 初始化进程 :start-kernel(main.c)—->do_basic_setup(main.c)—->sock_init(/net/socket.c)—->do_initcalls(main.c)
 
void __init sock_init(void)
{
 int i;
 
 printk(KERN_INFO "Linux NET4.0 for Linux 2.4\n");
 printk(KERN_INFO "Based upon Swansea University Computer Society NET3.039\n");
 
 /*
  * Initialize all address (protocol) families. 每一项表示的是针对一个地址族的操作集合,例如对于ipv4来说,在net/ipv4/af_inet.c文件中的函数inet_proto_init()就调用sock_register()函数将inet_families_ops初始化到属于IPV4的net_families数组中的一项。
  */
 
 for (i = 0; i < NPROTO; i++)
  net_families = NULL;  
 
 /*
  * Initialize sock SLAB cache.初始化对于sock结构预留的内存的slab缓存。
  */
 
 sk_init();
 
#ifdef SLAB_SKB
 /*
  * Initialize skbuff SLAB cache 初始化对于skbuff结构的slab缓存。以后对于skbuff的申请可以通过函数kmem_cache_alloc()在这个缓存中申请空间。
  */
 skb_init();
#endif
 
 /*
  * Wan router layer.
  */
 
#ifdef CONFIG_WAN_ROUTER 
 wanrouter_init();
#endif
 
 /*
  * Initialize the protocols module. 向系统登记sock文件系统,并且将其安装到系统上来。
  */
 
 register_filesystem(&sock_fs_type);
 sock_mnt = kern_mount(&sock_fs_type);
 /* The real protocol initialization is performed when
  *  do_initcalls is run. 
  */
 /*
  * The netlink device handler may be needed early.
  */
 
#ifdef CONFIG_NET
 rtnetlink_init();
#endif
#ifdef CONFIG_NETLINK_DEV
 init_netlink();
#endif
#ifdef CONFIG_NETFILTER
 netfilter_init();
#endif
 
#ifdef CONFIG_BLUEZ
 bluez_init();
#endif
 
/*yfhuang ipsec*/
#ifdef CONFIG_IPSEC            
 pfkey_init();
#endif
/*yfhuang ipsec*/
}
 
 
    2.2 do_initcalls() 中做了其它的初始化,其中包括
 
                协议初始化,路由初始化,网络接口设备初始化
 
(例如inet_init函数以_init开头表示是系统初始化时做,函数结束后跟module_init(inet_init),这是一个宏,在include/linux/init.c中定义,展开为_initcall(inet_init),表示这个函数在do_initcalls被调用了)
 
    2.3 协议初始化
此处主要列举inet协议的初始化过程。
 
static int __init inet_init(void)
{
 struct sk_buff *dummy_skb;
 struct inet_protocol *p;
 struct inet_protosw *q;
 struct list_head *r;
 
 printk(KERN_INFO "NET4: Linux TCP/IP 1.0 for NET4.0\n");
 
 if (sizeof(struct inet_skb_parm) > sizeof(dummy_skb->cb)) {
  printk(KERN_CRIT "inet_proto_init: panic\n");
  return -EINVAL;
 }
 
 /*
  * Tell SOCKET that we are alive… 注册socket,告诉socket inet类型的地址族已经准备好了
  */
  
   (void) sock_register(&inet_family_ops);
 
 /*
  * Add all the protocols. 包括arp,ip、ICMP、UPD、tcp_v4、tcp、igmp的初始化,主要初始化各种协议对应的inode和socket变量。
 
其中arp_init完成系统中路由部分neighbour表的初始化
 
ip_init完成ip协议的初始化。在这两个函数中,都通过定义一个packet_type结构的变量将这种数据包对应的协议发送数据、允许发送设备都做初始化。
  */
 
 printk(KERN_INFO "IP Protocols: ");
 for (p = inet_protocol_base; p != NULL;) {
  struct inet_protocol *tmp = (struct inet_protocol *) p->next;
  inet_add_protocol(p);
  printk("%s%s",p->name,tmp?", ":"\n");
  p = tmp;
 }
 
 /* Register the socket-side information for inet_create. */
 for(r = &inetsw[0]; r < &inetsw[SOCK_MAX]; ++r)
  INIT_LIST_HEAD(r);
 
 for(q = inetsw_array; q < &inetsw_array[INETSW_ARRAY_LEN]; ++q)
  inet_register_protosw(q);
 
 /*
  * Set the ARP module up 
  */
 
 arp_init();
 
   /*
    * Set the IP module up
    */
 
 ip_init();
 
 tcp_v4_init(&inet_family_ops);
 
 /* Setup TCP slab cache for open requests. */
 tcp_init();
 /*
  * Set the ICMP layer up
  */
 
 icmp_init(&inet_family_ops);
 
 /* I wish inet_add_protocol had no constructor hook…
    I had to move IPIP from net/ipv4/protocol.c 🙁 –ANK
  */
#ifdef CONFIG_NET_IPIP
 ipip_init();
#endif
#ifdef CONFIG_NET_IPGRE
 ipgre_init();
#endif
 
 /*
  * Initialise the multicast router
  */
#if defined(CONFIG_IP_MROUTE)
 ip_mr_init();
#endif
 
 /*
  * Create all the /proc entries.
  */
#ifdef CONFIG_PROC_FS
 proc_net_create ("raw", 0, raw_get_info);
 proc_net_create ("netstat", 0, netstat_get_info);
 proc_net_create ("snmp", 0, snmp_get_info);
 proc_net_create ("sockstat", 0, afinet_get_info);
 proc_net_create ("tcp", 0, tcp_get_info);
 proc_net_create ("udp", 0, udp_get_info);
#endif  /* CONFIG_PROC_FS */
 
 ipfrag_init();
 
 return 0;
}   
module_init(inet_init);
                                                 
 
     2.4 路由初始化(包括neighbour表、FIB表、和路由缓存表的初始化工作)
 
            2.4.1 rtcache表 ip_rt_init()函数 在net/ipv4/ip_output中调用,net/ipv4/route.c中定义
 
            2.4.2 FIB初始化 在ip_rt_init()中调用 在net/ipv4/fib_front.c中定义
 
           2.4.3 neigbour表初始化  arp_init()函数中定义 
 
     2.5 网络接口设备初始化
             
             在系统中网络接口都是由一个dev_base链表进行管理的。通过内核的启动方式也是通过这个链表进行操作的。在系统启动之初,将所有内核能够支持的网络接口都初始化成这个链表中的一个节点,并且每个节点都需要初始化出init函数指针,用来检测网络接口设备。然后,系统遍历整个dev_base链表,对每个节点分别调用init函数指针,如果成功,证明网络接口设备可用,那么这个节点就可以进一步初始化,如果返回失败,那么证明该网络设备不存在或是不可用,只能将该节点删除。启动结束之后,在dev_base中剩下的都是可以用的网络接口设备。
 
            2.5.1 do_initcalls—->net_dev_init()(net/core/dev.c)——>ethif_probe()(drivers/net/Space.c,在netdevice{}结构的init中调用,这边ethif_probe是以太网卡针对的调用)
 
 
 
3.网络设备驱动程序(略)
        
 
4.网络连接
 
     4.1 连接的建立和关闭
 
            tcp连接建立的代码如下:
                    server=gethostbyname(SERVER_NAME);
                    sockfd=socket(AF_INET,SOCK_STREAM,0);
                    address.sin_family=AF_INET;
                    address.sin_port=htons(PORT_NUM);
                    memcpy(&address.sin_addr,server->h_addr,server->h_length);
                    connect(sockfd,&address,sizeof(address));
 
       连接的初始化与建立期间主要发生的事情如下:
                      
       1)sys_socket调用:调用socket_creat(),创建出一个满足传入参数family、type、和protocol的socket,调用sock_map_fd()获取一个未被使用的文件描述符,并且申请并初始化对应的file{}结构。
        
       2)sock_creat():创建socket结构,针对每种不同的family的socket结构的初始化,就需要调用不同的create函数来完成。对应于inet类型的地址来说,在网络协议初始化时调用sock_register()函数中完成注册的定义如下:
        struct net_proto_family inet_family_ops={
                PF_INET;
                inet_create
        };所以inet协议最后会调用inet_create函数。
        
       3)inet_create: 初始化sock的状态设置为SS_UNCONNECTED,申请一个新的sock结构,并且初始化socket的成员ops初始化为inet_stream_ops,而sock的成员prot初始化为tcp_prot。然后调用sock_init_data,将该socket结构的变量sock和sock类型的变量关联起来。
 
       4)在系统初始化完毕后便是进行connect的工作,系统调用connect将一个和socket结构关联的文件描述符和一个sockaddr{}结构的地址对应的远程机器相关联,并且调用各个协议自己对应的connect连接函数。对应于tcp类型,则sock->ops->connect便为inet_stream_connect。
 
 
       5)inet_stream_connect: 得到sk,sk=sock->sk,锁定sk,对自动获取sk的端口号存放在sk->num中,并且用htons()函数转换存放在sk->sport中。然后调用sk->prot->connect()函数指针,对tcp协议来说就是tcp_v4_connect()函数。然后将sock->state状态字设置为SS_CONNECTING,等待后面一系列的处理完成之后,就将状态改成SS_CONNECTTED。
 
       6) tcp_v4_connect():调用函数ip_route_connect(),寻找合适的路由存放在rt中。ip_route_connect找两次,第一次找到下一跳的ip地址,在路由缓存或fib中找到,然后第二次找到下一跳的具体邻居,到neigh_table中找到。然后申请出tcp头的空间存放在buff中。将sk中相关地址数据做一些针对路由的变动,并且初始化一个tcp连接的序列号,调用函数tcp_connect(),初始化tcp头,并设置tcp处理需要的定时器。一次connect()建立的过程就结束了。
 
       连接的关闭主要如下:
 
        1)close: 一个socket文件描述符对应的file{}结构中,有一个file_operations{}结构的成员f_ops,它的初始化关闭函数为sock_close函数。
 
        2)sock_close:调用函数sock_release(),参数为一个socket{}结构的指针。
 
        3)sock_release:调用inet_release,并释放socket的指针和文件空间
 
        4)inet_release: 调用和该socket对应协议的关闭函数inet_release,如果是tcp协议,那么调用的是tcp_close;最后释放sk。
 
        4.2 数据发送流程图
 
 
 
各层主要函数以及位置功能说明:
        1)sock_write:初始化msghdr{}结构 net/socket.c
        2)sock_sendmsg:net/socket.c
        3)inet_sendmsg:net/ipv4/af_net.c
        4)tcp_sendmsg:申请sk_buff{}结构的空间,把msghdr{}结构中的数据填入sk_buff空间。net/ipv4/tcp.c
        5)tcp_send_skb:net/ipv4/tcp_output.c
        6)tcp_transmit_skb:net/ipv4/tcp_output.c
        7)ip_queue_xmit:net/ipv4/ip_output.c
        8)ip_queue_xmit2:net/ipv4/ip_output.c
        9)ip_output:net/ipv4/ip_output.c
        10)ip_finish_output:net/ipv4/ip_output.c
        11)ip_finish_output2:net/ipv4/ip_output.c
        12)neigh_resolve_output:net/core/neighbour.c
        13)dev_queue_xmit:net/core/dev.c
 
 
        4.3 数据接收流程图
 
各层主要函数以及位置功能说明:
 
        1)sock_read:初始化msghdr{}的结构类型变量msg,并且将需要接收的数据存放的地址传给msg.msg_iov->iov_base.      net/socket.c
        2)sock_recvmsg: 调用函数指针sock->ops->recvmsg()完成在INET Socket层的数据接收过程.其中sock->ops被初始化为inet_stream_ops,其成员recvmsg对应的函数实现为inet_recvmsg()函数. net/socket.c
        3)sys_recv()/sys_recvfrom():分别对应着面向连接和面向无连接的协议两种情况. net/socket.c
        4)inet_recvmsg:调用sk->prot->recvmsg函数完成数据接收,这个函数对于tcp协议便是tcp_recvmsg net/ipv4/af_net.c
        5)tcp_recvmsg:从网络协议栈接收数据的动作,自上而下的触发动作一直到这个函数为止,出现了一次等待的过程.函数tcp_recvmsg可能会被动地等待在sk的接收数据队列上,也就是说,系统中肯定有其他地方会去修改这个队列使得tcp_recvmsg可以进行下去.入口参数sk是这个网络连接对应的sock{}指针,msg用于存放接收到的数据.接收数据的时候会去遍历接收队列中的数据,找到序列号合适的.
        但读取队列为空时tcp_recvmsg就会调用tcp_v4_do_rcv使用backlog队列填充接收队列.
        6)tcp_v4_rcv:tcp_v4_rcv被ip_local_deliver函数调用,是从IP层协议向INET Socket层提交的"数据到"请求,入口参数skb存放接收到的数据,len是接收的数据的长度,这个函数首先移动skb->data指针,让它指向tcp头,然后更新tcp层的一些数据统计,然后进行tcp的一些值的校验.再从INET Socket层中已经建立的sock{}结构变量中查找正在等待当前到达数据的哪一项.可能这个sock{}结构已经建立,或者还处于监听端口、等待数据连接的状态。返回的sock结构指针存放在sk中。然后根据其他进程对sk的操作情况,将skb发送到合适的位置.调用如下:
 
        TCP包接收器(tcp_v4_rcv)将TCP包投递到目的套接字进行接收处理. 当套接字正被用户锁定,TCP包将暂时排入该套接字的后备队列(sk_add_backlog).这时如果某一用户线程企图锁定该套接字(lock_sock),该线程被排入套接字的后备处理等待队列(sk->lock.wq).当用户释放上锁的套接字时(release_sock,在tcp_recvmsg中调用),后备队列中的TCP包被立即注入TCP包处理器(tcp_v4_do_rcv)进行处理,然后唤醒等待队列中最先的一个用户来获得其锁定权. 如果套接字未被上锁,当用户正在读取该套接字时, TCP包将被排入套接字的预备队列(tcp_prequeue),将其传递到该用户线程上下文中进行处理.如果添加到sk->prequeue不成功,便可以添加到 sk->receive_queue队列中(用户线程可以登记到预备队列,当预备队列中出现第一个包时就唤醒等待线程.)   /net/tcp_ipv4.c
 
        7)ip_rcv、ip_rcv_finish:从以太网接收数据,放到skb里,作ip层的一些数据及选项检查,调用ip_route_input()做路由处理,判断是进行ip转发还是将数据传递到高一层的协议.调用skb->dst->input函数指针,这个指针的实现可能有多种情况,如果路由得到的结果说明这个数据包应该转发到其他主机,这里的input便是ip_forward;如果数据包是给本机的,那么input指针初始化为ip_local_deliver函数./net/ipv4/ip_input.c
 
        8)ip_local_deliver、ip_local_deliver_finish:入口参数skb存放需要传送到上层协议的数据,从ip头中获取是否已经分拆的信息,如果已经分拆,则调用函数ip_defrag将数据包重组。然后通过调用ip_prot->handler指针调用tcp_v4_rcv(tcp)。ip_prot是inet_protocol结构指针,是用来ip层登记协议的,比如由udp,tcp,icmp等协议。 /net/ipv4/ip_input.c
 

linux下socket通信之通信模型

  导读:
  1.Socket简介
  Socket是TCP/IP网络的API,可以用它来开发网络应用程序,Socket数据传输是一种特殊的I/O,Socket也是一种文件描述符
  2.Socket的建立
  int socket(int domain, int type, int protocol)
  函数返回:一个整型的Socket描述符,可以在后面调用它。
  参数说明:
  int domain:指明所使用的协议族, 通常是PF_INET, 表示网络(TCP/IP)协议族说明我们网络程序所在的主机采用的通讯协族(AF_UNIX和AF_INET等).
  AF_UNIX:只能够用于单一的Unix系统进程间通信,
  AF_INET:是针对Internet的,因而可以允许在远程主机之间通信(当我们man socket时发现domain可选项是 PF_*而不是AF_*,因为glibc是posix的实现所以用PF代替了AF,不过我们都可以使用的)
  int type:指定socket的类型, 通常是 SOCK_STREAM 流式Socket这样会提供按顺序的,可靠,双向,面向连接的比特流和SOCK_DGRAM数据报式Socket这样只会提供定长的,不可靠,无连接的通信
  int prottocol:通常为0 由于我们指定了type,所以这个地方我们一般只要用0来代替就可以了
  应用示例:int sockfd = socket(PF_INET, SOCK_STREAM, 0);
  
  3.Socket配置
  Socket描述符是一个指向内部数据结构的指针,它指向描述符表入口。调用Socket函数时,socket执行体将建立一个Socket,实际上"建立一个Socket"意味着为一个Socket数据结构分配存储空间。Socket执行体为你管理描述符表。
  两个网络程序之间的一个网络连接包括五种信息:通信协议、本地协议地址、本地主机端口、远端主机地址和远端协议端口。Socket数据结构中包含这五种信息。
  通过socket调用返回一个socket描述符后,在使用socket进行网络传输以前,必须配置该socket:
  1) 面向连接的socket客户端通过调用Connect函数在socket数据结构中保存本地和远端信息。
  2) 无连接socket的客户端和服务端以及面向连接socket的服务端通过调用bind函数来配置本地信息。
  
  4.Bind()
  Bind函数将socket与本机上的一个端口相关联,随后你就可以在该端口监听服务请求。
  函数原型:int bind(int sockfd, struct sockaddr *my_addr, int addrlen);
  函数返回:成功被调用时返回0;出现错误时返回"-1"并将errno置为相应的错误号。
  参数说明:
  Sockfd:是调用socket函数返回的socket描述符,
  my_addr:是一个指向包含有本机IP地址及端口号等信息的sockaddr类型的指针;
  addrlen:常被设置为sizeof(struct sockaddr)。
  1> struct sockaddr结构类型是用来保存socket信息的:
  struct sockaddr
  {
  unsigned short sa_family; /* 地址族, AF_xxx */
  char sa_data[14]; /* 14 字节的协议地址 */
  };
  sa_family:一般为AF_INET,代表Internet(TCP/IP)地址族;
  sa_data:则包含该socket的IP地址和端口号。
  2>sockaddr_in结构类型:
  struct sockaddr_in
  {
  short int sin_family; /* 地址族 */
  unsigned short int sin_port; /* 端口号 */
  struct in_addr sin_addr; /* IP地址 */
  unsigned char sin_zero[8]; /* 填充0 以保持与struct sockaddr同样大小sin_zero用来将sockaddr_in结构填充到与struct sockaddr同样的长度,可以用bzero()或memset()函数将其置为零。 */
  };
  这个结构更方便使用。
  指向sockaddr_in 的指针和指向sockaddr的指针可以相互转换,这意味着如果一个函数所需参数类型是sockaddr时,你可以在函数调用的时候将一个指向ockaddr_in的指针转换为指向sockaddr的指针;或者相反。使用bind函数时,可以自动获得本机IP地址和随机获取一个没有被占用的端口号:系统随机选择一个未被使用的端口号,通过将my_addr.sin_port置为0,函数会自动为你选择一个未占用的端口来使用。填入本机IP地址:通过将my_addr.sin_addr.s_addr置为INADDR_ANY,系统会自动填入本机IP地址。
  注意在使用bind函数是需要将sin_port和sin_addr转换成为网络字节优先顺序;而sin_addr则不需要转换。计算机数据存储有两种字节优先顺序:高位字节优先和低位字节优先: Internet上数据以高位字节优先顺序在网络上传输,所以对于在内部是以低位字节优先方式存储数据的机器,在Internet上传输数据时就需要进行转换,否则就会出现数据不一致。
  下面是几个字节顺序转换函数: (h: host n: network l: long s: short)
  htonl():把32位值从主机字节序转换成网络字节序, 转为高位字节优先
  htons():把16位值从主机字节序转换成网络字节序, 转为高位字节优先
  ntohl():把32位值从网络字节序转换成主机字节序, 从高位字节优先转换
  ntohs():把16位值从网络字节序转换成主机字节序, 从高位字节优先转换
  需要注意的是,在调用bind函数时一般不要将端口号置为小于1024的值,因为1到1024是保留端口号,你可以选择大于1024中的任何一个没有被占用的端口号。
  应用示例:
  A)服务端
  1)建立结构变量
  struct sockaddr_in my_addr;
  int SERVPORT;
  
  2)配置协议族、端口、地址、sin_zero填充位
  my_addr.sin_family = AF_INET;
  my_addr.sin_port = htons(SERVPORT);
  my_addr.sin_addr.s_addr = INADDR_ANY;
  bzero(&(my_addr.sin_zero), 8);
  
  3)把sockfd的本地端口、IP地址、连接协议进行绑定
  if( bind(sockfd, (struct sockaddr *)&my_addr, sizeof(struct sockaddr))== -1)
  {
  perror("bind");
  return 1;
  }
  
  
  B)客户端
  1)建立结构体变量和端口号
  struct sockaddr_in serv_addr;
  struct hostent *host;
  int SERVPORT;
  
  
  //struct hostent *host; 把服务器端IP通过gethostbyname赋给host结构体,如果传入的是域名则转为IP地址再赋值
  if((host = gethostbyname(“www.800hr.com”)) == NULL)
  //或 if((host = gethostbyname(“192.168.0.1”)) == NULL)
  {
  herror("gethostbyname error");
  return 1;
  }
  else
  {//输出IP地址: xxx.xxx.xxx.xxx
  printf("host: %s\n", inet_ntoa(*((struct in_addr*)host->h_addr)));
  }
  
  
  2)建立Socket
  if((sockfd = socket(AF_INET, SOCK_STREAM, 0)) == -1)
  {
  perror("create sock");
  return 1;
  }
  
  3)给服务端结构变量赋值
  serv_addr.sin_family = AF_INET;
  serv_addr.sin_port = htons(SERVPORT);
  serv_addr.sin_addr = *((struct in_addr *)host->h_addr);
  bzero(&(serv_addr.sin_zero), 8);
  
  4)连接服务端
  //int connect(int socfd, struct sockaddr *serv_addr, int addrlen)
  进行客户端程序设计无须调用bind(),因为这种情况下只需知道目的机器的IP地址,而客户通过哪个端口与服务器建立连接并不需要关心,socket执行体为你的程序自动选择一个未被占用的端口,并通知你的程序数据什么时候到达端口
  
  Connect函数启动和远端主机的直接连接。只有面向连接的客户程序使用socket时才需要将此socket与远端主机相连。
  面向连接的服务器从不启动一个连接,它只是被动的在协议端口监听客户的请求。
  无连接协议从不建立直接连接。
  
  if(connect(sockfd, (struct sockaddr *)&serv_addr, sizeof(struct sockaddr)) == -1)
  {
  perror("create sock");
  return 1;
  }
  
  5.Listen()
  Listen函数使socket处于被动的监听模式,为该socket建立一个输入数据队列,将到达的服务请求保存在此队列中,直到程序处理它们。
  函数原型:int listen(int socfd, int backlog)
  参数说明:
  sockfd 是socket()函数返回的socket描述符
  backlog 指定在请求队列中允许的最大请求数, 进入的连接请求将在队列中等待accept它们
  如果一个服务请求到来时,输入队列己满, 此socket将拒绝连接请求,客户收到一个出错信息。
  应用实例:
  int BACKLOG = 20;
  if(listen(sockfd, BACKLOG) == -1)
  {
  perror("listen");
  return 1;
  }
  
  6.accept()
  在建立好输入队列后,服务器就调用accept函数,然后睡眠并等待客户的连接请求。
  函数原型:int accept(int sockfd, void *addr, int *addrlen);
  参数说明:
  sockfd 被监听的socket描述符
  addr sockaddr_in变量的指针,该变量用来存放请求服务的客户机的信息
  addrlen 通常是sizeof(struct sockaddr_in)
  返回值:-1出错,client_fd 成功
  [注]:当accept函数监视的socket收到连接请求时,socket执行体将建立一个新的socket,执行体将这个新socket和请求连接进程的地址联系起来,收到服务请求的初始socket仍可以继续在以前的 socket上监听,同时可以在新的socket描述符上进行数据传输操作。
  应用实例:
  int client_fd;
  sin_size = sizeof(struct sockaddr_in);
  if((client_fd = accept(sockfd, (struct sockaddr *)&remote_addr.sin_addr, &sin_size)) == -1)
  {
  perror("accept");
  continue;
  }
  
  7.send()
  函数原型:int send(int sockfd, const void *msg, int len, int flags);
  参数列表:
  Sockfd:接收数据的socket方的id
  msg: 要发送数据的指针
  len: 以字节为单位的数据的长度
  flags: 一般为0
  返回值:失败 -1,成功 发送成功的字节数
  应用实例:
  char *msg = "Hello!";
  int len, bytes_sent;
  。。。
  len = strlen(msg);
  bytes_sent = send(client_fd, msg,len,0);
  。。。
  
  8.recv()
  函数原型:int recv(int sockfd, void *buf, int len, unsigned int flags);
  参数列表:
  Sockfd:接收数据的socket的fd
  Buf: 存放数据的数据缓冲区
  Len: len是缓冲的长度
  flags: 通常为0
  返回值:成功 实际接收的字节数,错误 -1。
  应用实例:
  while(1)
  {
  recvbytes = recv(sockfd, buf, MAXDATASIZE, 0);
  if(recvbytes <= 0)
  break;
  fwrite(buf,1,recvbytes, fp);
  };
  
  recv和send
  recv和send函数提供了和read和write差不多的功能.不过它们提供 了第四个参数来控制读写操作.
  int recv(int sockfd,void *buf,int len,int flags)
  int send(int sockfd,void *buf,int len,int flags)
  前面的三个参数和read,write一样,第四个参数可以是0或者是以下的组合
  MSG_DONTROUTE:不查找路由表
  MSG_OOB:接受或者发送带外数据
  MSG_PEEK:查看数据,并不从系统缓冲区移走数据
  MSG_WAITALL:等待所有数据
  MSG_DONTROUTE:是send函数使用的标志.这个标志告诉IP协议.目的主机在本地网络上面,没有必要查找路由表.这个标志一般用网络诊断和路由程序里面。
  MSG_OOB:表示可以接收和发送带外的数据.关于带外数据我们以后会解释的。
  MSG_PEEK:是recv函数的使用标志,表示只是从系统缓冲区中读取内容,而不清楚系统缓冲区的内容.这样下次读的时候,仍然是一样的内容.一般在有多个进程读写数据时可以使用这个标志。
  MSG_WAITALL:是recv函数的使用标志,表示等到所有的信息到达时才返回.使用这个标志的时候recv回一直阻塞,直到指定的条件满足,或者是发生了错误。
  1)当读到了指定的字节时,函数正常返回。返回值等于len
  2)当读到了文件的结尾时,函数正常返回.返回值小于len
  3) 当操作发生错误时,返回-1,且设置错误为相应的错误号(errno)。
  如果flags为0,则和read,write一样的操作.还有其它的几个选项,不过我们实际上用的很少,可以查看 Linux Programmer’s Manual得到详细解释。
  
  recvfrom和sendto
  这两个函数一般用在非套接字的网络程序当中(UDP),我们已经在前面学会了。
  recvmsg和sendmsg
  recvmsg和sendmsg可以实现前面所有的读写函数的功能.
  int recvmsg(int sockfd,struct msghdr *msg,int flags)
  int sendmsg(int sockfd,struct msghdr *msg,int flags)
  struct msghdr
  {
  void *msg_name;
  int msg_namelen;
  struct iovec *msg_iov;
  int msg_iovlen;
  void *msg_control;
  int msg_controllen;
  int msg_flags;
  }
  
  struct iovec
  {
  void *iov_base; /* 缓冲区开始的地址 */
  size_t iov_len; /* 缓冲区的长度 */
  }
  msg_name和 msg_namelen当套接字是非面向连接时(UDP),它们存储接收和发送方的地址信息.msg_name实际上是一个指向struct sockaddr的指针,msg_name是结构的长度.当套接字是面向连接时,这两个值应设为NULL. msg_iov和 msg_iovlen指出接受和发送的缓冲区内容.msg_iov是一个结构指针,msg_iovlen指出这个结构数组的大小. msg_control和msg_controllen这两个变量是用来接收和发送控制数据时的 msg_flags指定接受和发送的操作选项.和 recv,send的选项一样
  
  9.套接字的关闭
  关闭套接字有两个函数close和shutdown.用close时和我们关闭文件一样.
  shutdown
  int shutdown(int sockfd,int howto)
  TCP连接是双向的(是可读写的),当我们使用close时,会把读写通道都关闭,有时侯我们希望只关闭一个方向,这个时候我们可以使用shutdown.针对不同的howto,系统回采取不同的关闭方式。Howto = 0这个时候系统会关闭读通道.但是可以继续往接字描述符写.,howto=1关闭写通道,和上面相反,着时候就只可以读了。howto=2关闭读写通道,和close一样 在多进程程序里面,如果有几个子进程共享一个套接字时,如果我们使用shutdown, 那么所有的子进程都不能够操作了,这个时候我们只能够使用close来关闭子进程的套接字描述符.
  [附]
  Sendto()和recvfrom()用于在无连接的数据报socket方式下进行数据传输。由于本地socket并没有与远端机器建立连接,所以在发送数据时应指明目的地址。
  sendto()函数原型为:int sendto(int sockfd, const void *msg,int len,unsigned int flags,const struct sockaddr *to, int tolen);
  该函数比send()函数多了两个参数,to表示目地机的IP地址和端口号信息,而tolen常常被赋值为sizeof (struct sockaddr)。Sendto 函数也返回实际发送的数据字节长度或在出现发送错误时返回-1。
  Recvfrom()函数原型为:int recvfrom(int sockfd,void *buf,int len,unsigned int flags,struct sockaddr *from,int *fromlen);
  from是一个struct sockaddr类型的变量,该变量保存源机的IP地址及端口号。fromlen常置为sizeof (struct sockaddr)。当recvfrom()返回时,fromlen包含实际存入from中的数据字节数。
  Recvfrom()函数返回接收到的字节数或当出现错误时返回-1,并置相应的errno。
  如果你对数据报socket调用了connect()函数时,你也可以利用send()和recv()进行数据传输,但该socket仍然是数据报socket,并且利用传输层的UDP服务。但在发送或接收数据报时,内核会自动为之加上目地和源地址信息。
  
  10.结束传输
  当所有的数据操作结束以后,你可以调用close()函数来释放该socket,从而停止在该socket上的任何数据操作:close(sockfd)。
  你也可以调用shutdown()函数来关闭该socket。该函数允许你只停止在某个方向上的数据传输,而一个方向上的数据传输继续进行。如你可以关闭某socket的写操作而允许继续在该socket上接受数据,直至读入所有数据。
  int shutdown(int sockfd,int how);
  Sockfd是需要关闭的socket的描述符。参数 how允许为shutdown操作选择以下几种方式: 0 不允许继续接收数据,1 不允许继续发送数据,2 不允许继续发送和接收数据,均为允许则调用close ()
  shutdown在操作成功时返回0,在出现错误时返回-1并置相应errno。
  
  11.IP和域名的转换
  在网络上标志一台机器可以用IP或者是用域名.那么我们怎么去进行转换呢?
  struct hostent *gethostbyname(const char *hostname)
  struct hostent *gethostbyaddr(const char *addr,int len,int type)
  在中有struct hostent的定义
  struct hostent
  {
  char *h_name; /* 主机的正式名称 */
  char *h_aliases; /* 主机的别名 */
  int h_addrtype; /* 主机的地址类型 AF_INET*/
  int h_length; /* 主机的地址长度 对于IP4 是4字节32位*/
  char **h_addr_list; /* 主机的IP地址列表 */
  #define h_addr h_addr_list[0] /* 主机的第一个IP地址*/
  }
  gethostbyname:可以将机器名(如 linux.yessun.com)转换为一个结构指针,在这个结构里面储存了域名的信息。
  gethostbyaddr:可以将一个32位的IP地址(C0A80001)转换为结构指针。
  这两个函数失败时返回NULL 且设置h_errno错误变量,调用h_strerror()可以得到详细的出错信息。

mysql 的 unauthenticated user 现象

show processlist查看有大量如下信息,造成mysql假死.

| 364 | unauthenticated user | xxx.xxx.xxx.xxx:63249 | NULL | Connect |    | login | NULL          |
| 365 | unauthenticated user | xxx.xxx.xxx.xxx:56768 | NULL | Connect |    | login | NULL          |
| 366 | unauthenticated user | xxx.xxx.xxx.xxx:54127 | NULL | Connect |    | login | NULL          |
| 367 | unauthenticated user | xxx.xxx.xxx.xxx:51060 | NULL | Connect |    | login | NULL          |

看下手册中的解释是:unauthenticated user refers to a thread that has become associated with a client connection but for which authentication of the client user has not yet been done。意即:有一个线程在处理客户端的连接,但是该客户端还没通过用户验证。
原因可能有:
1、 服务器在做DNS反响解析,解决办法有2:
1、) 在 hosts 中添加客户端ip,如
192.168.0.1   yejr
2、) MySQL启动参数增加一个skip-name-resolve,即不启用DNS反响解析;或者在/etc/my.cnf里添加一行skip-name-resolve
2、服务器的线程还处于排队状态,因此可以加大 back_log

然后重启mysql就会发现unauthenticated user 全部消失

[手册]

7.5.10. How MySQL Uses DNS

When a new client connects to mysqld, mysqld spawns a new thread to handle the request. This thread first checks whether the hostname is in the hostname cache. If not, the thread attempts to resolve the hostname:

        

  •     

    If the operating system supports the thread-safe gethostbyaddr_r() and gethostbyname_r() calls, the thread uses them to perform hostname resolution.

        

  •     

  •     

    If the operating system does not support the thread-safe calls, the thread locks a mutex and calls gethostbyaddr() and gethostbyname() instead. In this case, no other thread can resolve hostnames that are not in the hostname cache until the first thread unlocks the mutex.

        

You can disable DNS hostname lookups by starting mysqld with the --skip-name-resolve option. However, in this case, you can use only IP numbers in the MySQL grant tables.

If you have a very slow DNS and many hosts, you can get more performance by either disabling DNS lookups with --skip-name-resolve or by increasing the HOST_CACHE_SIZE define (default value: 128) and recompiling mysqld.

You can disable the hostname cache by starting the server with the --skip-host-cache option. To clear the hostname cache, issue a FLUSH HOSTS statement or execute the mysqladmin flush-hosts command.

To disallow TCP/IP connections entirely, start mysqld with the --skip-networking option.