《精通Linux设备驱动程序开发》——内核

一、启动过程

 1、基于X86的处理器有两种操作模式:实模式保护模式


 2、基于X86硬件上的Linux启动过程(P12  图2-1)

   1)、BIOS-provided  physical  RAM  map

        I、内核会解析从BIOS读取到的系统内存映射,并率先将相关信息打印出来。

        II、实模式下的初始化代码通过使用BIOS的int0x15服务并执行0xe820号函数来获得系统的内存映射信息。内存映射信息包含了预留的和可用的内存,内核将随后使用这些信息

              创建可用的内存池。

   2)、758MB  LOWMEM  available

        I、896MB以内的常规的可被寻址的内存区域被称为低端内存。内存分配函数kmmalloc()就是从该区域分配内存的。

        II、高于896MB的内存区域被称为高端内存,只有在采取特殊的方式进行映射后才能访问。

   3)、Kernel  command  line:ro  root=/dev/hda1

        I、Linux引导装入程序通常会给内核传递一个命令行。可在引导装入配置文件中增加命令行参数,也可以在运行过程中通过引导装入程序修改Linux的命令行。

        II、命令行参数将影响启动过程中的代码执行路径。

   4)、Calibrating  delay. . . 1197.46  BogoMIPS(lpj=2394935)

        I、在启动过程中,内核会计算处理器在一个jiffy时间内运行一个内部的延迟循环的次数。jiffy的含义是系统定时器2个连续的节拍之间的间隔。该计算必须被校准到所有CPU 

             的处理速度。校准的结果被被存储到loops_per_jiffy的内核变量中。

        II、BogoMIPS = loops_per_jiffy*1秒内的jiffy数*延迟循环消耗的指令数(以百万为单位。)

   5)、Checking  HLT  instruction

        I、由于Linux内核支持多种硬件平台,启动代码会检查体系架构相关的bug。其中一项工作便是验证停机(HIT)指令

   6)、NET:Registered  protocol  family 

        I、Linux套接字层是用户空间应用程序访问各种网络协议的同一接口。每个协议通过include/linux/socket.h文件中定义的分配给它的独一无二的系列号注册。

   7)、Freeing  initrd  memory:387k  freed

        I、initrd是一种由引导装入程序加载的常驻内存的虚拟磁盘映像。在内核启动后,会将其挂载为初始化文件系统,这个初始根文件系统中存放着挂载实际根文件系统磁盘分

             区时所依赖的可动态链接的模块。

        II、Linux2.6内核提供了一种被称为initramfs的新功能,它在几个方面比initrd更加优秀。

   8)、io  scheduler  anticipatory  registered

        I、I/O调度器的主要目标是通过减少磁盘的定位次数来增加系统的吞吐率。

        II、2.6内核提供了四种不同的I/O调度器:Deadline、Anticipatory、Complete  Fair  Queuing以及NOOP。

   9)、Setting  up  standard  PCI  resources

        I、启动过程的下一阶段会初始化I/O总线和外围控制器。内核会通过遍历PCI总线来探测PCI硬件,接下来再初始化其他的I/O子系统。

   10)、EXT3-fs:mounted  filesystem

        I、EXT3文件系统已经成为Linux事实上的文件系统。

   11)、INIT:version  2.85   booting

        I、所有Linux进程的父进程init是内核完成启动序列后运行的第一个程序。



二、用户模式和内核模式

 1、在Linux机器上,CPU要么处于受信任的内核模式,要么处于受限制的用户模式


 2、内核模式的代码可以无限制的访问所有处理器指令集以及全部内存和I/O空间。如果用户模式的进程要享有此特权,必须通过系统调用向设备驱动程序或其他内核模式的代码

       发出请求。另外,用户模式的代码允许发生缺页,而内核模式的代码则不允许。



三、进程上下文和中断上下文

 1、内核可以处于两种上下文:进程上下文和中断上下文。

 

 2、在系统调用之后,用户空间程序进入内核空间,此后内核空间相应进程的代表就运行于进程上下文中。异步发生的中断会引发中断处理程序被调用,中断处理程序就运行于中断上下文。



四、内核定时器

 1、HZ和Jiffies

   1)、系统定时器能以可编程的频率中断处理器。此频率即为每秒的定时器节拍数,对应着内核变量HZ。

       I、选择合适的HZ值需要权衡。HZ值越大,定时器间隔时间就小,因此进程调度准确性会更高。相反,HZ越大也会导致开销和电源消耗更多,因此更多的处理器周期将被耗

            费在定时器中断上下文。

   2)、Jiffies变量记录了系统启动以来,系统定时器已经触发的次数。内涵每秒将Jiffies变量增加HZ次。

       I、Jiffies被定义为volatile类型它会告诉编译器不要优化该变量的存取代码。


 2、长延时

   1)、在内核中,以Jiffies为单位进行的延迟通常被称为是长延时。实现长延时的方法是忙等待(不推荐)和睡眠等待。

       I、用于睡眠等待的函数:schedule_timeout、wait_event_timeout和msleep。其中wait_event_timeout和msleep的实现都基于schedule_timeout。wait_event_timeout的

           使用场合是在一个特定的条件满足或者超时发生后,希望代码继续运行。msleep表示睡眠指定的时间(以毫秒为单位)。

       II、此长延时技术仅仅用于进程上下文。睡眠等待不能用于中断上下文。

   2)、为了支持在将来的某个时刻进行某项工作,内核提供了定时器API。

       I、可以通过init_timer动态定义一个定时器,也可以通过DEFINE_TIMER静态创建定时器。

       II、后将处理函数的地址和参数绑定给一个timer_list,并且使用add_timer注册它即可。

       III、可以使用mode_timer修改my_timer的到期时间,使用del_timer取消定时器,或者使用timer_timeout查看my_timer当前是否处于等待状态。

       IV、clock_settime和clock_gettime等用户空间函数可用于获得内核定时器服务。用户应用程序可以使用settimer和gettimer来控制一个报警信号在特定的超时后发生。


 3、短延时

   1)、在内核中,小于jiffy的延时被认为是短延时。短延时可在中断或进程上下文发生。惟一实现短延时的途径就是忙等待。

   2)、实现短延时的内核API包括mdelayudelayndelay,分别是微妙、毫秒、纳秒级的延时。这些函数的实际实现取决于体系结构,而且也并非在所有平台上都被完整实

             现。


 4、Pentium时间戳计数器

   1)、时间戳计数器(TSC)是Pentium兼容处理器中的一个计数器,它记录自启动以来处理器消耗的时钟周期数。

   2)、使用TSC通常被用于剖析和监测代码。使用rdtsc指令可测量某段代码的执行时间,其精度达到微秒级。


 5、实时钟

   1)、RTC在非易失性存储器上记录绝对时间。使用RTC可以完成如下工作:

        I、读取、设置绝对时间,在时钟更新时产生中断。

        II、产生频率为2~8192Hz之间的周期性中断。

        III、设置报警号。

   2)、许多应用程序需要使用绝对时间[或称墙上时间]。jiffies不包含墙上时间。内核将墙上时间记录在xtime变量中。在启动过程中,会根据从RTC读取到的目前的墙上时间初

              始化xtime。在系统停机后,墙上时间会被写回RTC。可以用do_gettimeofday读取墙上时间。

   3)、用户空间可以访问墙上时间的函数

         I、time(),该函数返回日历时间,或从新纪元以来经历的秒数。

         II、localtime(),以分散的形式返回日历时间。

         III、mktime(),进行localtime()函数的反向工作。

         IV、gettimeofday(),如果你的平台支持,该函数将以微秒级精度返回日历时间。



五、内核中的并发

 1、自旋锁和互斥体

   1)、访问共享资源的代码区域称作临界区。自旋锁和互斥体是保护内核临界区的两种基本机制。

   2)、自旋锁可以确保在同时只有一个线程进入临界区。其他想进入临界区的线程必须不停地原地打转,直到第一个线程释放自旋锁。

   3)、互斥体在进入一个被占用的临界区之前不会原地打转,而是使当前线程进入休眠状态。

   4)、自旋锁和互斥体的选择

       I、如果临界区需要睡眠,只能使用互斥体,因为获得自旋锁后进行调度、抢占以及等待队列上睡眠都是非法的。

       II、由于互斥体会在面临竞争的状态下将当前线程置于睡眠状态,因此,在中断处理函数中,只能使用自旋锁。

   5)、临界区的并发保护实例

       I、非抢占内核,单CPU情况下存在于进程上下文的临界区。

       II、非抢占内核,单CPU情况下存在于进程集合中断上下文的临界区。

       III、可抢占内核,单CPU情况下存在于进程集合中断上下文的临界区。

       IV、可抢占内核,SMP(对称多处理器)情况下存在于进程集合中断上下文的临界区。


 2、原子操作

   1)、原子擦操作作用于执行轻量级的、仅执行一次的操作。原子操作也可以确保操作的串行化,不再需要锁进行并发访问保护。原子操作的具体实现取决于体系架构。

   2)、内核支持set_bit()、clear_bit()和test_and_bit()操作,它们可用于原子地进行位修改。


 3、读-写锁

   1)、另一个特定的并发保护机制是自旋锁的读-写锁变体。如果每个执行单元在访问临界区的时候要么是读要么是写共享的数据结构,但是它们都不会同时进行读写操作,这

              种锁最好被选用。

         I、读自旋锁的用法(P31代码)

         II、写自旋锁的用法(P31代码)

   2)、读-写锁irq变体

         I、read_lock_orqsave()\read_unlock_irqrestore()\write_lock_irqsave()和write_unlock_irqrestore(),这些函数的含义与传统自旋锁相应的变体相似。

   3)、顺序锁是一种支持写多于读的读-写锁,在一个变量的写操作比读操作多得多的情况下,此锁非常有用。

   4)、读-复制-更新(RCU)的机制。该机制用于提高读操作多于写操作的性能。


 4、调试

   1)、在编译和测试代码时使能SMP(CONFIG_SMP)和抢占(CONFIG_PREEMPT)是一种很好的理念。

   2)、在Kerner  hacking下有一个称为Spinlock and rw-lock debugging的配置选项(CONFIG_DEBUG_SPINLOCK),它能帮助你找到一些常见的自旋锁错误。

   3)、Lockmeter等工具可用于收集锁相关的信息。



六、proc文件系统

 1、proc文件系统及其用途

   1)、proc文件系统是一种虚拟的文件系统,它创建内核内部的视窗。驻留于procfs中的文件并不与物理存储设备关联。相反,这些文件中的数据由内核中相应的入口点按需

             动态创建。

   2)、procfs中的文件可被用于配置内核参数、查看内核结构体、从设备驱动程序中收集统计信息或者获取通用的系统信息。


 2、procfs的能力



七、内存分配

 1、内存区

   1)、内核会以分页形式组织物理内存,而页大小则取决于具体的体系结构。在X86的机器上,其大小为4096B。

   2)、物理内存中的每一页都有一个与之对应的struct_page:

         struct  page{

             unsigned  long  flags;

             atomic_t  _count;

             /* .  .  .  */

             void  *  virtual;

         };

   3)、32位X86PC系统上默认的地址空间分布。(P33  图2-5)


 2、逻辑与内核虚拟地址

   1)、内核中用于映射低于896MB物理内存的地址与物理地址之间存在线性偏移,这种内核地址被称为逻辑地址。在支持特定的方式映射这些区域产生对应的虚拟地址后,内

             核将能访问查过896MB的内存。

   2)、所有逻辑地址都是内存虚拟地址,但所有的虚拟地址并非一定是逻辑地址。


 3、内存区的分类

   1)、ZONE_DMA(小于16MB),该区域用于直接内存访问(DMA)

   2)、ZONE_NORMAL(16-896MB),常规地址区域,也被称作低端内存。

   3)、ZONE_HIGH(大于896MB),仅仅在通过kmap()映射为虚拟机地址后才能访问。(通过kunmap()去除映射)


 4、内存分配函数

   1)、kmalloc()是从ZONE_NORMAL区域返回连续内存的内存分配函数:

           void  kmalloc(int  count,  int  flags);  

        I、count是要分配的字节数。

        II、flags是一个模式说明符:

           GFP_KERNEL,被进程上下文用来分配内存。如果指定了该标志,kmalloc()将被允许睡眠,以等待其他页释放。

           GFP_ATOMIC,被中断上下文用来分配内存。在这种模式下,kmalloc()不允许进行睡眠等待,以获得空闲页,因此GFP_ATOMIC分配成功的可能性比GFP_KERNEL低。

   2)、由于kmalloc()返回的内存保留了以前的内容,将它暴露给用户空间可能会导致安全问题,所以可以用kzalloc()获得被填充为0的内存。

   3)、如果需要分配大的内存缓冲区,而且也不要内存在物理上有联系,可以使用vmmlloc():

          void  *vmmalloc(unsigned  long  count);

         I、count是要请求分配的内存大小。该函数返回内存虚拟地址。

         II、vmmalloc分配速度慢,而且不能从中断上下文调用。另外,不能用vmmalloc返回的物理上不连续内存执行DMA。


 5、其他内存分配技术

   1)、后备缓冲区(look  aside  buffer)。

   2)、slab。

   3)、mempool。

你可能感兴趣的:(linux驱动)