ICode9

精准搜索请尝试: 精确搜索
首页 > 系统相关> 文章详细

Linux系统驱动程序开发实例

2021-02-21 20:00:26  阅读:166  来源: 互联网

标签:驱动程序 mem pos dev char 实例 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函数。

标签:驱动程序,mem,pos,dev,char,实例,Linux,设备
来源: https://blog.csdn.net/weixin_37926734/article/details/113875255

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有