Linux驱动入门——基础概念


Linux内核简介

Unix的历史

Unix是从贝尔实验室的一个失败的多用户操作系统Multics中涅槃而生的。Multics项目被终止后,贝尔实验室科学研究中心的人们发现自己处于一个没有交互式操作系统可用的境地。在这种情况下,1969年的夏天,贝尔实验室的程序员们设计了一个文件系统系统原型,而这个原型最终发展演化成了Unix
由于最初一六的设计和以后多年的创新与逐步提高,Unix系统成为一个强大、健壮和稳定的操作系统。下面的几个特点是使Unix强大的根本原因:

  • Unix很简洁,不像其他动辄提供数千个系统调用并且设计目的不明确的系统,Unix仅提供几百个系统调用并且有着非常明确的设计目的。
  • 在Unix中,所有的东西都被当做文件对待。
  • Unix的内核和相关的系统工具软件是用C语言编写而成——正是这一特点使得Unix在各种硬件体系架构面前都具备令人惊异的移植能力,并且使广大的开发人员很容易就能接受它。
  • Unix的进程创建非常迅速,并且有一个非常独特的fork()系统调用。
  • Unix提供了一套非常简单但又很稳定的进程间通信元语,快速简洁的进程创建过程使Unix的程序把目标放在一次执行保质保量地完成一个任务上,而简单稳定的进程间通信机制又可以保证这些单一目的的简单程序可以方便地组合在一起,去解决现实中变得越来越复杂的任务。
    今天,Unix已经发展成为一个支持抢占式、多线程、虚拟内存、换页、动态链接和TCP/IP网络的现代化操作系统。Unix的不同变体被应用在大到数百个CPU集群、小到嵌入式设备的各种系统上。

Linux简介

1991年,Linus Torvalds为当时新推出的,使用Intel 80386微处理器的计算机开发了一款全新的操作系统,Linux由此诞生。那时,作为芬兰赫尔辛基大学的一名学生的Linus,正为不能随心所欲使用强大而自由的Unix系统而苦恼。对Torvalds而言,使用当时流行的Microsoft的DOS系统,除了玩波斯王子游戏外,别无他用。Linus热衷于使用Minix,一种教学用的廉价Unix,但是,他不能轻易修改和发布该系统的源码(由于Minix的许可证),也不能对Minix开发者所作的设计轻举妄动,这让他耿耿于怀并由此对作者的设计理念感到失望。
Linus像任何一名生机勃勃的大学生一样决心走出这种困境:开发自己的操作系统。他开始写了一个简单的终端仿真程序,用于连接到本校的大型Unix系统上。他的终端仿真程序经过一学年的研发,不断改进和完善。不久,Linus手上就有了虽不成熟但五脏俱全的Unix。1991年底,他在Internet上发布了早期版本。
从此Linux便启航了,最初的Linux发布很快赢得了众多用户。而实际上,他成功的重要因素是,Linux很快吸引了很多开发者、黑客对其代码进行修改和完善。由于其许可条款的约定Linux迅速成为多人的合作开发项目。

操作系统和内核简介

操作系统是指在整个系统中负责完成最基本功能和系统管理的那些部分。这些部分应该包括内核、设备驱动程序、启动引导程序、命令行Shell或者其他种类用户界面、基本的文件管理工具和系统工具
内核有时被称作是管理者或操作系统核心。通常一个内核由负责响应中断的中断服务程序,负责管理多个进程从而分享处理器时间的调度程序、负责管理进程地址空间的内存管理程序和网络、进程间通信等系统服务程序共同组成。对于提供保护机制的现代操作系统来说,内核独立于普通应用程序,他一般处于系统态,拥有受保护的内存空间和访问硬件设备的所有权限。这种系统态和被保护起来的内存空间,统称为内核空间
在系统中运行的引用程序通过系统调用来与内核通信。应用程序通常调用库函数(比如C库函数)再由库函数通过系统调用界面,让内核代其完成各种不同的任务。一些库函数提供了系统调用不具备的许多功能,在那些较为复杂的函数中,调用内核的操作通常是这个工作的一个步骤而已。
内核还要负责管理系统的硬件设备。现有的几乎所有的体系结构,包括全部Linux支持的体系结构,都提供了中断机制。当硬件设备想和系统通信的时候,它首先要发出一个异步的中断信号去打断处理器的执行,继而打断内核的执行。中断通常对应着一个中断号,内核通过这个中断号去查找相应的中断服务程序,并调用这个程序响应和处理中断。许多操作系统的中断服务程序,包括Linux的,都不在进程上下文执行。它们在一个与所有进程都无关的、专门的中断上下文中运行。之所以存在这样一个专门的执行环境,就是为了保证中断服务程序能够在第一时间响应和处理中断请求,然后快速退出。
我们可以将每个处理器在任何指定时间点上的活动必然概括为下列三者之一:
在这里插入图片描述

  • 运行于用户空间,执行用户进程
  • 运行于内核空间,处于进程上下文,代表某个特定的进程执行。
  • 运行于内核空间,处于中断上下文,与任何进程无关,处理某个特定的中断。

单内核与微内核设计之比较

操作系统内核可以分为两大阵营:单内核和微内核(第三阵营是外内核,主要用于科研系统中)。
单内核是两大阵营中一种较为简单的设计。所谓单内核就是把它从整体上作为一个单独的大过程来实现,同时也运行在一个单独的地址空间上。因此,这样的内核通常以单个静态二进制文件的形式存放于磁盘中。所有内核服务都在这样的一个大内核地址空间上运行。内核之间的通信是微不足道的,因为大家都在内核态运行,并且处于同一地址空间:内核直接调用函数,这与用户空间应用程序没有什么区别。这种模式的支持者认为单模块具有简单和高性能的特点。
微内核不是作为一个单独的大过程来实现,微内核的功能被划分为多个独立的过程,每个过程叫做一个服务器。理想情况下,只有强烈请求特权服务的服务器才运行在特权模式下,其他服务器都运行在用户空间。不过,所有的服务器都保持独立并运行在各自的地址空间上。因此,就不可能像单模块内核那样直接调用函数,而是通过消息传递处理微内核通信:系统采用进程间通信(IPC)机制,因此,各个服务器之间通过IPC机制互通消息,互换“服务”。服务器的各自独立有效避免了一个服务器的失效祸及另一个服务器。
Linux是一个单内核,也就是说,Linux内核运行在单独的内核地址空间上。不过,Linux汲取了微内核的精华:其引以为豪的是模块化设计、抢占式内核、支持内核线程以及动态装载内核模块的能力。不仅如此,Linux还避免其微内核设计上性能损失的缺陷,让所有事情都运行在内核态,直接调用函数,无须消息传递。至今,Linux是模块化的,多线程的以及内核本身可调度的操作系统,食用主义再次占了上风。

小结

这部分主要用以介绍Linux相关的基础概念。详细的参考资料可加群下载原书籍查阅
qq:602877925

设备驱动简介

驱动程序的角色

作为一个程序员,你能够对你的驱动作出你自己的选择,并且在所需的编程实践和结果的灵活性之间,选择一个可接受的平衡。尽管说一个驱动是“灵活”的,听起来有些奇怪,但是我们喜欢这个字眼,因为它强调了一个驱动程序的角色是提供机制,而不是策略
机制和策略的区分是其中一个在Unix设计背后的最好观念。大部分的编程问题其实可以划分为2部分:“提供什么能力”(机制)和“如何使用这些能力”(策略)。如果这两方面由程序的不同部分来表达,或者甚至由不同的程序共同表达,软件包是非常容易开发和适应特殊的需求。
你也可以从不同的角度看你的驱动:它是一个存在于应用程序和实际设备间的软件层。驱动的这种特权的角色允许驱动程序员严密地选择设备应该如何表现:不同的驱动可以提供不同的能力,甚至是同一设备。实际的驱动设计应当是在许多不同考虑中的平衡。例如,一个单个设备可能由不同的程序并发使用,驱动程序员有完全的自由来决定如何处理并发性。你能在设备上实现内存映射而不依赖它的硬件能力,或者你能提供用户一个用户库来版主应用程序员在可用的原语之上实现新策略,等等。一个主要的考虑是在展现给用户尽可能多的选项,和你不得不花费的编写驱动的时间之间作出平衡,还有需要保持事情简单以避免错误潜入。

划分内核

内核的角色可以划分成下列几个部分(如下图):
在这里插入图片描述

  • 进程管理——内核负责创建和销毁进程,并处理它们与外部世界的联系(输入和输出)。不同进程间通讯(通过信号,管道,或者进程间通讯原语)对于整个系统功能来说是基本的,也由内核处理。另外,调度器,控制进程如何共享CPU,是进程管理的一部分。更通常地,内核的进程管理活动实现了多个进程在一个单个或几个CPU之上的抽象。
  • 内存管理——计算机的内存是主要的资源,处理它所用的策略对系统性能是至关重要的。内核为所有进程的每一个都在有限的可用资源上建立了一个虚拟地址空间。内核的不同部分与内存管理子系统通过一套函数调用交互,从简单的malloc/free对到更多更复杂的功能。
  • 文件系统——Unix在很大程度上基于文件系统的概念;几乎Unix中任何东西都可看作一个文件。内核在非结构化的硬件之上建立了一个结构化的文件系统,结果是文件的抽象非常多地在整个系统中应用。另外,Linux支持多个文件系统类型,也就说,物理介质上不同的数据组织方式。例如,磁盘可被格式化成标准Linux的ext3文件系统,普遍使用的FAT文件系统,或者其他几个文件系统。
  • 设备控制——几乎每个系统操作最终都映射到一个物理设备上。除了处理器,内存和非常少的别的实体外,全部中的任何设备控制操作都由特定于要寻址的设备相关的代码来进行。这些代码称为设备驱动。内核中必须嵌入系统中出现的每个外设的驱动,从硬盘驱动到键盘和磁带驱动器。
  • 网络——网络必须由操作系统来管理,因为大部分网络操作不是特定于某一个进程:进入系统的报文是异步事件。报文在某一个进程接手之前必须被收集,识别,分发。系统负责在程序和网络接口之间递送数据报文,它必须根据程序的网络活动来控制程序的执行。另外,所有的路由和地址解析问题都在内核中实现。

Linux的众多优良特性之一就是可以在运行时扩展由内核提供的特性的能力。这意味着你可以在系统正在运行着的时候增加内核的功能(也可以去除)。
每块可以在运行时添加到内核的代码,被称为一个模块。Linux内核提供了对许多模块类型的支持,包括但不限于,设备驱动。每个模块由目标代码组成(没有连接成一个完整可执行文件),可以动态连接到运行中的内核中,通过insmod程序加载,以及通过rmmod程序去卸载。

设备和模块的分类

以Linux的方式来看待设备可区分为3种基本设备类型。每个模块常常实现3种类型种的一种,因此可分类成字符模块,块模块,或者一个网络模块。

  • 字符设备——一个字符(char)设备是一种可以当作一个字节流来存取的设备(如同一个文件);一个字符驱动负责实现这种行为。这样的驱动常常至少实现open、close、read和write系统调用。文本控制台(/dev/console)和串口(/dev/ttyS0及其友)是字符设备的例子,因为它们很好地展现了的抽象。字符设备通过文件系统节点来存取,例如/dev/tty1/dev/lp0。在一个字符设备和一个普通文件之间唯一有关的不同就是,你经常可以在普通文件中移来移去,但是大部分字符设备仅仅是数据通道,你只能顺序存取。然而,存在看起来像数据区的字符设备,你可以在里面移来移去。例如,frame grabber经常这样,应用程序可以使用mmaplseek存取整个要求的图像。
  • 块设备——如同字符设备,块设备通过位于/dev目录的文件系统节点来存取。一个块设备(例如磁盘)应该是可以驻有一个文件系统的。大部分的Unix系统,一个块设备只能处理这样的I/O操作,传送一个或多个长度经常是512字节(或一个更大的2的冥的数)的整块。Linux,相反,允许引用程序读写一个块设备像字符设备一样,它允许一次传送任意数目的字节。结果就是,块和字符设备的区别仅仅在内核在内部管理数据的方式上,并且因此在内核/驱动的软件接口上不同。如同一个字符设备,每个块设备都通过一个文件系统节点被存取,它们之间的区别对用户是透明的。块驱动和字符驱动相比,与内核的接口完全不同。
  • 网络接口——任何网络事务都通过一个接口来进行,就是说,一个能够与其他主机交换数据的设备。通常,一个接口是一个硬件设备,但是它也可能是一个纯粹的软件设备,比如环回接口。一个网络接口负责发送和接受数据报文,在内核网络子系统的驱动下,不必知道单个事务是如何映射到实际的被发送的报文上的。很多网络连接(特别那些使用TCP的)是面向流的,但是网络设备却常常设计成处理报文的发送和接收。一个网络驱动对单个连接一无所知;它只处理报文。既然不是一个面向流的设备,一个网络接口就不像/dev/tty1那么容易映射到文件系统的一个节点上。Unix提供的对接口的存取方式仍然是通过分配一个名字给它们(例如eth0),但是这个名字在文件系统中没有对应的入库。内核与网络设备驱动间的通讯与字符设备和块设备驱动所用的完全不同。不用read和write,而是内核调用和报文传递相关的函数。

有其他的划分驱动模块的方式,与上面的设备类型是正交的。通常,某些类型的驱动与给定类型设备的其他层的内核支持函数一起工作。例如USB模块,串口模块,SCSI模块等等。每个USB设备由一个USB模块驱动,与USB子系统一起工作,但是设备自身在系统中表现为一个字符设备(比如一个USB串口),一个块设备(一个USB内存读卡器),或者一个网络设备(一个USB以太网接口)。
在设备驱动之外,别的功能,不论硬件和软件,在内核中都是模块化的。一个普通的例子是文件系统。一个文件系统类型决定了在块设备上信息是如何组织的,以便能表示一棵目录与文件的树。这样的实体不是设备驱动,因为没有明确的设备与信息摆放方式相联系;文件系统类型却是一种软件驱动,因为它将低级数据结构映射为高级的数据结构。文件系统决定了一个文件名多长,以及在一个目录入口中存储每个文件的什么信息。文件系统模块必须实现最低级的系统调用,来存取目录和文件,通过映射文件名和路径(以及其他信息,例如存取模式)到保存在数据块中的数据结构。这样的一个接口是完全与数据被传送来去磁盘(或其他介质)相互独立,这个传统是由一个块设备驱动完成的。

安全问题

安全是当今重要性不断增长的关注点。系统中任何安全检查都由内核代码强加上去,如果内核有安全漏洞,系统作为一个整体就有漏洞。在官方的内核发布里,只有一个有授权的用户可以加载模块;系统调用init_module检查调用进程是否有权加载模块到内核里。因此,当运行一个官方内核时只有超级用户或者一个成功获得特权的入侵者,才可以利用特权代码的能力。

作为一个设备驱动编写者,你应当知道在什么情形下,某些类型的设备存取可能反面地影响系统作为一个整体,并且应当提供足够地控制。例如,会影响全局资源的设备操作(例如设置一条中断线),可能会损坏硬件(例如,加载固件),或者它可能会影响其他用户(例如设置一个磁带驱动的缺省的块大小),常常是只对有足够授权的用户,并且这种检查必须由驱动自身进行。
驱动编写者也必须要小心,当然,来避免引入安全bug。C编程语言使得易于犯下几类的错误。例如,许多现今的安全问题是由于缓冲区覆盖引起的,它是由于程序员忘记检查有多少数据写入缓冲区,数据在缓冲区结尾之外结束,因此覆盖了无关的数据。这样的错误可能会危及整个系统的安全,必须避免。幸运的是,在设备驱动上下文中避免这样的错误经常是相对容易的,这里对用户的接口经过精细定义并被高度地控制。

版权条款

Linux是以GNU通用公共版权(GPL)的版本2作为许可的,它来自自由软件基金的GNU项目。GPL允许任何人重发布,甚至是销售,GPL涵盖的产品,只要接收放对源码能存取并且能够行驶同样的权力。另外,任何源自GPL产品的软件产品,如果它是完全的重新发布,必须置于GPL之发行。

Linux驱动开发概述

驱动程序概述

设备驱动程序(Device Driver),简称驱动程序(Driver)。它是一个允许计算机软件(Computer Software)与硬件(Hardware)交互的程序。这种程序建立了一个硬件与硬件,或硬件与软件沟通的界面。CPU经由主板上的总线(Bus)或其他沟通子系统(Subsystem)与硬件形成连接,这样的连接使得硬件设备之间的数据交互成为了可能。

设备驱动程序的作用

设备驱动程序是一种可以使计算机与设备进行通信的特殊程序,可以说相当于硬件接口。操作系统只有通过这个接口,才能控制硬件设备的工作。
设备驱动程序用来将硬件本身的功能告诉操作系统,完成硬件设备电子信号与操作系统及软件的高级编程语言之间的互相翻译。当操作系统需要使用某个硬件时,例如让声卡播放音乐,它会先发送指令到声卡驱动程序。声卡驱动程序接收到后,马上将其翻译成声卡才能读懂的电子信号命令,从而让声卡播放音乐。所以简单的说,驱动程序是提供硬件到操作系统的一个接口,并且协调二者之间的关系。而因为驱动程序有如此重要的作用,所以人们都称“驱动程序是硬件的灵魂”、“硬件的主宰”,同时驱动程序也被形象地称为“硬件和系统之间的桥梁”。

设备驱动的分类

计算机系统的主要硬件由CPU、存储器和外部设备组成。驱动程序的对象一般是存储器和外部设备。随着芯片制造工艺的提高,为了节约成本,通常将很多原属于外部设备的控制器嵌入到CPU内部。所以现在的驱动程序应该支持CPU中的嵌入控制器。Linux将这些设备分为3大类,分别是字符设备、块设备、网络设备。
字符设备
字符设备是指那些能一个字节一个字节读取数据的设备,如LED等、键盘、鼠标等。字符设备一般需要底层驱动实现open()、close()、wirte()、ioctl()等函数。这些函数最终将被文件系统中的相关函数调用。内核为字符设备对应一个文件,如字符设备文件/dev/console。对字符设备的操作可以通过字符设备文件/dev/console来进行。这些字符设备文件与普通文件没有太大的差别,差别之处是字符设别一般不支持寻址,但特殊情况下也有很多字符设备支持寻址的。
块设备
块设备与字符设备相似,一般是像磁盘一样的设备。在块设备中还可以容纳文件系统,并存储大量的信息。在Linux系统中,进行块设备读写时,每次只能传输一个或多个块。Linux可以让应用程序像访问字符设备一样访问块设备,一次只读取一个字节。所以块设备从本质上更像一个字符设备的扩展,块设备能完成更多的工作,例如传输一块数据。
综合来说,块设备比字符设备要求更复杂的数据结构来描述,其内部实现也是不一样的。所以,在Linux内核中,与字符驱动程序相比,块设别驱动程序具有完全不同的API接口。
网络设备
计算机连接到互联网上需要一个网络设备,网络设备主要负责主机之间的数据交换。与字符设备和块设备完全不同,网络设备主要是面向数据包的接收和发送而设计的。网络设备在Linux操作系统中是一种非常特殊的设备,其没有实现类似块设备和字符设备的read()、write()、ioctl()等函数。网络设备实现了一种套接字接口,任何网络数据传输都可以通过套接字来完成。

Linux操作系统与驱动的关系

Linux操作系统与设备驱动之间的关系参考下图:
在这里插入图片描述

用户空间包括应用程序和系统调用两层。应用程序一般依赖于函数库,而函数库是由系统调用来编写的,所以应用程序间接依赖于系统调用。
系统调用是内核空间和用户空间的接口层。通过这个系统调用,应用程序不需要直接访问内核空间的程序,增加了内核的安全性。同时,应用程序也不能访问硬件设备,只能通过系统调用层来访问硬件设备。如果应用程序需要访问硬件设备,那么应用程序先访问系统调用层,由系统调用层去访问内核的设备驱动程序。这样的设计,保证了各个模块的功能独立性,也保证了系统的安全。
系统调用层依赖内核空间的各个模块来实现。在Linux内核中,包含很多实现具体功能的模块。这些模块包括文件系统、网络协议栈、设备驱动、内核调度、内存管理、进程管理等,都属于内核空间。
最底层是硬件层,这一层是实际硬件设备的抽象。设备驱动程序的功能就是驱动这一层硬件。设备驱动程序可以工作在有操作系统的情况下,也可以工作在没有操作系统的情况下。如果只需要实现一些简单的控制设备的操作,那么可以不使用操作系统。如果嵌入式系统完成的功能比较复杂,则往往需要操作系统来帮忙。大多数操作系统具有多任务的特性,所以对于设备驱动程序来说,应该充分考虑并发、阻塞等问题。

Linux驱动开发

Linux操作系统分为用户态和内核态。用户态处理上层的软件工作。内核态用来管理用户态程序 ,完成用户态请求的工作。驱动程序与底层的硬件交互,所以工作在内核态。
Linux操作系统分为两个状态的原因主要是,为应用程序提供一个统一的计算机硬件抽象。工作在用户态的应用程序完全可以不考虑底层的硬件操作,这些操作由内核态程序来完成。这些内核态的程序大部分是设备驱动程序。一个好的操作系统的驱动程序对用户态应用程序应该是透明的,也就说,应用程序可以在不了解硬件工作原理的情况下,很好地操作硬件设备,同时不会使硬件进入非法状态。Linux操作系统很好的做到了这一点。
模块是可以在运行时加入内核的代码,这是Linux一个很好的特性。这个特性使内核可以很容易地扩大或者缩小,一方面扩大内核可以增加内核的功能,另一方面缩小内核可以减小内核的大小。
Linux内核支持很多种模块,驱动程序就是其中最重要的一种,甚至文件系统也可以写成一个模块,然后加入内核中。每个模块由编译好的目标代码组成,可以使用insmod命令将模块加入正在运行的内核,也可以使用rmmod命令将一个未使用的模块从内核中删除。试图删除一个正在使用的模块,将是不允许的。
模块在内核启动时装载称为静态装载,在内核已经运行时装载称为动态装载。模块可以扩充内核所期望的任何功能,但通常用于实现设备驱动程序。一个模块的最基本框架代码如下所示:

#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/init.h>

int __init xxx_init(void)
{
    
    
	/*模块加载时的初始化工作*/
	return 0;
}

void __exit xxx_exit(void)
{
    
    
	/*模块卸载时的销毁工作*/
}
module_init(xxx_init); /*指定模块的初始化函数的宏*/
module_exit(xxx_exit); /*指定模块的卸载函数的宏*/

Linux操作系统有三、四百万行代码,其中驱动代码就有四分之三左右。所以对于驱动开发者来说,学习和编写设备驱动程序都是一个漫长的过程。因此需要掌握以下知识:

  • 良好的C语言基础,并能灵活的运用C语言的结构体、指针、宏等基本语言结构。另外,Linux系统使用的C编译器是GNU C编译器,所以对GNU C标准的C语言也应该有所了解。
  • 驱动开发人员应该有良好的硬件基础。虽然不要求驱动开发人员具有设计电路的原理,但也应该对芯片手册上描述的接口设备有清楚的认识。常用的设备有SRAM、Flash、UART、IIC、USB等。
  • 驱动开发人员应该对Linux内核源码代码有初步的了解。例如一些重要的数据结构和函数等。
  • 驱动开发人员应该有多任务程序设计的能力,同时驱动中也会使用大量的自旋锁、互斥锁、信号等。

编写设备驱动程序的注意事项

在Linux上的程序开发一般分为两种,一种是内核及驱动程序开发,另一种是应用程序开发。这两种开发种类对应Linux的两种状态,分别是内核态和用户态。内核态用来管理用户态的程序,完成用户态请求的工作;用户态处理上层的软件工作。驱动程序及底层的硬件交互,所以工作在内核态。相比于应用程序开发,内核及驱动程序的开发有很大的不同。最重要的差异包括以下几点:

  • 内核及驱动程序开发时不能访问C库,因为C库是使用内核中的系统调用来实现的,而且是在用户空间实现的。
  • 内核及驱动程序开发时必须使用GNU C,因为Linux操作系统从一开始就使用GNU C,虽然也可以使用其他的编译工具,但是需要对以前的代码做大量的修改。
  • 内核支持异步中断、抢占和SMP,因此内核及驱动程序开发时必须时刻注意同步和并发。
  • 内核只有一个很小的定长堆栈
  • 内核及驱动程序开发时缺乏像用户空间那样的内存保护机制
  • 内核及驱动程序开发时浮点数很难使用,应该使用整型数。
  • 内核及驱动程序开发要考虑可移植性,因为对于不同的平台,驱动程序是不兼容的。

GNU C开发驱动程序

GNU C语言最早起源于一个GNU计划,GNU的意思是“GNU is not Unix”。GNU计划开始于1984年,这个计划的目的是开发一个类似UNIX并且软件自由的完整操作系统。这个计划一直在进行,直到Linus开发Linux操作系统时,GNU计划已经开发出来了很多高质量的自由软件,其中最著名的GCC编译器,GCC编译器能够编译GNU C语言。Linus考虑到GNU计划的自由和免费,所以选择了GCC编译器来编写内核代码,之后很多开发者也使用这个编译器,所以直到现在,驱动开发人员也使用GNU C语言来开发驱动程序。

不能使用C库开发驱动程序

与用户空间的应用程序不同,内核不能调用标准的C库函数,主要的原因是在于对内核来说完整的C库太大了。一个编译的内核大小可以是1MB左右,而一个标准的C语言库大小可能操作5MB。这对于存储容量小的嵌入式设备来说,是不实用的。
内核程序中包含的头文件是指内核代码树种的内核头文件,不是指开发应用程序时的外部头文件。在内核种实现的库函数中的打印函数printk(),它时C库函数printf()的内核版本。有着基本相同的用法和功能。

没有内存保护机制

当一个应用程序由于编程错误,试图访问一个非法的内存空间,那么操作系统内核会结束这个进程,并返回错误码。应用程序可以在操作系统内核的帮助下恢复过来,而且应用程序并不会对操作系统内核有太大的影响。但是如果操作系统内核访问了一个非法的内存,那么就有可能破坏内核的代码或者数据。这将导致内核处于未知状态,内核会通过oops错误给用户一些提示,但是这些提示都是不支持、难以分析的。
在内核编程中,不应该访问非法内存,特别是空指针,否则,内核会忽然死掉,没有任何机会给用户提示。对于不好的驱动程序,引起系统崩溃是很常见的事情,所以对于驱动开发人员来说,应该非常重视对内存的正确访问。一个好的建议是,当申请内存后应该对返回的地址进行检测。

小内核栈

用户空间的程序可以从栈上分配大量的空间存放变量,甚至用栈存放巨大的数据结构或者数组都没问题。之所以能这样做是因为应用程序是非常驻内存空间的,它们可以动态地申请核释放所用可用的内存空间。内核要求使用固定常驻的内存空间,因此要求尽量少地占用常驻内存,而尽量多留出内存提供给用户程序用。因此内核栈的长度是固定大小的,不可动态增长的32位机的内核栈是8KB,64为机的内核栈是16KB。
由于内核栈比较小,所以在编写程序时,应该充分考虑小内核栈问题。尽量不要使用递归调用,在应用程序中,递归调用4000多次就有可能溢出,在内核中,递归调用的次数非常少,几乎不能完成程序的功能。另外按使用完内存空间后,应该尽快释放内存,以防止资源泄露,引起内核崩溃。

重视可移植性

对于用户空间程序来说,可移植性一直是一个重要的问题。一般可移植性通过两种方式来实现。一种方式是定义一套可移植性的API,然后对这套API在这两个需要移植的平台上分别实现。应用程序开发人员,只要使用这套可移植性的API,就可以写出可移植的程序。在嵌入式领域,比较常见的API套件是QT。另一种方式是使用类似Java、ActionScript等可移植到很多操作系统上的语言。这些语言一般通过虚拟机执行,所以可以移植到很多平台上。
对于驱动程序来说,可移植性需要注意以下几个问题:

  • 考虑字节顺序,一些设备使用大端字节序,一些设备使用小端字节序。Linux内核提供了大小端字节序转换的函数
#define cpu_to_le16(v16) (v16)
#define cpu_to_le32(v32) (v32)
#define cpu_to_le64(v64) (v64)
#define le16_to_cpu(v16) (v16)
#define le16_to_cpu(v32) (v32)
#define le16_to_cpu(v64) (v64)
  • 即使是同一种设备的驱动程序,如果使用的芯片不同,也应该读写不同的驱动程序,但是应该给用户提供一个统一的编程接口。
  • 尽量使用宏代替设备端口的物理地址,并且可以使用ifdefine宏确定版本等信息
  • 针对不同的处理器,应该使用相关处理器的函数。

小结

随着嵌入式设备的迅猛发展,学习驱动程序开发对个人的进步是非常有帮助的。

总结

欢迎热爱Linux驱动的小伙伴一起学习,一起进步,交流和讨论(_)
qq:602877925

猜你喜欢

转载自blog.csdn.net/m0_56145255/article/details/130814785
今日推荐