Linux系统驱动程序开发实例

Linux系统驱动程序开发实例

  • Linux系统的驱动程序开发主要包括:内核模块开发、块(字符)设备驱动程序开发、网络设备驱动程序开发三大块。其中内核模块与驱动程序的区别主要体现在以下几点:
  • (1)模块运行在内核空间,而应用程序则运行在用户空间;
  • (2)模块只能使用内核导出的函数,而不能使用其他函数库(包括glibc库);
  • (3)模块必须考虑到并发,所以代码必须是可重载的。

一、编写内核模块

1.1 编写内核模块原则

  • 在Linux系统中,有两种设备驱动开发方法:(1)直接修改系统核心源代码,把设备驱动程序加入内核;(2)将设备驱动程序作为可加载模块,由系统管理员动态地加载、卸载。设备驱动程序通常是以内核模块的形式呈现的,因此学会编写内核模块是设备驱动程序开发的第一步,下面列出内核模块编写的整体步骤:
  • (1)在Linux系统中,模块可以用C语言编写,采用gcc编译为目标文件(.o),需要注意的是这里能对文件进行链接,因此需要在gcc命令前加上-c参数。
  • (2)为了说明是针对内核进行模块开发的,因此也需要在gcc命令行加上-D_KERNEL_-DMODULE参数。
  • (3)由于在不链接的情况下,gcc只允许一个输入文件,因此一个模块的所有内容都应该放在一个文件中实现。比如,编译好的模块.o文件放到/lib/modules/2.0.30/misc下,其中2.0.30表示内核版本号,然后采用depmod-a将模块变成可加载模块。另外,模块可以用insmod命令加载,用rmmod命令卸载,还可以用lsmod命令查看所有加载的模块状态信息。
  • (4)编写模块程序时,必须提供两个函数int init_module(void)与void cleanup_module(void),int init_module(void)函数为insmod加载模块时候自动调用,完成设备驱动程序的初始化,该函数返回0表示初始化成功,返回负数表示失败;void cleanup_module(void)函数在模块卸载时被调用,完成设备驱动程序的清除。
  • (5)在成功地向系统注册了设备驱动程序后,即register_chrdev()成功后,可以采用mknod命令将设备映射为一个特别文件,当其他程序使用该设备时,只需要对该特别文件进行操作即可。

1.2 编写内核模块实例

  • 在一小节编写内核模块步骤的基础上,该小节讲述一个内核模块组成与编写实例,代码如下:
/*(1)程序说明:
* 这是一个可加载内核模块,加载时显示“Hello,Everyone!”,
* 卸载时显示“Goodbye”。
* 注意:由于内核模块的编写本来就是为应用程序提供系统调用的代码,
* 因此,编写内核模块不能用编写应用程序时的系统调用或库函数。
* 内核有专门的库函数,比如<linux/kernel.h>、<linux/fs.h>、
* <linux/sche.h>等。
* 该程序中的printk()的功能类似于printf()。
* /usr/src/linux是实际的内核源代码目录的一个符号链接,
* 如果没有的话,可以自行创建以供后续使用。
* 编译该内核模块可以用gcc-c-I /usr/src/linux/include hello.c,
* 如果编写征程则生成hello.o目标文件,并用insmod hello.o加载,
* 就会在文本中断看到“Hello,Everyone”输出。
* 卸载该模块可以用rmmod hello命令。
*/
/*(2)小技巧:
* 在用户目录中的.bashrc中加入一行:
* alias mkmod='gcc-c-I /usr/src/linux/include',重新登录shell,
* 以后就可以直接用mkmod hello.c命令来直接编译内核模块了。
*/

/*例行公事*/
#ifndef __KERNEL__
#define __KERNEL__
#endif
#ifndef MODULE
#define MODULE
#endif

#include<linux/config.h>
#include<linux/module.h>

MODULE_LICENSE("GPL");
#ifdef CONFIG_SMP
#define __SMP__
#endif

/*内核模块编写*/
#include<linux/kernel.h>
static int init_module()
{
	printk("Hello,Everyone!\n");
	return 0;
}
static void cleanup_module()
{
	printk("Goodbye!\n")
}

二、编写块(字符)设备驱动程序

  • 本小节介绍一个块(字符)设备驱动程序开发实例,其源码及注释如下:
/*(1)程序说明:
* 该文件是一个内核模块。
* 该模块功能是创建一个字符设备,该设备是一块4096字节的共享内存。
* 内核分配的主设备号会在加载模块时显示。
*/

/*例行公事*/
#ifndef __KERNEL__
#define __KERNEL__
#endif
#ifndef MODULE
#define MODULE
#endif

#include<linux/config.h>
#include<linux/module.h>
MODULE_LICENSE("GPL");
#ifdef CONFIG_SMP
#define __SMP__
#endif

/*内核模块编写*/
#include<asm/uaccess.h> /*该内核库文件包括copy_to_user(),
						copy_from_user()函数*/
#include<linux/fs.h>	/*包括struct file_operations,
						register_chrdev()等函数*/
#include<linux/kernel.h>/*printk()在该内核库文件中*/
#include<linux/sched.h>	/*与任务调度相关*/
#include<linux/types.h>	/* u8,u16,u32...*/

/*文件被操作时的回调功能包括:(1)open回调、(2)release回调、
* (3)read回调、(4)write回调、(5)lseek回调。*/
static int mem_char_open(struct inode *inode,
				struct file *filp);
static int mem_char_release(struct inode *inode,
				struct file *filp);
static ssize_t mem_char_read(struct file *filp,char *buf,
				size_t count,loff_t *f_pos);
static ssize_t mem_char_write(struct file *filp,
				const char *buf,size_t count,loff_t *f_pos);
static loff_t mem_char_lseek(struct file *file,loff_t offset,
				int orig);

/*申请主设备号时候用的结构,在/linux/fs.h中定义*/
struct file_operations mem_char_fops={
	open:		mem_char_open,
	release:	mem_char_release,
	read:		mem_char_read,
	write:		mem_char_write,
	lseek:		mem_char_lseek,
};

static int mem_char_major;	/*用来保存申请到的主设备号*/
static u8 mem_char_body [4096]="mem_char_body\n";	/*设备*/

static int init_module()
{
	printk("Hello, This' A Simple Device File!\n");
	/*申请字符设备的主设备号*/
	mem_char_major=register_chrdev(0,"A Simple Device File",
						&mem_char_fops);
	if(mem_char_major<0)
		return mem_char_major;	/*申请失败就直接返回错误编号*/
	/*如果申请成功,显示主设备号*/
	printk("The major is: %d\n", mem_char_major);
	return 0;	/*返回0,表示模块正常初始化*/
}

/*设备的注销,注销以后设备就不存在了*/
static void cleanup_module()
{
	unregister_chrdev(mem_char_major,"A Simple Device File");
	printk("A Simple Device has been removed, Goodbye!\n");
}

/*
* 编译上述模块并加载,如果执行正常则会显示主设备号。
* 下面进行测试,假设模块申请到的主设备号为254,
* 运行mknod abc c 254 0,就建立了设备文件abc。
* 这样就可以把abc文件当作一个4096的字节内存块用以下指令来测试:
* cat abc、cp abc image、cp image abc,
* 或者写几个应用程序用它来进行通信。
* 需要注意的是:
* (1)printk()的显示只有在非图形模式的中断下才能看到;
* (2)加载过的模块不用以后最好卸载。
*/

/*(1)open回调*/
static int mem_char_open(struct inode *inode,
				struct file *filp)
{
	printk("^_^:open %s\n",current->comm);
	return 0;
/*说明:
* 应用程序的运行环境由内核提供,内核的运行环境由硬件提供。
* 该回调中的current是一个指向当前进程的指针,目前没必要了解细节。
* 在这里,当前进程正在打开该设备,
* 返回0表示打开成功,并且内核给它一个文件描述符。
* 这里的comm是当前进程在shell下的command字符串。
*/
}

/*(2)release回调*/
static int mem_char_release(struct inode *inode,
				struct file *filp)
{
	printk("^_^:close\n");
	return 0;
}

/*(3)read()回调*/
static ssize_t mem_char_read(struct file *filp,char *buf,
				size_t count,loff_t *f_pos)
{
	loff_t pos;
	pos=*f_pos;	/*文件的读写位置*/
	if((pos==4096) || (count>4096)) return 0;
	/*判断是否已经到设备尾或者写的长度超过设备大小*/
	pos += count;
	if(pos>4096){
		count -= (pos - 4096);
		pos = 4096;
	}
	if(copy_to_user(buf, mem_char_body+*f_pos,count))
		return -EFAULT;	/*把数据读到应用程序空间*/
	*f_pos = pos;	/*改变文件的读写位置*/
	return count;	/*返回读到的字节数*/
}

/*(4)write()回调,与read()一一对应*/
static ssize_t mem_char_write(struct file *filp,
				const char *buf,size_t count,loff_t *f_pos)
{
	loff_t pos;
	pos = *f_pos;
	if((pos==4096) || (count>4096)) return 0;
	pos += count;
	if(pos > 4096){
		count -= (pos - 4096);
		pos = 4096;
	}
	if(copy_from_user(mem_char_body+*f_pos,buf,count))
		return -EFAULT;
	*f_pos = pos;
	return count;
}

/*(5)lseek()回调*/
static loff_t mem_char_lseek(struct file *file,
				loff_t offset, int orig)
{
	loff_t pos;
	pos = file->f_pos;
	switch(orig){
		case 0:
			pos = offset;
			break;
		case 1:
			pos += offset;
			break;
		case 2:
			pos = 4096+offset;
			break;
		default:
			return -EINVAL;
	}
	if((pos>4096) || (pos<0)){
		printk("^_^:lseek error %d\n",pos);
		return -EINVAL;
	}
	return file->f_pos = pos;
}

三、编写网络设备驱动程序

  • Linux网络驱动程序遵循通用的接口,设计时采用的是面向对象方法。一个设备就是一个对象(device结构),它内部具有自己的数据与方法。每个设备的方法被调用时的第一个参数就是设备对象本身,这样这个方法就可以存取自身数据,类似于面向对象中的this引用。

3.1 网络设备驱动设计方法

  • 整体来说,网络设备最基本的方法主要包括初始化、发送与接收数据。初始化程序完成硬件的初始化、device中变量初始化与系统资源的申请;发送程序指在驱动程序的上层协议层有数据要发送时自动调用的,通常驱动程序中不对发送数据进行缓存,而是直接通过硬件将数据发送出去;接收数据通常是通过硬件中断完成,在中断处理程序中,将硬件帧信息存放到一个skbuff结构中,然后条用netif_rx()传递给上层处理的。
    1、初始化(initialize)
  • 在驱动程序载入系统时会调用初始化程序,初始化程序做以下几方面工作:
  • (1)检测设备:在初始化程序里可以根据硬件的特征检查硬件是否存在,然后决定是否启动这个驱动程序;
  • (2)配置和初始化硬件:在初始化程序里可以完成对硬件资源的配置;
  • (3)申请资源:配置完硬件资源后,就可以向系统申请这些硬件了;
  • (4)初始化device结构中的变量。
  • 完成初始化后,硬件就可以正常工作了。
    2、打开(open)
  • (1)在网络设备驱动程序中,open方法是在网络设备被激活时被调用的(即设备状态由down变为up);
  • (2)open方法的另外一种应用场景:当程序作为一个模块被载入时,为了防止模块卸载时设备处于打开状态,在open方法中调用MOD_INC_USE_COUNT宏。
    3、关闭(close)
  • (1)close方法可以释放某些资源以减小系统负荷,close是在设备状态由up变为down时调用的;
  • (2)另外,当驱动程序作为模块载入时,close可以调用MOD_DEC_USE_COUNT宏,以减少设备被引用次数,使得设备程序可以被卸载。
    4、发送(hard_start_xmit)
  • 所有网络设备驱动程序都必须有发送方法,在系统调用驱动程序的xmit时,发送的数据被存放在sk_buff结构中。如果发送成功,hard_start_xmit方法释放sk_buff,并返回0;如果hard_start_xmit发送不成功,则不释放sk_buff。另外,传送下来的sk_buff中的数据已经包含硬件需要帧头,所以在发送时不需要再填充帧头,数据可以直接提交给硬件发送。sk_buff是被锁住的(locked),以确保其他程序不会存取它。
    5、接收(reception)
  • 驱动程序并不存在一个接收方法,收到的数据是由驱动程序来通知系统的。设备收到数据后通常会产生一个中断,在中断中申请一块sk_buff(skb),从硬件读出数据存放到申请号的缓冲区。接下来填充sk_buff中的一些信息:
  • (1)skb->dev=dev:判断收到的帧的协议类型;
  • (2)skb->protocol:多协议支持;
  • (3)skb->mac.raw:把指针指向硬件数,然后丢弃硬件帧头(skb_pull);
  • (4)skb->pkt_type:标明第二层(链路层)数据类型。
  • 其中,第二层链路层可以是以下类型:
  • (1)PACKET_BROADCAST:链路层广播;
  • (2)PACKET_MULTICAST:链路层组播;
  • (3)PACKET_SELF:发送给自己的帧;
  • (4)PACKET_OTHERHOST:发送给别人的帧头(侦听模式)。
  • 最后,调用netif_rx()将数据传送给协议层,在netif_rx()中数据被存放到处理队列并返回,调用netif_rx()以后,驱动程序就不能在存取数据缓冲区skb了。
    6、硬件帧头(hard_header)
  • 硬件都会在上层数据发送之前加上自己的硬件帧头,比如以太网(Ethernet)具有14字节的帧头。硬件帧头是加载上层ip、ipx等数据包之前的。驱动程序提供一个hard_header方法,协议层(ip、ipx、arp等)在发送数据之前会调用这段程序。
  • 在协议层调用hard_header时,传送的参数包括:(1)数据的sk_buff、(2)device指针、(3)protocol、(4)目的地址(daddr)、(5)源地址(saddr)、(6)数据长度(len)。
  • 其中,(1)源地址saddr是为NULL表示使用缺省地址(default);(2)目的地址(daddr)为NULL表示使用协议层不知道硬件目的地址;(3)如果hard_header完全填好了硬件帧头,则返回添加的字节数。
    7、地址解析(xarp)
  • 有些网络有硬件地址,且在发送硬件帧时需要知道硬件地址,此时就需要上层协议(ip、ipx)与硬件地址对应,这个对应过程就是地址解析。需要注意的是需要做arp的设备在发送之前会调用驱动程序的rebuild_header方法,调用的主要参数包括指向硬件帧头的指针、协议层地址。对rebuild_header的调用在net/core/dev/c的do_dev_queue_xmit()中。如果驱动程序能够解析硬件地址,则返回1;如果不能则返回0。
    8、参数设置与统计数据
  • 在驱动程序中还提供一些方法实现对设备参数设置与信息读取,通常只有超级用户(root)权限才能对设备参数进行设置。
  • (1)dev->set_mac_address():
  • 当用户调用ioctl类型为SIOCSIFHWADDR时需要设置该设备的mac地址,其他情况通常没有必要进行该设置。
  • (2)dev->set_config():
  • 当用户调用ioctl类型为SIOCSIFMAP时,系统会调用驱动程序set_config方法,此时用户会传递一个ifmap结构,包含需要的I/O、中断等参数。
  • (3)dev->do_ioctl():
  • 如果用户调用ioctl类型在SIOCDEVPRIVATE与SIOCDEVPRIVATE+15之间,系统就会调用驱动程序的do_ioctl方法,该方法通常用于设置设备的专用数据。
  • (4)信息读取
  • 信息读取也是通过ioctl调用实现的,驱动程序提供了dev->get_stats方法,返回一个enet_statistics结构,包含发送接收的统计信息。ioctl的处理在net/core/dev.c的dev_ioctl()与dev_ifsioc()中。

3.2 网络设备驱动设计实例

  • 下面以ne2000兼容网卡为例,来具体介绍基于模块的网络驱动程序设计实例,可以参考/linux/drivers/net/ne.c与linux/drivers/net/8390.c。
    1、模块加载与卸载
  • (1)ne2000网卡的模块加载功能由init_module()函数完成,具体过程及解释如下:
int init_module(void)
{
	int this_dev, found = 0;
	//循环检测ne2000类型的网络设备接口
	for(this_dev = 0; this_dev < MAX_NE_CARDS; this_dev++)
	{
		//获得网络接口对应的net-device结构指针
		struct net_device *dev = &dev_ne[this_dev];
		dev->irq=irq[this_dev];	//初始化该接口的中断请求信号
		dev->mem_end=bad[this_dev];	//初始化接收缓冲区的终点位置
		dev->base_addr=io[this_dev];//初始化网络接口的I/O基地址
		dev->init=ne_probe; //初始化init为ne_probe
		/*调用register_netdevice()向系统等级网络接口,在这该
		  函数中将给网络接口分配在系统中的唯一名称,并将该网络
		  添加到系统管理的链表dev_base中进行管理*/
		if (register_netdev(dev) == 0){
			found++;
			continue;
		}
		...	//省略
	}
	return 0;
}
  • (2)ne2000网卡的模块卸载功能由cleanup_module()函数完成,具体过程及解释如下:
void cleanup_module(void)
{
	int this_dev;
	//遍历整个dev_ne数组
	for (this_dev = 0; this_dev < MAX_NE_CARDS; this_dev++)
	{
		//获得net_device结构指针
		struct net_device *dev = &dev_ne[this_dev];
		if (dev-> != NULL){
			void *priv = dev->priv;
			struct pci_dev *idev = 
						(struct pci_dev *)ei_status.priv;
			//调用函数指针idev->deactive,将已经激活的网卡关闭
			if (idev)
				idev -> deactivate(idev);
			free_irq(dev->irq, dev);
			//调用函数release_region()释放网卡占用的I/O地址空间
			release_region(dev->base_addr, NE_IO_EXTENT);
			//调用unregister_netdev()注销net_device()结构
			unregister_netdev(dev);
			kfree(priv);	//释放priv空间
		}
	}
}

2、网络接口初始化

  • 网络接口初始化是由ne_probe()函数实现的,在init_module()函数中用ne_probe()函数用来初始化init函数指针。ne_probe()函数主要对网卡进行检测,并且初始化系统中的昂罗设备信息,用于后面的网络数据的发送与接收,具体过程及解释如下所示:
int __init ne_probe(struct net_device *dev)
{
	unsigned int base_addr = dev->base_addr;
	/*初始化dev->owner成员,因为使用模型类型驱动,会将dev->owner
	  指向对象modules结构指针。*/
	SET_MODULE_OWNER(dev);
	/*检测dev->base_addr是否合法,如果合法则执行ne_probel()函数;
	  如果不合法则需要自动检测。*/
	if(base_addr > 0x1ff)
		return ne_probel(dev, base_addr);
	else if(base_addr != 0)
		return -ENXIO;
	//若有ISAPnP设备,需要调用ne_probe_isapnp()检测该类型网卡
	if (isapnp_present() && (ne_probe_isapnp(dev) == 0))
		return 0;
	...//省略
	return -ENODEV;
}
  • 其中,两个函数ne_probe_isapnp()与ne_probel()的区别在于检测中断号上;PCI方式只需要指定I/O基地址就可以自动获取irq,是由BIOS自动分配的,而ISA方式需要获得空闲的中断资源才可以分配。
    3、网络接口设备的打开与关闭
  • 网络接口设备的打开就是激活网络接口,使得它能够接收来自网络的数据并传递到网络协议栈上,也可以将数据发送到网络上;网络设备的关闭就是停止操作。
  • 在ne2000网络驱动程序中网络设备的打开由dev_open()与ne_open()实现;而设备的关闭则由dev_close()与ne_close()实现。它们对应的调用底层函数ei_open()与ei_close()函数实现网络接口设备的打开与关闭。
    4、数据包的接收与发送
  • 在驱动程序层次上数据的发送与接收都是通过底层对硬件的读/写来完成的。当网络上数据到来时,将触发硬件中断,然后根据注册的中断向量表确定处理函数,进入中断向量处理程序,最终将数据传输到上层协议进行处理。
  • (1)ne2000网卡数据接收是通过ne_probe()函数的中断处理函数ei_interrupt()来完成的,进入ei_interrupt()之后再通过ei_receive()从接收缓冲区获得数据,并组合为sk_buff结构,最后通过netif_rx()函数将接收到的数据存放到系统的接收队列中。
  • ei_interrupt()函数的原型如下所示:
void ei_interrupt(int irq,void *dev_id,struct pt_regs *regs)

其中,irq为中断号,dev_id为产生中断的网络接口设备对应的结构指针,regs为当前的今存其内容。

  • (2)对于ne2000网卡的数据发送是由dev_start_xmit函数指针处理的,对应的函数为ei_start_xmit()函数,由它来完成数据包的发送。再函数ethdev_init()中把net_device结构的hard_start_xmit指针初始化为ei_start_xmit函数。
上一篇:第6章:移动操作指令的实现


下一篇:一起谈.NET技术,使用LINQ to SQL更新数据库(中):几种解决方案