您的位置:首页 > 运维架构 > Linux

国嵌视频学习——Linux内核驱动

2012-05-13 10:11 253 查看
字符设备驱动

驱动分类

——字符设备驱动

字符设备:字符设备是一种按字节来访问的设备,字符驱动则负责驱动字符设备,这样的驱动通常实现open,close,read,write系统调用

——网络接口驱动

网络接口:任何网络事务都通过一个接口来进行,一个接口通常是一个硬件设备(eth0),但是它也可以是一个纯粹的软件设备,比如回环接口(lo)。一个网络接口负责发送和接收数据报文。

——块设备驱动

块设备:——在大部分的unix系统,块设备不能按字节处理数据,只能一次传送一个活多个长度是512字节(或一个更大的2次幂的数)的整块数据

——而linux则允许块设备传送任意数目的字节。因此,块和字符设备的区别仅仅是驱动的与内核的接口不同。

字符设备与块设备的区别:块设备是可以进行随机访问的。而字符设备不能。在linux系统中,块设备也可以进行字节访问

驱动程序安装

——模块方式

——直接编译进内核:修改Kconfig、修改Makefile,即可。

将要编译进内核的代码(比如hello.c)cp进内核源码树的/kernel/drivers/char。在char目录下改写Kconfig。然后再make menuconfig的时候便能看见hello world项(Kconfig是用来在menuconfig中增加菜单的,menuconfig配置后的结果保存在.config中);再修改/char目录下的Makefile添加obj-$(CONFIG_HELLO_WORLD) +=hello.o(Makefile根据配置去选择CONFIG_HELLO_WORLD的值)。如此之后便能编译内核了(进入源码树编译)。编译好的内核位于arch/arm/boot/uImage



A:linux用户程序通过设备文件(又名:设备节点)来使用驱动程序操作字符设备和块设备

Q:设备(字符、块)文件在何处?——在/dev/目录下

字符设备驱动程序设计
主次设备号
字符设备通过字符设备文件来存取。字符设备文件由使用ls –l的输出的第一列的“c”标识。如果使用ls –l命令,会看到在设备文件项中有2个数(由一个逗号分隔)这些数字就是设备文件的主次设备编号(举例说明,进入/dev/目录,ls –l)

Q:内核中如何描述设备号?
A:dev_t 其实质为unsigned int 32位整数,其中高12位(4K)为主设备号,低20位(64K)为次设备号

Q:如何从dev_t中分解出主设备号?
A:MAJOR(dev_t dev)

Q:如何从dev_t中分解出此设备号?

A:MINOR(dev_t dev)
设备号

每个设备文件对应有自己的设备号

驱动程序也有自己的设备号

如果两者的设备号对应相同,那么设备文件便和设备驱动建立关联



设备号作用

——主设备号用来标识与设备文件相连的驱动程序。次编号被驱动程序用来辨别操作的是哪个设备

主设备号用来反映设备类型

此设备号用来区分同类型的设备

分配主设备号
Linux内核如何给设备分配主设备号?

——静态申请和动态分配两种方法

静态申请

——方法:1.根据documentation/devices.txt,确定一个没有使用的主设备号

2.使用register_chrdev_region函数注册设备号

——优点:简单

——缺点:一旦驱动被广泛使用,这个随机选定的主设备号可能会导致设备号冲突,而使驱动程序无法注册。

Intregister_chrdev_region(dev_t from, unsigned count, const char *name)

功能——申请使用从from开始的count个设备号(主设备号不变,次设备号增加)

参数——from:希望申请使用的设备号

——count:希望申请使用设备号数目

——name:设备名(体现在/proc/devices)

动态分配(让内核自动来分)

——方法:使用alloc_chrdev_region分配设备号

——有点:简单,易于驱动推广(因为内核知道哪些驱动有没使用)

——缺点:无法在安装驱动前创建设备文件(因为安装前还没有分配到主设备号)

——解决办法:安装驱动后,从/proc/devices中查询设备号

Intalloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const char*name)

——功能:请求内核动态分配count个设备号,且次设备号从baseminor开始

——参数:dev:分配到的设备号

Baseminor:起始设备号

Count:需要分配的设备号数目

Name:设备名(体现在/proc/devices)

注销设备号

不论使用何种方法分配设备号,都应该在不再使用它们时释放这些设备号

Voidunregister_chrdev_region(dev_t from, unsigned count)

——功能:释放从from开始的count个设备号

创建设备文件

2种方法:

——1.使用mknod命令手工创建

Mknod用法: mknod filename typemajor minor

——filename:设备文件名

——type:设备文件类型(b/c)

——major:主设备号

——minor:次设备号

例: mknod serial0 c 100 0

——2.自动创建

重要结构

在linux字符设备驱动程序设计中,有3种非常重要的数据结构:struct file ; struct inode ; struct file_operations

Structfile

代表一个打开的文件。系统中每个打开的文件在内核空间都有一个关联的struct file。它由内核在打开文件时创建,在文件关闭后释放。(如果有3个程序打开同一个文件,那么也有3个struct file)

——重要成员:loff_tf_pos //文件读写位置,loff_t其实是个整形

Struct file_operations *f_op

Structinode

用来记录文件的物理上的信息。因此,它和代表打开文件的file结构是不同的。一个文件可以对应多个file结构,但只有一个inode结构

——重要成员:dev_ti_rdev:设备号

Structfile_operations

一个函数指针的集合(更像是一个对应关系表,把应用程序中对文件的操作转化为驱动程序中相应的函数),定义能在设备上进行的操作。结构中的成员指向驱动中的函数,这些函数实现一个特别的操作,对于不支持的操作保留为NULL

例: mem_fops

Structfile_operations mem_fops={

.owner= THIS_MODULE;

.llseek= mem_seek;

.read= mem_read;

.write= mem_write;

.ioctl= mem_ioctl;

.open= mem_open;

.release= mem_release;

};

内核代码导读

设备注册

在linux2.6内核中,字符设备使用struct cdev来描述

字符设备的注册可分为如下3个步骤:

1.分配cdev

Structcdev的分配可使用cdev_alloc函数来完成

Structcdev *cdev_alloc(void)

——2.初始化cdev

Structcdev的初始化使用cdev_init函数来完成

Voidcdev_init(struct cdev*cdev, const structfile_operations *fops)

——参数:cdev:待初始化的cdev结构

:fops:设备对应的操作函数集

——3.添加cdev

Structcdev的注册使用cdev_add函数来完成

Intcdev_add(struct cdev *p, dev_t dev, unsigned count)

——参数:p:待添加到内核的字符设备结构

dev:设备号

count:添加的设备个数、

设备操作实现

完成了驱动程序的注册,下一步该做什么呢?——实现设备所支持的操作(即是file_operations中的函数指针集)

——int (*open)(struct inode *, struct file *)

在设备文件上的第一个操作,并不要求驱动程序一定要实现这个方法。如果该项为NULL,设备的打开操作永远成功。Open这个函数指针名可以改,比如改为上述mem_fops中的mem_open,但是其参数的类型是固定的,不能更改的。下同。

——void (*release)(struct inode *, struct file *)

当设备文件被关闭时调用这个操作。与open相仿,release也可以没有

——ssize_t (*read)(struct file*, char __user *, size_tloff_t)

从设备中读取数据

——ssize_t(*write)(struct file *, const char __user *,size_t loff_t)

向设备发送数据

——unsigned int (*poll)(struct file *, structpoll_table_struct *)

对应select系统调用

——int (*ioctl)(struct inode *, struct file *, unsignedint , unsigned long)

控制设备

——int (*mmap)(struct file *, struct vm_area_struct *)

将设备映射到进程虚拟地址空间中

——off_t (*llseek)(struct file *, loff_t, int)

修改文件的当前读写位置,并将新位置作为返回值

——参数:要操作的文件,移动的偏移量,移动的起始位置(有三种取值,头、当前位置、尾)

那么如何实现上述函数的呢?

OPEN方法

OPEN方法是驱动程序用来为以后的操作完成初始化准备工作的。在大部分驱动程序中,open完成如下工作:

——初始化设备

——标明次设备号

RELEASE方法

RELEASE方法的作用正好与open相反。这个设备方法有时也称为close,它应该:——关闭设备

读和写

读和写方法都完成类似的工作:从设备中读取数据到用户空间;将数据传递给驱动程序,它们的原型也相当相似:

ssize_t xxx_read(struct file *filp, char__user * buff, size_t count , loff_t * offp);

ssize_t xxx_write(struct file *filp, char__user *buff, size_t count , loff_t *offp);

对于2个方法,filp是文件指针,count是请求传输的数据量。buff参数指向数据缓存。最后,offp指出文件当前的访问位置(buff和count来自用户空间,filp和offp来自内核)

Read和write方法的buff参数是用户空间指针。因此,它不能被内核代码直接引用(而应由内核提供的专门函数来引用),理由如下:用户空间指针在内核空间时可能根本是无效的——没有那个地址的映射

内核提供了专门的函数用于访问用户空间的指针,例如:

——intcopy_from_user(void *to, const void __user *from, int n)

对应写操作,为真则是写失败

——intcopy_to_user(void __user *to , const void *from, int n)

对应读操作,为真则是读失败



设备注销

字符设备的注销使用cdev_del函数来完成

Int cdev_del(struct cdev *p)

——参数:p:要注销的字符设备结构

:字符设备驱动程序:memdev.c

(分析驱动程序不像应用程序那样从头到尾看,应该看入口module_init())
(分析一个字符设备驱动程序,首先分析初始化、分析file operations的各函数(open、read、write、seek))

memdev.h

#ifndef _MEMDEV_H_
#define _MEMDEV_H_

#ifndef MEMDEV_MAJOR
#define MEMDEV_MAJOR 254	//预设的mem的主设备号
#endif

#ifndef MEMDEV_NR_DEVS
#define MEMDEV_NR_DEVS 2		//设备数
#endif

#ifndef MEMDEV_SIZE
#define MEMDEV_SIZE 4096    //4K,申请的用于模拟字符设备的内存是4K

//mem设备描述结构体
Struct mem_dev
{
Char *data;     //由于是用内存模拟字符设备,所以需要记录那块内存的地址,用data保存
Unsigned long size;
};

#endif		/*_MEMDEV_H_*/

memdev.c

//此函数是用内存中的某一段数据来模拟一个字符设备
#include <linux/module.h>
#include <linux/types.h>
#include <linux/fs.h>
#include <linux/errno.h>
#include <linux/mm.h>
#include <linux/sched.h>
#include <linux/init.h>
#include <linux/cdev..h>
#include <asm/io.h>
#include <asm/system.h>
#include <asm/uaccess.h>

#include “memdev..h”

static mem_major = MEMDEV_MAJOR;
module_param(mem_major, int, S_IRUGO);

struct mem_dev *mem_devp ; //设备结构体指针

struct cdev cdev;

/*文件打开函数*/
int mem_open(struct inode *inode, struct file *filp)
{
struct mem_dev *dev;

/*获取次设备号*/
int num = MINOR(inode->i_rdev);

if (num >= MEMDEV_NR_DEVS)
return -ENODEV;
dev = &mem_devp[num];

/*将设备描述结构指针赋值给文件私有数据指针*/
filp->private_data = dev;

return 0;
}

/*文件释放函数*/
int mem_release(struct inode * inode , struct file * filp)
{
return 0;
}

/*读函数*/
static ssize_t mem_read(struct file * filp, char __user *buf, size_t size, loff_t *ppos)
{
unsigned long p = *ppos;
unsigned int count = size;
int ret = 0;
struct mem_dev *dev = filp->private_data; //获得设备结构体指针

/*判断读位置是否有效*/
if (p >= MEMDEV_SIZE)
return 0;
if (count > MEMDEV_SIZE -p)
count = MEMDEV_SIZE -p;

/*读数据到用户空间*/
if(copy_to_user(buf, (void *)(dev->data + p), count))
{
ret = -EFAULT;
}
else
{
*ppos += count;
ret = count;

printk(KERN_INFO, "read %d bytes(s) from %d \n", count, p);
}
return ret;

}
/*写函数*/
static ssize_t mem_write(struct file *filp, const char __user *buf, size_t size, loff_t *ppos)
{
unsigned long p = *ppos;
unsigned int count = size;
int ret = 0;
struct mem_dev *dev = filp->private_data;//获得设备结构体指针

/*分析和获取有效的写长度*/
if (p >= MEMDEV_SIZE)
return 0;
if (count > MEMDEV_SIZE -p)
count = MEMDEV_SIZE -p;

/*从用户空间写入数据*/
if (copy_from_user(dev->data +p , buf, count))
ret = -EFAULT;
else
{
*ppos += count;
ret = count;

printk(KERN_INFO "written %d bytes(s) from %d \n", count, p);
}
return ret;
}

/*seek文件定位函数*/
static loff_t mem_llseek(struct file *filp, loff_t offset, int whence)
{
loff_t newpos;

switch(whence){
case 0: /*SEEK_SET*/
newpos = offset;
break;

case 1: /*SEEK_CUR*/
newpos = filp->f_ops + offset;
break;

case 2: /*SEEK_END*/
newpos = MEMDEV_SIZE - 1 + offset;//此时offset应为负数
break;

default: /*can't happen*/
return -EINVAL;
}
if ((newpos < 0) || (newpos > MEMDEV_SIZE))
return -EINVAL;

filp->f_pos = newpos;
return newpos;
}

/*文件操作结构体*/
static const struct file_operations mem_fops =
{
.owner = THIS_MODULE,
.llseek = mem_llseek,
.read = mem_read,
.write = mem_write,
.open = mem_open,
.release = mem_release,
};

/*设备驱动模块加载函数*/
static int memdev_init(void)
{
int result;
int i;

dev_t devno = MKDEV(mem_major, 0);

/*静态申请设备号*/
if (mem_major)
result = register_chrdev_region(devno, 2, "memdev");
else /*动态分配设备号*/
{
result = alloc_chrdev_region(&devno, 0, 2, "memdev");
mem_major = MAJOR(devno);
}

if (result < 0)
return result;

/*初始化cdev结构*/
cdev_init(&cdev, &mem_fops); //因为cdev是之前定义好了的struct cdev cdev,所以不需要分配,而直接进行初始化
cdev.owner = THIS_MODULE;
cdev.ops = &mem_fops;

/*注册字符设备*/
cdev_add(&cdev, MKDEV(mem_major, 0), MEMDEV_NR_DEVS);

/*为设备描述结构分配内存*/
mem_devp = kmalloc(MEMDEV_NR_DEVS * sizeof(struct mem_dev), GFP_KERNEL);
if(!mem_devp) //申请失败
{
result = -ENOMEM;
goto fail_malloc;
}
memset(mem_devp, 0, sizeof(struct mem_dev));

/*为设备分配内存*/
for (i=0; i<MEMDEV_NR_DEVS; i++)
{
mem_devp[i].size = MEMDEV_SIZE;
mem_devp[i].data = kmalloc(MEMDEV_SIZE, GFP_KERNEL);
memset(mem_devp[i].data, 0, MEMDEV_SIZE);
}

return 0;

fail_malloc:
unregister_chrdev_region(devno, 1);

return result;
}

/*模块卸载函数*/
static void memdev_exit(void)
{
cdev_del(&cdev); //注销设备
kfree(mem_devp); //释放设备结构体内存
unregister_chrdev_region(MKDEV(mem_major, 0), 2); //释放设备号
}

MODULE_AUTHOR("David Jason");
MODULE_LICENCE("GPL");

module_init(memdev_init);
module_exit(memdev_exit);

main.c

//此为应用程序。用来测试驱动程序的读、写、重定位等功能
//当写入的内容和读出的内容一样,那么即是说读写功能成功。
#include <stdio.h>

int main()
{
FILE *fp0 = NULL;
char Buf[4096];

/*初始化Buf*/
strcpy(Buf, "Mem is char dev!");
printf("BUF:%s \n", Buf);

/*打开设备文件*/
fp0 = fopen("/dev/memdev0", "r+");//用fopen打开/dev/memdev0这个设备文件,这个设备文件是由我们自己去创建的:安装驱动程序之后,再去创建这个设备文件。
if (fp0 == NULL)
{
printf("Open Memdev0 Error!\n");
return -1;
}

/*写入设备*/
fwrite(Buf, sizeof(Buf), 1, fp0);

/*重新定位文件位置(思考没有该指令,会有何后果)*/
fseek(fp0, 0, SEEK_SET);//如果没有SEEK_SET,那么会因为每次写入之后seek指针的位置总是在跟随变化的,所以,当读的时候,便不是从文件开头读起!

/*清除Buf*/
strcpy(Buf, "Buf os NULL!\n");
printf("Buf:%s \n ", Buf);

/*读出设备*/
fread(Buf, sizeof(Buf), 1, fp0);

/*检测结果*/
printf("Buf :%s \n", Buf);

return 0;
}


竞争与互斥

调试技术分类

对于驱动程序设计来说,核心问题之一就是如何完成调试。当前常用的驱动调试技术科分为:

——打印调试(printk)

在调试应用程序时,最常用的调试技术是打印,就是在应用程序中合适的点调用printf。当调试内核代码的时候,可以用printk完成类似任务

合理使用printk

在驱动开发时,printk非常有助于调试。但当正式发行驱动程序时,应当去掉这些打印语句。但你有可能很快又发现,你又需要在驱动程序中实现一个新功能(或者修复一个bug),这时你又要用到那些被删除的打印语句。这里介绍一种是用printk的合理方法,可以全局地打开或关闭它们,而不是简单地删除:

#ifdefPDEBUG

#define PLOG(fmt, args…) printk(KERN_DEBUG”scull:”fmt,##args)

#else

#define PLOG(fmt,args..) //do nothing

#endif

Makefile作如下修改:

——DEBUG=y

ifeq ($(DEBUG),y)

DEBFLAGS=-O2 –g –DPDEBUG //D的作用是相当于#define

else

DEBFLAGS=-O2

endif

CFLAGS +=$(DEBFLAGS)

——调试器调试(kgdb)

——查询调试(/proc文件系统)

并发与竞态

——并发:多个执行单元同时被执行

——竞态:并发的执行单元对共享资源(硬件资源和软件上的全局变量等)的访问导致的竞争状态

例:

If(copy_from_user(&(dev->data[pos]), buf, count))

Ret = -EFAULT;

Goto out;

假设有2个进程试图同时向一个设备的相同位置写入数据,就会造成数据混乱(对应于多核情况)

处理并发的常用技术是加锁或者互斥,即确保在任何时间只有一个执行单元可以操作共享资源。在Linux内核中主要通过semaphore(信号量)机制和spin_lock(自旋锁)机制实现

信号量

Linux内核的信号量在概念和原理上与用户态的信号量是一样的,但是它不能在内核之外使用,它是一种睡眠锁。如果有一个任务想要获得已经被占用的信号量时,信号量会将这个进程放入一个等待队列,然后让其睡眠。当持有信号量的进程将信号释放后,处于等待队列中的任务将被唤醒,并让其获得信号量

——信号量在创建时需要设置一个初始值,表示允许有几个任务同时访问该信号量保护的共享资源,初始值为1就变成互斥锁(Mutex),即同时只能有一个任务可以访问信号量保护的共享资源。

——当任务访问完被信号量保护的共享资源后,必须释放信号量,释放信号量通过把信号量的值加1实现,如果释放后信号量的值为非正数,表明有任务等待当前信号量,因此要唤醒等待该信号量的任务。

信号量的实现也是与体系结构相关的,定义在<asm/semaphore.h>中,struct semaphore类型用来表示信号量。

1.定义信号量 struct semaphore sem;

2.初始化信号量

voidsema_init(struct semaphore *sem, int vall)该函数用户初始化设置信号量的初值,它设置信号量sem的值为val

voidinit_MUTEX(struct semaphore * sem)该函数用于初始化一个互斥锁,即它把信号量sem的值设置为1。

voidinit_MUTEX_LOCKED(struct semaphore *sem)该函数用于初始化一个互斥锁,但它把信号量sem的值设置为0,即一开始就处在已锁状态。

定义及初始化的工作可由如下宏一步完成:

DECLARE_MUTEX(name):定义一个信号量name,并初始化它的值为1

DECLARE_MUTEX_LOCKED(name):定义一个信号量name,但它把它的初始值设置为0,即锁在创建时就处在已锁状态。

3.获取信号量

voiddown(struct semaphore * sem)获取信号量sem,可能会导致进程睡眠,因此不能在中断上下文使用该函数。该函数将把sem的值减1,如果信号量sem 的值非负,就直接返回,否则调用者将被挂起,直到别的任务释放该信号量才能继续运行(此时处于TASK_UNINTERRUPTIBLE的状态)。

——intdown_interruptible(struct semaphore * sem):获取信号量sem。如果信号量不可用,进程将被设置为TASK_INTERRUPTIBLE(可被信号和中断唤醒)类型的睡眠状态。该函数由返回值来区分是正常返回还是被信号中断返回,如果返回0,表示获得信号量正常返回,如果被信号打断,返回-EINTR

——down_killable(structsemaphore * sem):获取信号量sem。如果信号量不可用,进程将被置为TASK_KILLABLE类型的睡眠状态

注:down()函数(linux 2.4)现已不建议继续使用。建议使用down_killable()或down_interruptible()函数

4.释放信号量

voidup(struct semaphore * sem):该函数释放信号量sem,即把sem 的值加1,如果sem的值为非正数,表明有任务等待该信号量,因此唤醒这些等待者。

自旋锁

自旋锁最多只能被一个可执行单元持有。自旋锁不会引起调用者睡眠,如果一个执行线程试图获得一个已经被持有的自旋锁,那么线程就会一直进行忙循环(一直占有CPU),一直等待下去,在那里看是否该自旋锁的保持者已经释放了锁,“自旋”就是这个意思。

——spin_lock_init(x):该宏用于初始化自旋锁x,自旋锁在使用前必须先初始化

——spin_lock(lock):获取自旋锁lock,如果成功,立即获得锁,并马上返回,否则它将一直自旋在那里,直到该自旋锁的保持者释放。

——spin_trylock(lock):试图获取自旋锁lock,如果能立即获得锁,并返回真,否则立即返回假。它不会一直等待被释放

——spin_unlock(lock):释放自旋锁lock,它与spin_trylock或spin_lock配对使用

信号量PK自旋锁

——信号量可能允许有多个持有者,而自旋锁在任何时候只能允许一个持有者。当然也有信号量叫互斥信号量(只能一个持有者),允许有多个持有者的信号量叫计数信号量

——信号量适合于保持时间较长的情况;而自旋锁适合于保持时间非常短的情况,在实际应用中自旋锁控制的代码只有几行,而持有自旋锁的时间也一般不会超过两次上下文切换的时间,因为线程一旦要进行切换,就至少花费切出切入两次,自旋锁的占用时间如果远远长于两次上下文切换,我们就应该选择信号量。

实验1: 1.在mini2440平台编写实现了读、写,定位功能的字符设备驱动程序

2.编写应用程序,测试驱动

实验2: 基于实验1设计的驱动程序,加入竞争控制
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息