⭐ 欢迎来到虫虫下载站! | 📦 资源下载 📁 资源专辑 ℹ️ 关于我们
⭐ 虫虫下载站

📄 (ldd) ch02-编写和运行模块(转载).txt

📁 献给ARM初学者
💻 TXT
📖 第 1 页 / 共 4 页
字号:
(LDD) Ch02-编写和运行模块(转载)
      第2章 编写和运行模块
       
       
      非常高兴现在终于可以开始编程了。本章将介绍模块编程和内核编程所需的所有必要的
      概念。我们将要不多的篇幅来编写和运行一个完整的模块。这种专业技术(expertise)
      是编写如何模块化设备驱动程序的基础。为了避免一下子给你很多概念,本章仅介绍模
      块,不介绍任何类别的设备。
       
      这里介绍的所有内核内容(函数,变量,头文件和宏)也将在本章最后的参考部分再次
      介绍。
       
      如果你已经座不住了,下面的代码是一个完整的“Hello, World”模块(这个模块事实
      上并没什么功能)。它可以在Linux 2.0或以上版本上编译通过,但不能低于或等于1.2
      ,关于这一点本章将在稍后的部分解释*。
       

       
      (代码)
       
      函数printk是由Linux内核定义的,功能与printf相似;模块可以调用printk,这是因为
      在insmod加载了模块后,模块就被连编到内核中了,也就可以调用内核的符号了。字符
      串<1>是消息的优先级。我之所以在模块中使用了高优先级是因为,如果你使用的是内核
      2.0.x和旧的klogd守护进程,默认优先级的消息可能不能显示在控制台上(关于这个问
      题,你可以暂且忽略,我们将在第4章,“调试技术”,的“Printk”小节中详细解释)
       
      通过执行insmod和rmmod命令,你可以试试这个模块,其过程如下面的屏幕输出所示。注
      意,只有超级用户才能加载 托对啬 块。
       
      (代码)
       
      正如你所见,编写一个模块很容易。通过本章我们将深入探讨这个内容。
       
      模块与应用程序
      在深入探讨模块之前,很有必要先看一看内核模块与应用程序之间的区别。
       
      一个应用从头到尾完成一个任务,而模块则是为以后处理某些请求而注册自己,完成这
      个任务后它的“主”函数就立即中止了。换句话说就是,init_module()(模块的入口点
      )的任务就是为以后调用模块的函数做准备;这就好比模块在说,“我在这,这是我能

      )的任务就是为以后调用模块的函数做准备;这就好比模块在说,“我在这,这是我能
      做的。”模块的第二个入口点,cleanup_module,仅当模块被下载前才被调用。它应该
      跟内核说,“我不在这了,别再让我做任何事了。”能够卸载也许是你最喜爱的模块化
      的特性之一,它可以让你减少开发时间;你无需每次都花很长的时间开机关机就可以测
      试你的设备驱动程序。
       
      作为一个程序员,你一定知道一个应用程序可以调用应用程序本身没有定义的函数:前
      后的连编过程可以用相应的函数库解析那些外部引用。printf就是这样一个函数,它定
      义在libc中。然而,内核要仅能连编到内核中,它能调用的仅是由内核开放出来的那些
      函数。例如,上面的helllo.c中的printk函数就是内核版的printf,并由内核开放给模
      块给使用;除了没有浮点支持外,它和原函数几乎一模一样。
       
      如图2-1所示,它勾画了为了在运行的内核中加入新函数,是如何调用函数以及如何使用
      函数指针的。
       
      由于没有库连接到模块中,源码文件不应该模块任何常规头文件。与内核有关的所有内
      容都定义在目录/usr/include/linux和/usr/include/asm下的头文件中。在编译应用程
      序也会间接使用这些头文件;其中的内核代码通过#ifdef __KERNEL__保护起来。这两个
      内核头文件目录通常都是到内核源码所在位置的符号连接。如果你根本就想要整个内核
      源码,你至少还要这两个目录的头文件。在比较新的内核中,你还可以在内核源码中发
      现net和scsi头文件目录,但很少有模块会需要这两个目录。
       
      内核头文件的作用将稍后需要它们的地方再做介绍,

      内核头文件的作用将稍后需要它们的地方再做介绍,
       
      内核模块与应用程序的另一个区别是,你得小心“名字空间污染”问题。程序员在写小
      程序时,往往不注意程序的名字空间,但当这些小程序成为大程序的一部分时就会造成
      许多问题了。名字空间污染是指当存在很多函数和全局变量时,它们的名字已不再富有
      足够的意义来很容易的区分彼此的问题。不得不处理这种应用程序的程序员必须花很大
      的精力来单单记住这些“保留”名,并为新符号寻找新的唯一的名字。如果在写内核代
      码时出现这样的错误,这对我们来说是无法忍受的,因为即便最小的模块也要连编到整
      个内核中。防止名字空间污染的最佳方法是把所有你自己的符号都声明为static的,而
      且给所有的全局量加一个well-defined前缀。此外,你还可以通过声明一个符号表来避
      免使用static声明,这些内容将在本章的“注册符号表”小节中介绍。即便是模块内的
      私有符号也最好使用选定的前缀,这样有时会减轻调试的工作。通常,内核中使用的前
      缀都是小写的,今后我们将贯彻这一约定。
       
      内核编程和应用程序编程的最后一个区别是如何处理失效:在应用程序开发期间,段违
      例是无害的,利用调试器可以轻松地跟踪到引起问题的错误之处,然而内核失效却是致
      命的,如果不是整个系统,至少对于当前进程是这样的。我们将在第4章“调试系统失效
      ”小节中介绍如何跟踪内核错误。
       
      用户空间和内核空间
      本节的讨论概而言之就是,模块是在所谓的“内核空间”中运行的,而应用程序则是在
      “用户空间”中运行的。这些都是操作系统理论的最基本概念。
       

       
      事实上,操作系统的作用就是给程序提供一个计算机硬件的一致的视图。此外,操作系
      统处理程序的独立操作,并防止对资源的未经授权的访问。当且仅当CPU可以实现防止系
      统软件免受应用软件干扰的保护机制,这些不同寻常的工作才有可能实现。
       
      每种现代处理器都能实现这种功能。人们选择的方案是在CPU内部实现不同的操作模式(
      或级)。不同的级有不同的作用,而且某些操作不允许在最低级使用;程序代码仅能通
      过有限数目的“门”从一个级切换到另一个级。Unix系统就是充分利用这一硬件特性设
      计而成的,但它只使用了两级(与此不同,例如,Intel处理器就有四级)。在Unix系统
      中,内核在最高级执行(也称为“管理员态”),在这一级任何操作就可以,而应用程
      序则执行在最低级(所谓的“用户态”),在这一级处理器禁止对硬件的直接访问和对
      内存的未授权访问。
       
      正如前面所述,在谈到软件时,我们通常称执行态为“内核空间”和“用户空间”,它
      们分别引用不同的内存映射,也就是程序代码使用不同的“地址空间”。
       
      Unix通过系统调用和硬件中断完成从用户空间到内核空间的控制转移。执行系统调用的
      内核代码在进程的上下文上执行――它代表调用进程操作而且可以访问进程地址空间的
      数据。但与此不同,处理中断的代码相对进程而言是异步的,而且与任何一个进程都无
      关。
       
      模块的功能就是扩展内核的功能;运行在内核中的模块化的代码。通常,一个设备驱动
      程序完成上面概括的两个任务:模块的某些函数做为系统调用执行,而某些函数则负责

       
      事实上,操作系统的作用就是给程序提供一个计算机硬件的一致的视图。此外,操作系
      统处理程序的独立操作,并防止对资源的未经授权的访问。当且仅当CPU可以实现防止系
      统软件免受应用软件干扰的保护机制,这些不同寻常的工作才有可能实现。
       
      每种现代处理器都能实现这种功能。人们选择的方案是在CPU内部实现不同的操作模式(
      或级)。不同的级有不同的作用,而且某些操作不允许在最低级使用;程序代码仅能通
      过有限数目的“门”从一个级切换到另一个级。Unix系统就是充分利用这一硬件特性设
      计而成的,但它只使用了两级(与此不同,例如,Intel处理器就有四级)。在Unix系统
      中,内核在最高级执行(也称为“管理员态”),在这一级任何操作就可以,而应用程
      序则执行在最低级(所谓的“用户态”),在这一级处理器禁止对硬件的直接访问和对
      内存的未授权访问。
       
      正如前面所述,在谈到软件时,我们通常称执行态为“内核空间”和“用户空间”,它
      们分别引用不同的内存映射,也就是程序代码使用不同的“地址空间”。
       
      Unix通过系统调用和硬件中断完成从用户空间到内核空间的控制转移。执行系统调用的
      内核代码在进程的上下文上执行――它代表调用进程操作而且可以访问进程地址空间的
      数据。但与此不同,处理中断的代码相对进程而言是异步的,而且与任何一个进程都无
      关。
       
      模块的功能就是扩展内核的功能;运行在内核中的模块化的代码。通常,一个设备驱动
      程序完成上面概括的两个任务:模块的某些函数做为系统调用执行,而某些函数则负责

      程序完成上面概括的两个任务:模块的某些函数做为系统调用执行,而某些函数则负责
      处理中断。
       
      内核中的并发
      内核编程新手首先要问的问题之一就是多任务是如何管理的。事实上,除了调度器之外
      ,关于多任务并没有什么可以多说的,而且调度器也超出了程序员的一般活动范围。你
      可能会遇到这些任务,除了掌握如下这些原则外,模块编写者无需了解多任务。
       
      与串行的应用程序不同,内核是异步工作的,代表进程执行系统调用。内核负责输入/输
      出以及系统内对每一个进程的资源管理。
       
      内核(和模块)函数完全在一个线程中执行,除非它们要“睡眠”,否则通常都是在单
      个进程的上下文中执行――设备驱动程序应该能够通过交织不同任务的执行来支持并发
      。例如,设备可能由两个不同的进程同时读取。设备驱动程序串行地响应若干read调用
      ,每一个都属于不同的进程。由于代码需要区别不同的数据流,内核(以及设备驱动程
      序)必须维护内部数据结构以区分不同的操作。这与一个学生学习交织在一起的若干门
      课程并非不无相似之处:每门课都有一个不同的笔记本。解决多个访问问题的另一个方
      法就是避免它,禁止对设备的并发访问,但这种怠惰的技术根本不值的讨论。
       
      当内核代码运行时,上下文切换不可能无意间发生,所以设备驱动程序无需是可重入的
      ,除非它自己会调用schedule。必须等待数据的函数可以调用sleep_on,这个函数接着
      又调用schedule。不过你必须要小心,存在某些函数会无意导致睡眠,特别是任何对用
      户空间的访问。利用“天然非抢占”特性不是什么好的方法。我将在第5章,“字符设备

      户空间的访问。利用“天然非抢占”特性不是什么好的方法。我将在第5章,“字符设备
      驱动程序的扩展操作”的“编写可重入代码”小节中讲解可重入函数。
       
      就对设备驱动程序的多个访问而言,有许多不同的途径来分离这些不同的访问,但都是
      依赖于任务相关的数据。这种数据可以是全局内核变量或是传给设备驱动程序函数的进
      程相关参数。最重要的用来跟踪进程的全局变量是current:一个指向struct
      task_struct结构的指针,在<linux/sched.h>中定义。current指针指向当前正在运行的
      用户进程。在系统调用执行期间,如open或read,当前进程就是调用这个调用的进程*。
      如果需要的话,内核代码就可以利用current使用进程相关信息。第5章“设备文件的访
      问控制”小节中就有使用这种技术的例子。
       
      编译器就象外部引用printk一样处理current。模块可以在任何需要的地方引用current
      ,insmod会在加载时解析出所有对它的引用。例如,如下语句通过访问struct
      task_struct中的某些域打印当前进程的进程ID和命令名:
       

⌨️ 快捷键说明

复制代码 Ctrl + C
搜索代码 Ctrl + F
全屏模式 F11
切换主题 Ctrl + Shift + D
显示快捷键 ?
增大字号 Ctrl + =
减小字号 Ctrl + -