第 1 章 Linux内核介绍

第 1 章 Linux内核介绍

系统移植的核心是内核移植。内核移植不仅影响内核的功能,而且还影响到整个系统的性能。因此,了解Linux内核,有利于开发人员进行系统裁剪和移植。下面主要针对Linux内的5个重要部分(系统调用接口、进程管理、内存管理、虚拟文件系统和设备驱动程序)进行介绍。

1.1 系统调用接口

系统调用是操作系统提供给用户程序调用的一组“特殊”接口。用户程序可以通过这组“特殊”接口获得操作系统内核提供的服务。例如,用户可以通过文件系统相关的调用请求系统打开文件、关闭文件或读写文件;通过时钟相关的系统调用获得系统时间或设置系统时间;通过进程控制相关的系统调用来创建进程、实现进程调度、进程管理等。

1.1.1 Linux系统调用

所有的操作系统在内核里都有一些内建的函数,这些函数完成对硬件的访问和对文件的打开、读、写、关闭等操作。Linux系统中称这些函数为“系统调用”(即systemcall)。这些函数实现了将操作从用户空间转换到内核空间,有了这些接口函数,用户就可以方便地访问硬件。例如,在用户空间调用open()函数,则会在内核空间调用sys_open()。一个已经安装的系统所支持的系统调用都可以在/usr/include/bits/syscall.h文件里看到。

为了对系统进行保护,Linux系统定义了内核模式和用户模式。内核模式可以执行一些特权指令并进入用户模式,而用户模式不能进入内核模式。Linux将程序的运行空间也分为内核空间和用户空间,它们分别运行在不同的级别上。在逻辑上,它们是相互隔离的。系统调用规定用户进程进入内核空间的具体位置。在执行系统调用时,程序运行空间将会从用户空间转移到内核空间,处理完毕后再返回到用户空间。

1.1.2 用户编程接口

用户编程接口是为用户编程过程提供的各种功能库函数,如分配空间、拷贝字符、打开文件等。Linux用户编程接口(API)遵循了在UNIX中最流行的应用编程界面标准——POSIX标准。它与系统调用之间存在一定的联系和区别。不同的语言和平台为用户提供了丰富的编程接口,包括网络编程接口、图形编程接口、数据库编程接口等,但这些不是系统调用。

系统调用与用户编程接口之间存在联系。一个或者多个系统调用会对应到一个具体的应用程序使用的API;但是,并非所有的API都需要使用到系统调用。

根据《深入理解Linux内核》一书中对用户编程接口(API)和系统调用两者区别的描述为,前者只是一个函数定义,说明了如何获得一个给定的服务;而后者是通过软中断向内核态发出一个明确的请求。如果按照层次关系来分,系统调用为底层,而用户编程接口为上层。一个用户编程接口由0个或者多个系统调用组成。

1.1.3 系统调用与服务例程的对应关系

为了通过系统调用号来调用不同的内核服务例程,系统必须创建并管理好一张系统调用表。该表用于系统调用号与内核服务函数的映射,Linux用数组sys_call_table表示这个表。在这个表的每个表项中存放着对应内核服务例程的指针,而该表项的下标就是该内核服务例程的系统调用号。Linux规定,在i386体系中,处理器的寄存器eax用来传递系统调用号。

1.1.4 系统调用过程

通常情况下,abc()系统调用对应的服务例程的名字是sys_abc()。图1.1表示了系统调用和应用程序、对应的封装例程、系统调用处理程序及系统调用服务例程之间的关系。下面使用一个例子来简单说明系统调用过程。

{%}

图 1.1 系统调用的处理过程

1. 用户程序中调用库函数abc()。

2. 系统加载libc库调用索引和参数后,执行int0x80或者sysenter汇编指令进入系统调用,执行system_call()函数。

3. system_call()函数根据传递过来的参数处理所有的系统调用。使用system_call_table[参数]执行系统调用。

4. 系统调用返回。

5. 执行iret或者sysexit汇编指令两种方式退出系统调用,并调用resume_userspace()函数进入用户空间。

6. 继续在libc库中执行,执行完成后返回到用户应用程序中。

执行int0x80或者sysenter汇编指令两种方式进入系统调用,在老版本的Linux内核中只支持int0x80中断方式。执行iret或者sysexit汇编指令两种方式退出系统调用,如图1.1中的虚线指引线所示。

1.1.5 系统调用传递的参数

系统调用中输入输出的参数为实际传递的值或者是用户态进程的地址,或者是指向用户态函数指针的数据结构地址。传递的参数放在寄存器eax中,即系统调用号。寄存器传递参数的个数满足两个条件:

  • 参数的长度不超过寄存器的长度,如果是32位平台不超过32位,64位平台不超过64位。

  • 不包括eax中的系统调用号,参数的个数不超过6个。

内核在为用户请求提供服务时,会检查所有的系统调用参数。检查参数时,主要对参数的权限和参数表示的地址空间的有效性进行验证。

1.2 进程管理

进程管理包括创建进程、管理进程及删除进程。进程管理是Linux内核的重要部分,对系统的核心资源进行管理。要做好系统移植就需要对这部分知识有一定的了解。

1.2.1 进程

进程是程序执行时的一个实体。程序包含指令和数据,而进程包含程序计数器和全部CPU寄存器的值。进程的堆栈中存储着一些数据,如子程序参数、返回地址及变量之类的临时数据。当前的执行程序(进程)包含当前处理器中的活动状态。

Linux是一个多处理操作系统,进程拥有独立的权限和单一职责。如果系统中某个进程发生崩溃,它不会影响到另外的进程。每个进程都运行在各自独立的虚拟地址空间中,只有通过核心控制下可靠的进程通信机制,它们之间才能发生通信。进程通信机制包括管道、信号、信号量、消息队列等。

从内核的观点看,进程的目的就是担当分配系统资源的实体。系统资源包括CPU时间、内存等。因此,进程管理的最终目的就是在进程畅通执行的条件下,合理分配系统资源给不同的进程。

当一个进程创建时,它几乎与父进程相同,即是父进程地址空间的一个(逻辑)备份。从进程创建系统调用的下一条指令开始,执行与父进程相同的代码。虽然父子进程含有共享的程序代码,但是分别拥有独立的数据备份。因此子进程对堆和栈中的数据进行修改时,对父进程的数据是不会有影响的。

1.2.2 进程描述符

内核对进程的优先级、进程的状态、地址空间等采用进程描述符表示。在Linux内核中,进程用一个相当大的称为 task_struct 的结构表示。下面是从linux-2.6.29\include\linux\ sched.h中摘抄出来的进程描述的部分信息。

struct task_struct {
    volatile long state;    /* -1 unrunnable, 0 runnable, >0 stopped */
    void *stack;
    atomic_t usage;
    unsigned int flags; /* per process flags, defined below */
    unsigned int ptrace;
    int lock_depth;     /* BKL lock depth */
    int prio, static_prio, normal_prio;
    unsigned int rt_priority;
    const struct sched_class *sched_class;
    struct sched_entity se;
    struct sched_rt_entity rt;
    unsigned char fpu_counter;
    s8 oomkilladj; /* OOM kill score adjustment (bit shift). */
    unsigned int policy;
    cpumask_t cpus_allowed;
    struct list_head tasks;
    struct mm_struct *mm, *active_mm;

/* task state */
    struct linux_binfmt *binfmt;
    int exit_state;
    int exit_code, exit_signal;
    int pdeath_signal;  /*  The signal sent when the parent dies  */

    unsigned int personality;
    unsigned did_exec:1;
    pid_t pid;
    pid_t tgid;

    struct task_struct *real_parent; /* real parent process */
    struct task_struct *parent; /* recipient of SIGCHLD, wait4() reports */
    struct list_head children;  /* list of my children */
    struct list_head sibling;   /* linkage in my parent's children list */
    struct task_struct *group_leader;   /* threadgroup leader */
    struct list_head ptraced;
    struct list_head ptrace_entry;
    struct pid_link pids[PIDTYPE_MAX];
    struct list_head thread_group;

    struct completion *vfork_done;      /* for vfork() */
    int __user *set_child_tid;     /* CLONE_CHILD_SETTID */
    int __user *clear_child_tid;       /* CLONE_CHILD_CLEARTID */
    ...
};

下面简单对上述内容进行描述。

  • state:表示进程的状态,-1代表“不能运行”,0代表“运行”,>0代表“停止”。

  • flags:定义了很多指示符,表明进程是否正在被创建(PF_STARTING)或退出(PF_EXITING),或是进程当前是否在分配内存(PF_MEMALLOC)。可执行程序的名称(不包含路径)占用comm(命令)字段。

  • 每个进程都会被赋予优先级(称为 static_prio),但进程的实际优先级是基于加载及其他几个因素动态决定的。优先级值越低,实际的优先级越高。

  • tasks字段提供了链接列表的能力。它包含一个 prev 指针(指向前一个任务)和一个next指针(指向下一个任务)。

进程的地址空间由mm和active_mm字段表示。mm代表的是进程的内存描述符,而 active_mm则是前一个进程的内存描述符(为改进上下文切换时间的一种优化)。

1.2.3 进程状态

进程描述符中state字段描述进程当前的状态。它由一组标志组成,其中每个标志描述一种可能的进程状态。在2.6内核中,进程只能处于这些状态中的一种。下面分别对这些状态进行描述。

  • 可运行状态(TASK_RUNNING):进程处于运行(它是系统的当前进程)或者准备运行状态(它在等待系统将CPU分配给它)。

  • 等待状态(WAITING):进程在等待一个事件或者资源。Linux将等待进程分成两类:可中断的等待状态(TASK_TNTERRUPTIBLE)与不可中断的等待状态(TASK_UNINTERRUPTIBLE)。可中断等待进程可以被信号中断;不可中断等待进程直接在硬件条件等待,并且任何情况下都不可中断。

  • 暂停状态(TASK_STOPPED):进程被暂停,通常是通过接收一个信号(SIGSTOP、SIGTSTP、SIGTTIN或SIGTTOU)转为暂停状态。正在被调试的进程可能处于停止状态。

  • 僵死状态(EXIT_ZOMBIE):进程的执行被终止,但是,父进程还没有发布wait4()或waitpid()系统调用返回有关死亡进程的信息。

1.2.4 进程调度

Linux进程调度的目的就是调度程序运行时,要在所有可运行状态的进程中选择最值得运行的进程投入运行。每个进程的task_struct结构中有policy、priority、counter和rt_priority这4项,是选择进程的依据。

其中,policy为进程调度策略,用于区分普通进程和实时进程,实时进程优先于普通进程运行;priority是进程(包括实时和普通)的静态优先级;counter是进程剩余的时间片,它的起始值就是priority的值;因为counter用于计算一个处于可运行状态的进程值的运行的程度goodness,所以counter也被看做是进程的动态优先级。rt_priority是实时进程特有的优先级别,用于实时进程间的选择。

1. Linux进程分类

Linux在执行进程调度的时候,对不同类型的进程采取的策略也不同,一般将Linux分为以下3类。

  • 交互式进程:这类进程经常和用户发生交互,所以花费一些时间等待用户的操作。当有用户输入时,进程必须很快地激活。通常要求延迟在50~150毫秒。典型的交互式进程有控制台命令、文本编辑器、图形应用程序等。

  • 批处理进程(Batch Process):这类进程不需要用户交互,一般在后台运行。所以不需要非常快地反应,它们经常被调度期限制。典型的批处理进程有编译器、数据库搜索引擎和科学计算等。

  • 实时进程:这类进程对调度有非常严格的要求,这种类型的进程不能被低优先级进程阻塞,并且在很短的时间内做出反应。典型的实时进程有音视频应用程序、机器人控制等。

2. Linux进程优先级

Linux系统中每一个普通进程都有一个静态优先级。这个值会被调度器作为参考来调度进程。在内核中调度的优先级区间为[100,139],数字越小,优先级越高。一个新的进程总是从它的父进程继承此值。Linux进程优先级还包括动态优先级、实时优先级等,各个进程优先级描述如下。

  • 静态优先级(priority):被称为“静态”是因为它不随时间而改变,只能由用户进行修改。它指明了在被迫和其他进程竞争CPU之前该进程所被允许的时间片的最大值(20)。

  • 动态优先级(counter):counter即系统为每个进程运行而分配的时间片。Linux用它来表示进程的动态优先级。当进程拥有CPU时,counter就随着时间不断减小。当它递减为0时,标记该进程将重新调度。它指明了在当前时间片中所剩余的时间量(最初为20)。

  • 实时优先级(rt_priority):它的变化范围是从0~99。任何实时进程的优先级都高于普通的进程。

  • Base time quantum:是由静态优先级决定,当进程耗尽当前Base time quantum,kernel会重新分配一个Base time quantum给它。静态优先级和Base time quantum的关系如下所述。

1. 当静态优先级小于120:

Base time quantum(in millisecond)= (140 – static priority) * 20

2. 当静态优先级大于等于120:

Base time quantum(in millisecond)= (140 – static priority) * 5

3. Linux进程的调度算法

在Linux系统中,进程作为程序的实体始终运行在系统中,并且进程占用系统资源,所以进程调度算法优劣将会严重影响到系统的性能。为提高系统性能设计进程调度算法的原则,应该遵循进程响应尽量快,后台进程的吞吐量尽量大,尽量避免进程“饿死”现象,低优先级和高优先级进程需要尽可能调和。以下为常见的进程调度算法。

  • 时间片轮转调度算法(round-robin):SCHED_RR用于实时进程。系统使每个进程依次地按时间片轮流执行的方式。

  • 优先权调度算法:SCHED_NORMAL用于非实时进程。每次系统都会选择队列中优先级最高的进程运行。Linux采用抢占式的优级算法,即系统中当前运行的进程永远是可运行进程中优先权最高的进程。

  • FIFO(先进先出)调度算法:SCHED_FIFO用于实时进程。采用FIFO调度算法选择的实时进程必须是运行时间较短的进程,因为这种进程一旦获得CPU就只有等到它运行完或因等待资源主动放弃CPU时,其他进程才能获得运行机会。

1.2.5 进程地址空间

Linux的虚拟地址空间为0~4GB。虚拟的4GB空间被Linux内核分为内核空间和用户空间两部分。将最高的1GB(从虚拟地址0xC0000000~0xFFFFFFFF)留给内核使用,称为“内核空间”。将较低的3GB(从虚拟地址0x00000000~0xBFFFFFFF)留给用户进程使用,称为“用户空间”。因为每个进程可以通过系统调用进入内核,因此,Linux内核空间被系统的所有进程共享,实际上对于某个进程来说,它仍然可以拥有4GB的虚拟空间。

其中,很重要的一点是虚拟地址空间,并不是实际的地址空间。在为进程分配地址空间时,根据进程需要的空间进行分配,4GB仅仅是最大限额而已,并非一次性将4GB分配给进程。一般一个进程的地址空间总是小于4GB的,可以通过查看/proc/pid/maps文件来获悉某个具体进程的地址空间。但进程的地址空间并不对应实际的物理页,Linux采用Lazy的机制来分配实际的物理页(Demand paging和“写时复制(Copy On Write)的技术”),从而提高实际内存的使用率,即每个虚拟内存页并不一定对应于物理页。虚拟页和物理页的对应是通过映射机制来实现的,即通过页表进行映射到实际的物理页。因为每个进程都有自己的页表,因此可以保证不同进程的相同虚拟地址可以映射到不同的物理页,从而为不同的进程都可以同时拥有4GB的虚拟地址空间提供了可能。

内核是系统中优先级最高的部分。内核函数申请动态内存时系统不会推迟这个请求;而进程申请内存空间时,进程的可执行文件被装入后,进程不会立即对所有的代码进行访问。因此内核总是尽量推迟给用户进程分配动态空间。

内核分配空间时,可以通过__get_free_pages()或alloc_pages从分区页框分配器中获得页框;通过kmem_cache_alloc()或kmalloc()函数使用slab分配器为专用或通用对象分配块;通过vmalloc()或vmalloc32()函数获得一块非连续的内存区。

与进程地址空间有关的全部信息都包含在一个叫做内存描述符(memeory desvriptor)的数据结构中,其结构类型为mm_structs进程描述符的mm字段就是指向这个结构的。

1. 创建进程地址空间

copy_mm()函数通过建立新进程的所有页表和内存描述符,来创建进程的地址空间。

01  static int copy_mm(unsigned long clone_flags, struct task_struct * tsk)
02  {
03      struct mm_struct * mm, *oldmm;
04      int retval;
05
06      tsk->min_flt = tsk->maj_flt = 0;
07      tsk->nvcsw = tsk->nivcsw = 0;
08
09      tsk->mm = NULL;
10      tsk->active_mm = NULL;
11
12  /*如果是内核线程的子线程(mm=NULL),则直接退出,此时内核线程mm和active_mm均为NULL*/
13      oldmm = current->mm;
14      if (!oldmm)
15          return 0;
16
17  /*内核线程,只是增加当前进程的虚拟空间的引用计数*/
18      if (clone_flags & CLONE_VM) {
19          /*如果共享内存,将mm由父进程赋值给子进程,两个进程将会指向同一块内存*/
20          atomic_inc(&oldmm->mm_users);
21          mm = oldmm;
22          goto good_mm;
23      }
24
25      retval = -ENOMEM;
26      mm = dup_mm(tsk);       /*完成了对vm_area_struct和页面表的复制*/
27      if (!mm)
28          goto fail_nomem;
29
30  good_mm:
31      /* Initializing for Swap token stuff */
32      mm->token_priority = 0;
33      mm->last_interval = 0;
34
35  /*内核线程的mm和active_mm指向当前进程的mm_struct结构*/
36      tsk->mm = mm;
37      tsk->active_mm = mm;
38      return 0;
39
40  fail_nomem:
41      return retval;
42  }

2. 删除进程地址空间

内核调用exit_mm()函数释放进程的地址空间。

01  static void exit_mm(struct task_struct * tsk)
02  {
03  m_release(tsk, mm);
04  /*得到读写信号量*/
05      down_read(&mm->mmap_sem);
06      core_state = mm->core_state;
07      if (core_state) {
08          struct core_thread self;
09  /*释放读写信号量*/
10          up_read(&mm->mmap_sem);
11
12          self.task = tsk;
13          self.next = xchg(&core_state->dumper.next, &self);
14
15          if (atomic_dec_and_test(&core_state->nr_threads))
16              complete(&core_state->startup);
17
18          for (;;) {
19              set_task_state(tsk, TASK_UNINTERRUPTIBLE);
20              if (!self.task)    /*take字段可以查看函数coredump_
                finish()*/
21                  break;
22              schedule();
23          }
24          __set_task_state(tsk, TASK_RUNNING);
25          down_read(&mm->mmap_sem);
26      }
27      atomic_inc(&mm->mm_count);
28      BUG_ON(mm != tsk->active_mm);
29      /* more a memory barrier than a real lock */
30      task_lock(tsk);
31      tsk->mm = NULL;
32      up_read(&mm->mmap_sem);
33      enter_lazy_tlb(mm, current);
34      /*释放用户虚拟空间的数据结构*/
35      clear_freeze_flag(tsk);
36      task_unlock(tsk);
37      mm_update_next_owner(mm);
38      /*递减mm的引用计数并是否为0,如是,则释放mm所代表的映射*/
39      mmput(mm);
40  }

1.3 内存管理

RAM的一部分被静态地划分给了内核,用来存放内核代码和静态数据结构。RAM的其余部分称为动态内存(dynamic memory),这不仅是运行用户进程所需的宝贵资源,也是内核所需的宝贵资源。事实上,整个系统的性能取决于如何有效地管理动态内存。

1.3.1 内存管理技术

页表(page tables):进程在读取指令和存取数据时都要访问内存。在一个虚拟内存系统中,所有的地址都是虚拟地址而非物理地址。操作系统维护虚拟地址和物理地址转换的信息,处理器通过这组信息将虚拟地址转换为物理地址。虚拟内存和物理内存被分为适当大小的块,叫做页。为了将虚拟地址转换为物理地址,首先,处理器要找到虚拟地址的页编号和页内偏移量;然后,处理器根据虚拟地址和物理地址的映射关系将虚拟页编号转换为物理页;最后,根据偏移量访问物理页的确定偏移位置。每个物理页面都有一个struct page结构,位于include/linux/mm.h,该结构体包含了管理物理页面时的所有信息,下面给出该结构体的具体描述:

typedef struct page {
         struct list_head list;        //指向链表中的下一页
         struct address_space *mapping;
                       //用来指定我们正在映射的索引节点(inode)
         unsigned long index;          //在映射表中的偏移
         struct page *next_hash;       //指向页高速缓存哈希表中下一个共享的页
         atomic_t count;               //引用这个页的个数
         unsigned long flags;          //页面各种不同的属性
         struct list_head lru;         //用在active_list中
         wait_queue_head_t wait;       //等待这一页的页队列
         struct page **pprev_hash;
                        //指向页高速缓存哈希表中前一个共享的页与next_hash相对应
         struct buffer_head * buffers; //把缓冲区映射到一个磁盘块
         void *virtual;
         struct zone_struct *zone;     //页所在的内存管理区
      } mem_map_t;

1. 请求页面调度(Demand Paging)

为了节省物理内存,只加载执行程序正在使用的虚拟页,这种进行访问时才加载虚拟页的技术叫做Demand Paging。当一个进程试图访问当前不在内存中的虚拟地址时,处理器无法找到引用的虚拟页对应的页表条目。当处理器无法将虚拟地址转换为物理地址时,处理器通知操作系统发生page fault。出错的虚拟地址无效,则意味着进程试图访问它不应该访问的虚拟地址。这种情况下,操作系统会中断它,从而保护系统中的其他进程。

如果出错的虚拟地址有效,而只是它所在的页当前不在内存中,操作系统应该从磁盘映像中将对应的页加载到内存中。相对内存存取来讲,磁盘存取需要更长的时间,所以进程一直处于等待状态直到该页被加载到内存中。如果系统当前有其他进程可以运行,操作系统将选择其中一个运行;接着将取到的页写进一个空闲页面,并将一个有效的虚拟页条目加到进程的页表中;然后,这个等待的进程重新执行发生内存出错地方的机器指令。本次虚拟内存存取进行时,处理器能够将虚拟地址转换为物理地址,使得进程能够继续运行。Linux使用demand paging技术将可执行映像加载到进程的虚拟内存中,在执行命令时,包含命令的文件被打开,将该文件的内容映射到进程的虚拟内存中。这个过程通过修改描述进程内存映射的数据结构来实现,也叫做内存映射(memory mapping),但实际上只有映像的第一部分真正放在了物理内存中,映像的剩余部分仍然在磁盘上。当映像执行时,它产生page fault,Linux使用进程的内存映像表来确定映像的哪一部分需要加载到内存中执行。

2. 页面置换技术(Swapping)

如果进程需要将虚拟页放到物理内存中,而此时已经没有空闲的物理页,操作系统必须废弃物理空间中的另一页,为该页让出空间。如果物理内存中需要废弃的页来自磁盘上的映像或者数据文件,并且该页没有被写过不需要存储,则该页被废弃。如果进程又需要该页,它可以从映像或数据文件中再次加载到内存中。但如果该页已经被改变,操作系统必须保留它的内容以便以后进行访问。这种也叫做dirty page ,当它从物理内存中废弃时,被存到一种叫做交换文件的特殊文件中。由于访问交换文件与访问处理器、物理内存的速度相比较慢,操作系统必须判断是将数据页写到磁盘上还是将它们保留在内存中以便下次访问。

如果判断哪些页将被废弃或者交换的算法效率不高,则会发生颠簸(thrashing),这时页不停地被写到磁盘上,然后又被读回,操作系统频繁地处理此读写任务而无法执行实际的工作。Linux 使用LRU(Least Recently Used,最近最少使用置换算法)的页面技术公平地选择需要从系统中废弃的页面。

伙伴系统算法用以解决外碎片问题。把所有的空闲页框分为11个块链表,每个块链表分别包含1、2、4、8、16、32、64、128、256、512和1024个连续的页框。对于1024个页框的最大请求对应着4MB大小的连续RAM块。每个块的第一个页框的物理地址是该块大小的整数倍。

非连续内存管理,当对内存区的请求不是很频繁的时候,通过连续的线性地址访问非连续的页框,该方法可以避免外碎片,但是其带来的负面因素打乱了内核表。非连续内存区的大小必须是4096字节的倍数。非连续内存区应用的场合分别有分配数据结构给活动的交换区、分配空间给模块和分配缓冲区给某些I/O驱动程序。

1.3.2 内存区管理

内存区(memory area)是具有连续的物理地址和任意长度的内存单元序列。伙伴系统采用的是页框作为基本内存区,适合于大内存的请求,对小内存的请求容易造成内碎片。为了解决内碎片的问题,将内存区大小按几何分布划分,也就是将内存区划分成2的幂的大小。不论请求的大小为多大时,总能保证内碎片小于内存区的50%。为此,内核建立了13个按几何分布的空闲内存区链表,大小从32~131?072字节。伙伴系统的调用是为了获得存放新内存区所需的额外页框,同时也为了释放不再包含内存区的页框,用一个动态链表来记录每个页框所包含的空闲内存区。

物理内存被划分为3个区来管理,它们是ZONE_DMA、ZONE_NORMAL和ZONE_HIGHMEM。每个区都用struct zone_struct结构表示,定义于include/linux/mmzone.h:

typedef struct zone_struct {
      /* Commonly accessed fields:*/
         spinlock_t lock;
         unsigned long    free_pages;
         unsigned long    pages_min, pages_low, pages_high;
         int            need_balance;
      /* free areas of different sizes*/
          free_area_t free_area[MAX_ORDER];
      /* Discontig memory support fields.*/
         struct pglist_data *zone_pgdat;
         struct page *zone_mem_map;
         unsigned long         zone_start_paddr;
         unsigned long         zone_start_mapnr;
      /*
      * rarely used fields:
      */
         char                *name;
         unsigned long         size;
      } zone_t;

各个字段的含义如下。

  • lock :用来保证对该结构中其他域的串行访问。

  • free_pages :在这个区中现有空闲页的个数。

  • pages_min、pages_low及 pages_high是对这个区最少、次少及最多页面个数的描述。

  • need_balance:与kswapd合在一起使用。

  • free_area:在伙伴分配系统中的位图数组和页面链表。

  • zone_pgdat:该管理区所在的存储结点。

  • zone_mem_map:该管理区的内存映射表。

  • zone_start_paddr:该管理区的起始物理地址。

  • zone_start_mapnr:在mem_map中的索引(或下标)。

  • name:该管理区的名字。

1.3.3 内核中获取内存的几种方式

操作系统的内存管理方案优劣是决定其效率高低的重要因素,时间与空间常常作为内存管理方案优劣的衡量指标。首先,分配/释放内存是一个发生频率很高的操作,所以它要求有一定的实时性,另外,内存又是一种非常宝贵的资源,所以要尽量减少内存碎片的产生。下面介绍内核获取内存的几种方式。

1. 伙伴算法分配大片物理内存

  • alloc_pages(gfp_mask, order):获得连续的页框,返回页描述符地址,是其他类型内存分配的基础。

  • _get_free_pages(gfp_mask, order):获得连续的页框,并返回页框对应的线性地址。线性地址与物理地址是内核直接映射方式。该方法不能用于大于896MB的高端内存。

2. slab缓冲区分配小片物理内存

内核提供了后备高速缓存机制,称为“slab分配器”。slab分配器实现的高速缓存具有kmem_cache_t类型,可通过调用kmem_cache_create创建。

  • kmem_cache_create:建立slab的高速缓冲区。

  • kmem_cache_alloc:试图从本地高速缓存获得一个空闲的对象。

  • kmalloc(gfp_mask, size):获得连续的以字节为单位的物理内存,返回线性地址。

3. 非连续内存区分配

vmalloc(size):分配非连续内存区,其线性地址连续,物理地址不连续,减少了外碎片,但是其性能低,因为要打乱内核页表。通常只是分配大内存,例如为活动的交互区分配数据结构、加载内核模块时分配空间、为I/O驱动程序分配缓冲区。

4. 高端内存映射

  • kmap(struct page * page):用于获得高端内存永久内核映射的线性地址。

  • kmap_atomic(struct page * page, enum km_type type):用于获得高端内存临时内核映射的线性地址。

5. 固定线性地址映射

  • set_fixmap(idx, phys):把一个物理地址映射到一个固定的线性地址上。

  • set_fixmap_nocache(idx, phys):把一个物理地址映射到一个固定的线性地址上,禁用该页高速缓存。

1.4 虚拟文件系统

虚拟文件系统的思想是把不同种类的文件系统的共同信息放入内核。其中一个字段或函数来支持Linux所支持的各种文件系统提供的操作。对所调用的读、写或其他函数,内核都能把它们替换成支持Linux文件系统、NFS文件系统,或者其他文件系统的实际函数。在第2章中,会讲到Linux的安装,在虚拟机上安装Linux,同时实现Linux和Windows文件共享,即实现在Linux环境下能够直接访问Windows的FAT32文件系统。

1.4.1 虚拟文件系统作用

虚拟文件系统(Virtual Filesystem),实际上是对各种文件系统的一种封装,为各种文件系统提供了一个通用的接口。通常情况下,为了实现不同操作系统下的文件访问,例如,复制/usr/local/arm目录下的zImage文件到/mnt/hgfs/Windows目录下。

$cp /usr/local/arm/zImage /mnt/hgfs/Windows/

在不同文件系统中实现文件复制,其执行的原理如图1.2所示。

图 1.2 不同文件系统中实现文件复制

VFS支持的文件系统可分为以下3个主要类型。

1. 磁盘文件系统

这些文件系统管理本地磁盘中可用的存储空间或者其他可以起到磁盘作用的设备(如USB闪存或硬盘)。这些文件系统包括:

  • Linux使用的第二扩展文件系统(Ext2),第三扩展文件系统(Ext3)及Reiser文件系统(TeiserFS)。

  • UNIX家族文件系统,如sysv文件系统(System V、Coherent、Xenix)、UFS(BSD、Solaris、NEXTSTEP),MINIX文件系统及VERITAS VxFS(SCO UnixWare)。

  • Windows支持的文件系统,如MS-DOS、FAT、FAT32、NTFS等文件系统。

  • ISO9660CD-ROM文件系统和通用磁盘的DVD文件系统。

  • 其他文件系统,如HPFS(IBM公司的OS/2)、HFS(苹果公司的Macintosh)、AFFS(Amiga公司的快速文件系统)及ADFS(Acorn公司的磁盘文件系统)。

2. 网络文件系统(NFS)

网络文件系统最主要的功能就是让网络上的主机可以共享目录及资料。将远端所共享出来的系统,挂载(mount)在本地端的系统上,然后就可以很方便地使用远端的资料,而操作起来就像在本地操作一样。使用NFS有相当多的好处,例如文档可以集中管理、节省磁盘空间、资源共享等。

3. 特殊文件系统

特殊文件系统可以为系统程序员和管理员提供一种容易的方式,来操作内核的数据结构并实现操作系统的特殊特征。

1.4.2 文件系统的注册

每个注册的文件系统是指可能会被挂载到目录树中的各个实际文件系统。实际文件系统,是指VFS中的实际操作最终要通过它们来完成而已,并不表示它们必须要存在于特定的某种存储设备上。注册过程实际上是将表示各实际文件系统的struct file_system_type数据结构的实例化,接着形成一个链表,内核中用一个名为file_systems的全局变量来指向该链表的表头。下面为file_system_type数据结构。

struct file_system_type {
    const char *name;
    int fs_flags;
    int (*get_sb) (struct file_system_type *, int, const char *, void *, struct vfsmount *);
    void (*kill_sb) (struct super_block *);
    struct module *owner;
    struct file_system_type * next;
    struct list_head fs_supers;
    struct lock_class_key s_lock_key;
    struct lock_class_key s_umount_key;
};

  • name:文件系统名,如ext2。

  • fs_flags:文件系统类型标志。

  • get_sb:读超级块的方法。

  • kill_sb:删除超级块的方法。

  • owner:指向实现文件模块的文件指针。

  • next:指向文件系统类型链表中下一个文件系统的指针。

  • fs_supers:具有相同文件系统类型的超级块对象链表的头。

1.4.3 文件系统的安装和卸载

在Linux系统中,同一个文件系统可以被多次安装。如果一个文件系统被安装多次,那么就可以通过这多个安装点来访问文件系统。尽管可以通过这多个安装点来访问,但是文件系统却只有一个。不论文件系统被安装了多少次,都只有一个超级块对象。安装一个文件系统遵循的步骤:

1. Linux系统内核必须首先检查有关参数的有效性。VFS首先应找到准备安装的文件系统。查找的方式是,通过查找file_systems指针指向的链表中的file_system_type数据结构项,来搜索已知的文件系统(该结构中包含文件系统的名字和指向VFS超级块读取程序地址的指针),当找到一个匹配的名字时,就可以得到读取文件系统超级块的程序地址。

2. 查找作为新文件系统安装点的VFS索引结点,并且同一目录下只能安装一个文件系统;VFS安装程序必须分配一个VFS超级块(super_block),并且向它传递一些有关文件系统安装的信息。

3. 申请一个vfsmount数据结构(其中包括存储文件系统的块设备的设备号、文件系统安装的目录和一个指向文件系统的VFS超级块的指针),并使它的指针指向所分配的VFS超级块。

4. 当文件系统安装以后,该文件系统的根索引结点就一直保存在VFS索引结点缓存中。

卸载文件系统:验证被卸载文件系统是否为可卸载的,如果该文件系统中的文件当前正被使用,则该文件系统不能被卸载;如果文件系统中的文件或目录正在使用,则VFS索引结点高速缓存中可能包含对应的VFS索引结点;如果相应的结点标志为“被修改过”,则该文件系统不能被卸载。如果验证被卸文件系统为可卸载的,就释放相应的VFS超级块和安装点,从而卸载该文件系统。

vfsmount数据结构如下:

struct vfsmount
{
        struct list_head mnt_hash;
        struct vfsmount *mnt_parent;          /* fs we are mounted on */
        struct dentry *mnt_mountpoint;        /* dentry of mountpoint */
        struct dentry *mnt_root;              /* root of the mounted tree */
        struct super_block *mnt_sb;           /* pointer to superblock */
        struct list_head mnt_mounts; /* list of children, anchoredhere */
        struct list_head mnt_child;  /* and going through their mnt_child */
        atomic_t mnt_count;
        int mnt_flags;
        int mnt_expiry_mark;         /* true if marked for expiry */
        char *mnt_devname;           /* Name of device e.g. /dev/dsk/hda1 */
        struct list_head mnt_list;
        struct list_head mnt_fslink; /* link in fs-specific expiry list */
        struct namespace *mnt_namespace;     /* containing namespace */
};

  • mnt_hash:用于散列表链表的指针。

  • mnt_parent:指向父文件系统,这个文件系统安装在其上。

  • mnt_mountpoint:指向这个文件系统安装点目录的dentry。

  • mnt_root:指向这个文件系统根目录的dentry。

  • mnt_sb:指向这个文件系统的超级块对象。

  • mnt_mounts:包含所有文件系统描述符的链表头。

  • mnt_child:用于已安装文件系统mnt_mounts的指针。

  • mnt_count:引用计数器,增加该值禁止文件系统被卸载。

  • mnt_flags:安装标志。

  • mnt_expiry_mark:如果文件系统到期就设置该标志为true。

  • mnt_devname:设备文件名。

  • mnt_list:已安装文件描述符的namespace链表的指针。

  • mnt_fslink:具体文件系统到期链表的指针。

  • mnt_namespace:指向安装了文件系统的namespace链表的指针。

1.5 设备驱动程序

设备驱动,实际上是硬件功能的一个抽象。针对同一个硬件不同的驱动可以将硬件封装成不同的功能。设备驱动是硬件层和应用程序(或者操作系统)的媒介,能够让应用程序或者操作系统使用硬件。在Linux操作系统下有3类主要的设备文件类型:块设备、字符设备和网络设备。设备驱动程序是指管理某个外围设备的一段代码,它负责传送数据、控制特定类型的物理设备的操作,包括开始和完成I/O操作,检测和处理设备出现的错误。

1.5.1 字符设备驱动程序

字符设备是一种能像字节流一样进行串行访问的设备,对设备的存取只能按顺序按字节存取而不能随机访问。字符设备没有请求缓冲区,必须按顺序执行所有的访问请求。应用程序对字符设备的访问是通过字符设备结点来完成的。字符设备是Linux中最简单的设备,可以像文件一样访问。应用程序使用标准系统调用打开、读、写和关闭字符设备,完全可以把它们当做普通文件一样进行操作,甚至被PPP守护进程使用,用于将一个Linux系统连接到网上的modem,也被看做一个普通文件。当字符设备初始化时,它的设备驱动程序向Linux内核注册,向chrdevs向量表中增加一个device_struct数据结构项。通常一种类型设备的主设备标识符是固定的。设备的主设备标识符(例如对于tty设备是4),用作该向量表的索引。chrdevs向量表中的每一项,即device_struct数据结构,包括两个元素:一个是指向登记的设备驱动程序名字的指针;另一个是指向一组文件操作的指针。这组文件操作本身位于这个设备的字符设备驱动程序中,每一个都处理一个特定的文件操作,如打开、读、写和关闭。常见的字符设备有鼠标、键盘、串口、控制台等。

用户进程通过设备文件对硬件进行访问,对设备文件的操作方式通过一些系统调用来实现,如open、read、write和close等。下面通过一个关键的数据结构file_operations,将系统调用和驱动程序关联起来。

struct file_operations {
    int (*seek) (struct inode * ,struct file *, off_t ,int);
    int (*read) (struct inode * ,struct file *, char ,int);
    int (*write) (struct inode * ,struct file *, off_t ,int);
    int (*readdir) (struct inode * ,struct file *, struct dirent * ,int);
    int (*select) (struct inode * ,struct file *, int ,select_table *);
    int (*ioctl) (struct inode * ,struct file *, unsined int ,unsigned long);
    int (*mmap) (struct inode * ,struct file *, struct vm_area_struct *);
    int (*open) (struct inode * ,struct file *);
    int (*release) (struct inode * ,struct file *);
    int (*fsync) (struct inode * ,struct file *);
    int (*fasync) (struct inode * ,struct file *,int);
    int (*check_media_change) (struct inode * ,struct file *);
    int (*revalidate) (dev_t dev);
};

该结构中每一个成员的名字都对应着一个系统调用。用户进程利用系统调用在对设备文件进行诸如read/write操作时,系统调用根据设备文件的主设备号找到对应的设备驱动程序,然后读取这个数据结构相应的函数指针,接着把控制权交给该函数。

编写驱动程序就是针对上面相应的函数编写具体的实现,然后将它们对应上。编写完驱动后,把驱动程序嵌入内核。驱动程序可以采用两种方式进行编译。一种是编译进内核,驱动被静态加载;另一种是编译成模块(modules),驱动模块需要动态加载。在模块被调入内存时,init()函数向系统的字符设备表登记了一个字符设备:

int __init chr_dev_init(void)
{
    if (devfs_register_chrdev(CHR_MAJOR,"chr_name",&chr_fops))
        printk("unable to get major %d for chr devs\n", MEM_MAJOR);
    …
    return 0;
}

当cleanup_chr_dev()函数被调用时,它释放字符设备chr_name在系统字符设备表中占有的表项。

void cleanup_chr_dev(void)
{
    unregister_chrdev(CHR_MAJOR,"chr_name");
}

1.5.2 块设备驱动程序

块设备具有请求缓冲区,从块设备读取数据时,可以从任意位置读取任意长度,即块设备支持随机访问而不必按照顺序存取数据。例如,可以先存取后面的数据,然后再存取前面的数据,字符设备则不能采用该方式存取数据。Linux下的磁盘设备均为块设备,应用程序访问Linux下的块设备结点是通过文件系统及其高速缓存来访问块设备的,并非直接通过设备结点读写块设备上的数据。

块设备既可以用做普通的裸设备存放任意数据,也可以将块设备按某种文件系统类型的格式进行格式化,然后读取块设备上的数据,读取时根据该文件系统类型的格式进行读取。无论使用哪种方式,访问设备上的数据都必须通过调用设备本身的操作方法实现。两者的区别在于前者直接调用块设备的操作方法,而后者则间接调用块设备的操作方法。常见的块设备有各种硬盘、flash磁盘、RAM磁盘等。

块设备也可以与字符设备register_chrdev、unregister_chrdev()函数类似的方法进行设备的注册与释放。但是,字符设备的register_chrdev()函数使用一个file_operations 结构的指针,而块设备的register_blkdev()函数则使用 block_device_operations 结构的指针,其中定义的open、release 和 ioctl 方法和字符设备的对应方法相同,但没有对read 和 write 操作定义。这是因为所有涉及块设备的 I/O 通常由系统进行缓冲处理。

块驱动程序最终必须提供完成实际块I/O操作的机制,在 Linux中,用于这些 I/O 操作的方法称为request(请求)。注册块设备时,需要对request队列进行初始化,这一动作通过blk_init_queue来完成,blk_init_queue函数创建队列,并将该驱动程序的request函数关联到队列。在模块的清除阶段,应调用blk_cleanup_queue函数。

初始化块设备的时候,将块设备注册到内核中,下面为块设备的注册函数mtdblock_release()的实现:

01  int register_blkdev(unsigned int major, const char *name)
02  {
03      struct blk_major_name **n, *p;
04      int index, ret = 0;
05      mutex_lock(&block_class_lock);
06      /*为块设备指定主设备号,如果指定为0则表示由系统来分配*/
07      if (major == 0) {
08          for (index = ARRAY_SIZE(major_names)-1; index > 0; index--) {
09              if (major_names[index] == NULL)
10                  break;
11          }
12          if (index == 0) {
13              printk("register_blkdev: failed to get major for %s\n",
14                    name);
15              ret = -EBUSY;
16              goto out;
17          }
18          major = index;
19          ret = major;
20      }
21      /*为块设备名字分配空间*/
22      p = kmalloc(sizeof(struct blk_major_name), GFP_KERNEL);
23      if (p == NULL) {
24          ret = -ENOMEM;
25          goto out;
26      }
27      p->major = major;
28      strlcpy(p->name, name, sizeof(p->name));
29      p->next = NULL;
30      index = major_to_index(major);
31      for (n = &major_names[index]; *n; n = &(*n)->next) {
32          if ((*n)->major == major)
33              break;
34      }
35      if (!*n)
36          *n = p;
37      else
38          ret = -EBUSY;
39      if (ret < 0) {
40          printk("register_blkdev: cannot get major %d for %s\n",
41                major, name);
42          kfree(p);
43      }
44  out:
45      mutex_unlock(&block_class_lock);
46      return ret;
47  }

块设备被注册到系统后,访问硬件的操作open和release等就能够被对应的系统调用指针所绑定,应用程序使用系统调用就可以对硬件进行访问了。下面是块设备主要的操作函数open()和release()。

下面为块设备open()操作函数。

01  static int mtdblock_open(struct mtd_blktrans_dev *mbd)
02  {
03      struct mtdblk_dev *mtdblk;
04      struct mtd_info *mtd = mbd->mtd;
05      int dev = mbd->devnum;
06      DEBUG(MTD_DEBUG_LEVEL1,"mtdblock_open\n");
07      if (mtdblks[dev]) {
08          /*如果设备已经打开,则只需要增加其引用计数*/
09          mtdblks[dev]->count++;
10          return 0;
11      }
12      /*为设备创建mtdblk_dev 对象保存mtd设备的信息*/
13      mtdblk = kzalloc(sizeof(struct mtdblk_dev), GFP_KERNEL);
14      if (!mtdblk)
15          return -ENOMEM;
16      mtdblk->count = 1;
17      mtdblk->mtd = mtd;
18      mutex_init(&mtdblk->cache_mutex);
19      mtdblk->cache_state = STATE_EMPTY;
20      if ( !(mtdblk->mtd->flags & MTD_NO_ERASE) && mtdblk->mtd->erasesize) {
21          mtdblk->cache_size = mtdblk->mtd->erasesize;
22          mtdblk->cache_data = NULL;
23      }
24      mtdblks[dev] = mtdblk;
25      DEBUG(MTD_DEBUG_LEVEL1, "ok\n");
26      return 0;
27  }

释放时递减用户计数,当用户计数递减为0时,释放缓存中的数据,并释放为设备分配的空间。

01  static int mtdblock_release(struct mtd_blktrans_dev *mbd)
02  {
03      int dev = mbd->devnum;
04      struct mtdblk_dev *mtdblk = mtdblks[dev];
05         DEBUG(MTD_DEBUG_LEVEL1, "mtdblock_release\n");
06      mutex_lock(&mtdblk->cache_mutex);
07      write_cached_data(mtdblk);
08      mutex_unlock(&mtdblk->cache_mutex);
09      if (!--mtdblk->count) {
10          /*用户计数递减为0时释放设备*/
11          mtdblks[dev] = NULL;
12          if (mtdblk->mtd->sync)
13              mtdblk->mtd->sync(mtdblk->mtd);
14          vfree(mtdblk->cache_data);
15          kfree(mtdblk);
16      }
17      DEBUG(MTD_DEBUG_LEVEL1, "ok\n");
18      return 0;
19  }

1.5.3 网络设备驱动程序

网络设备与字符设备的区别是,网络设备是面向数据报文的,而字符设备是面向字符流的。网络设备与块设备的区别是,网络设备不支持随机访问,也没有请求缓冲区。在Linux里网络设备也可以被称为网络接口,如eth0,应用程序是通过Socket(套接字),而不是设备结点来访问网络设备,在系统中不存在网络设备结点。

网络设备用来与其他设备交换数据,它可以是硬件设备,也可以是纯软件设备,如loopback接口。网络设备由内核中的网络子系统驱动,负责发送和接收数据包,但它不需要了解每项事务如何映射到实际传送的数据包。许多网络连接(例如使用TCP协议的连接)是面向流的,但网络设备围绕数据包的传输和接收设计。网络驱动程序不需要知道各个连接的相关信息,它只需处理数据包。字符设备和块设备都有设备号,而网络设备没有设备号,只有一个独一无二的名字,例如eth0、eth1等,这个名字也无须与设备文件结点对应。内核利用一组数据包传输函数与网络设备驱动程序进行通信,它们不同于字符设备和块设备的read()和write()方法。

Linux网络设备驱动程序从下到上分为4层,依次为网络设备与媒介层、设备驱动功能层、网络设备接口层和网络协议接口层。在设计具体的网络设备驱动程序时,需要完成的主要工作是编写设备驱动功能层的相关函数以填充net_device数据结构的内容,并将net_device注册入内核。

下面以DM9000代码为例说明网络设备驱动的注册、注销等主要过程。其驱动的注册过程在设备初始化时被调用。

01  static int __init dm9000_init(void)
02  {
03      printk(KERN_INFO   "%s   Ethernet   Driver,   V%s\n",   CARDNAME,   DRV_VERSION);
04      return platform_driver_register(&dm9000_driver);
05  }

驱动注册函数。

01  int platform_driver_register(struct platform_driver *drv)
02  {
03      drv->driver.bus = &platform_bus_type;
04      if (drv->probe)
05          drv->driver.probe = platform_drv_probe;
06      if (drv->remove)
07          drv->driver.remove = platform_drv_remove;
08      if (drv->shutdown)
09          drv->driver.shutdown = platform_drv_shutdown;
10      if (drv->suspend)
11          drv->driver.suspend = platform_drv_suspend;
12      if (drv->resume)
13          drv->driver.resume = platform_drv_resume;
14      return driver_register(&drv->driver);
15  }

在网络设备被清除时调用注销网络设备驱动函数。

01  static void __exit dm9000_cleanup(void)
02  {
03      platform_driver_unregister(&dm9000_driver);
04  }

驱动注销过程。驱动注销的过程中还包括将设备从系统中移除和将驱动从总线上移植。

01  void platform_driver_unregister(struct platform_driver *drv)
02  {
03      driver_unregister(&drv->driver);
04  }
05  void driver_unregister(struct device_driver *drv)
06  {
07      driver_remove_groups(drv, drv->groups);
08      bus_remove_driver(drv);
09  }
10  static void driver_remove_groups(struct device_driver *drv, struct attribute_group **groups)
11  {
12      int i;
13      if (groups)
14          for (i = 0; groups[i]; i++)
15              sysfs_remove_group(&drv->p->kobj, groups[i]);
16  }
17  void bus_remove_driver(struct device_driver *drv)
18  {
19      if (!drv->bus)
20          return;
21      remove_bind_files(drv);
22      driver_remove_attrs(drv->bus, drv);
23      driver_remove_file(drv, &driver_attr_uevent);
24      klist_remove(&drv->p->knode_bus);
25      pr_debug("bus:   '%s':   remove   driver   %s\n",   drv->bus->name,   drv->name);
26      driver_detach(drv);
27      module_remove_driver(drv);
28      kobject_put(&drv->p->kobj);
29      bus_put(drv->bus);
}

有关网络设备驱动的详细接口函数解析和驱动移植将在后面的章节中叙述。

1.5.4 内存与I/O操作

一般来说,在系统运行时,外设的I/O内存资源的物理地址是已知的,由硬件的设计决定。但是CPU通常并没有为这些已知的外设I/O内存资源的物理地址,预定义虚拟地址范围,驱动程序并不能直接通过物理地址访问I/O内存资源,只能先将它们映射到内核的虚拟地址空间内(通过页表),然后才能根据映射的内核虚拟地址范围,通过访内指令访问这些I/O内存资源。Linux在io.h头文件中声明了函数ioremap(),用来将I/O内存资源的物理地址映射到核心虚拟地址空间(3GB~4GB)中,原型如下:

void * ioremap(unsigned long phys_addr, unsigned long size, unsigned long flags);

iounmap()函数用于取消ioremap()所做的映射,原型如下:

void iounmap(void * addr);

在将I/O内存资源的物理地址映射成内核的虚拟地址后,理论上可以像读写RAM那样直接读写I/O内存资源了。为了保证驱动程序跨平台的可移植性,应该使用Linux中特定的函数访问I/O内存资源,而不应该通过指向内核虚拟地址的指针来访问。如在ARM平台上,读写I/O的函数如下:

#define __raw_base_writeb(val,base,off) __arch_base_putb(val,base,off)
#define __raw_base_writew(val,base,off) __arch_base_putw(val,base,off)
#define __raw_base_writel(val,base,off) __arch_base_putl(val,base,off)

#define __raw_base_readb(base,off) __arch_base_getb(base,off)
#define __raw_base_readw(base,off) __arch_base_getw(base,off)
#define __raw_base_readl(base,off) __arch_base_getl(base,off)

驱动程序中mmap()函数的实现原理是,用mmap映射一个设备,表示将用户空间的一段地址关联到设备内存上,这样当程序在分配的地址范围内进行读取或者写入时,实际上就是对设备的访问。这一映射原理类似于Linux下mount命令,将一种类型的文件系统或设备挂载到另外一个文件系统或者目录下时,挂载成功后,对挂载点的任何操作实际上是对被挂载的文件系统和设备的操作。

1.6 小结

Linux内核是一个比较庞大的系统,深入理解内核可以减少在系统移植中的障碍。在系统移植中设备驱动开发是一项很复杂的工作,由于Linux内核提供了一部分源代码,同时还提供了对某些公共部分的支持,例如,USB驱动对读写U盘、键盘、鼠标等设备提供了通用驱动程序,一般情况可以直接使用内核提供的驱动。但是对于复杂的USB设备没有现成的驱动,就需要读者对驱动开发过程有一定的认识,必要时参考Linux源码重新开发驱动程序。

目录

  • 前言
  • 第 1 篇 系统移植基础篇
  • 第 1 章 Linux内核介绍
  • 第 2 章 嵌入式Linux开发环境搭建
  • 第 2 篇 系统移植技术篇
  • 第 3 章 Bootloader移植
  • 第 4 章 Linux内核裁剪与移植
  • 第 5 章 嵌入式文件系统制作
  • 第 3 篇 系统移植与驱动篇
  • 第 6 章 LCD驱动移植
  • 第 7 章 触摸屏驱动移植
  • 第 8 章 USB设备驱动移植
  • 第 9 章 网卡驱动程序移植
  • 第 10 章 音频设备驱动程序移植
  • 第 11 章 SD卡驱动移植
  • 第 12 章 NandFlash驱动移植
  • 第 4 篇 系统移植高级篇
  • 第 13 章 MiniGUI与移植
  • 第 14 章 Qt开发与Qtopia移植
  • 第 15 章 嵌入式数据库Berkeley DB移植
  • 第 16 章 嵌入式数据库SQLite移植
  • 第 17 章 嵌入式Web服务器BOA移植
  • 第 18 章 嵌入式Web服务器Thttpd移植
  • 第 19 章 JVM及其移植
  • 第 20 章 VoIP技术与Linphone编译