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

Linux驱动学习——简单字符设备

2014-08-07 15:47 429 查看
Linux系统划分为用户空间和内核空间。

用户空间有应用程序和应用程序运行时使用的一些库,内核空间包含七大子系统。现代CPU通常实现了不同的工作模式,以ARM为例,有7种工作模式:用户模式、管理模式、快速中断、外部中断、数据访问中止、未定义指令中止、系统模式。其中USR模式和SVC模式,这两种本身硬件上就定义了自己的访问权限,后者的权限最高,能访问所有硬件资源。Linux系统的软件形式空间的划分要依赖处理器的这种工作模式的划分。由于这种划分用户空间的软件不能随意访问硬件资源、内核空间的地址(代码和数据),如果用户要访问内核空间必须通过系统调用。这种实现最终起到安全的保护。

 

内核七大子系统

系统调用接口:为用户提供一套标准的系统调用函数来访问Linux内核。

进程管理:用户进程的创建,停止,通信和调度。

内存管理:用于多个进程安全的共享内存区域。

网络协议栈:实现网络数据的传输。

设备驱动:用于控制操作硬件。

虚拟文件系统:隐藏各文件系统的具体细节,为文件提供统一的操作接口。

平台相关:与具体处理器架构相关的实现。

 

Linux系统调用:

Linux的系统调用大致可以分为:进程控制、文件系统控制、系统控制、内存管理、socket控制、用户管理、进程间通信等几大类。Linux实现系统调用利用了软中断(x86中的 int $0x80 汇编指令以及ARM中的swi 指令)。通常系统调用靠C库支持。

系统调用实现原理,以ARM和open为例

1.     应用程序调用open,首先调用C 库的open实现。

2.     C库的open将open对应的系统调用号(__NR_open)填充到寄存器(R7)。

3.     open的实现中调用软中断指令(swi),出发一个软中断异常。CPU跳转到异常向量表的软中断入口执行,至此进程由用户空间转向内核空间。

4.     异常向量表有内核在初始化是建立,入口在Linux内核空间的地址为0XFFFF0000。

5.     进程跳转到软中断入口vector_swi后根据之前寄存器中保存的系统调用号,以此为索引在系统调用表中找到对应的内核实现函数sys_open,并执行内核函数。

6.     执行sys_open后转入ret_from_sys_call例程,从系统调用中返回。

 

系统调用会进入内核执行,但仍处于进程上下文中,因此可以访问进程的许多信息。系统调用是用户空间和内核交互的唯一手段,但是完成交互功能并非需要添加新的系统调用不可。可以使用以下几种方式与内核交互:编写字符驱动程序、使用proc文件系统、使用虚拟文件系统。新的进程可以使用相同的系统调用,必须保证系统调用是重入的。

 

驱动模块的编译:静态编译、模块化编译

模块的编译通常使用makefile

模块的makefile范例

ifneq($(KERNELRELEASE), )    //变量不等于空则执行下面语句,不过第一次

obj-m :=hello.o             //执行makefile,这个变量一般为空。

else                      //所以执行else的语句。

KDIR :=/lib/modules/2.6.18-53.e15/build   //给出内核源代码的路径

all:               //M表示内核模块的代码的路径,(此处当前目录)

       make –C $(KDIR) M=$(PWD) modules  //使用-C指定目录下的makelfile

clean:                              

       rm –f *.ko  *.o  *.mod.o  *.mod.c *.symvers

endif

 

模块化编译:

方法1:多个文件编译成一个模块

obj-m+=test.o

test.o–objs= hello.o file1.o file2.o …

**一个模块只能有一个module_init,module_exit,不能有多个入口和出口。

方法2:分别编译对应的模块

       obj-m+= file1.o file2.o  或者

       obj-m+=file1.o

       obj-m+=file2.o

**这种编译方法对入口出口没有限定。

 

对模块的操作

insmod:加载模块命令,module_init的入口函数被内核调用。

rmmod:下载模块命令,module_exit的出口函数被内核调用。

lsmod:通过读取/proc/modules虚拟文件,列出当前内核加载的模块

modinfo;查看模块的信息

modprobe:根据modules.dep文件检查模块的依赖关系,装载和卸载模块以及

                       所依赖的模块。(-r 参数卸载模块)

mocprobe会默认到目录/lib/modules/…找modules.dep文件,所以编译好的模块需安装到此目录下(模块的Makefile中添加语句):

       make–C /kerneldir/kernel M=/moduledir/mymodule  modules_install INSTALL_MOD_PATH=/….installpath/

编译后把安装路径下的内容拷贝至根文件系统的/lib目录

 

内核编程的注意点:

不允许使用C库;

必须使用GNU C(标C的扩展)

没有内存保护机制

难以执行浮点运算

进程在内核空间执行时,它的内核栈大小只有8K,编程时注意局部变量的大小。内核编程时申请的内存一般都是从堆中分配。

由于内核支持异步中断、抢占和SMP,须时刻注意同步和并发。

 

内核模块的添加信息:

MODULE_LICENSE(“GPL”) 许可证必须添加

MODULE_AUTHOR(author)

MODULE_VERSION(description)

MODUEL_DEVICE_TABLE(table_info) 模块所支持的设备

MODULE_ALIAS(alter_name)模块别名

 

内核模块参数:

module_param(var,type,perm):如果权限不为0,模块加载后在/sys/module/ module<name>/parameters/会生成跟变量同名的文件,这个变量的值只能在加载模块是修改。由于/sys的内容存在于内存中,如果大量的模块参数声明并指定权限,势必会占用内存,所以没有特殊需求一般权限写0。

module_param_array(var,type,num,perm)数组,num记录有效的数组元素个数。

 

内核符号导出:

EXPORT_SYMBOL(函数名或变量)

EXPORT_SYSMBOL_GPL(….)导出的符号只能给遵循GPL协议的模块使用。

 

printk 打印消息:

<linux/kernel.h> 中定义了8种日志级别用于消息打印。0~7数字越小级别越高。

为了节省CPU资源,提高系统性能,不是所有的内核打印信息都要输出。

cat  /proc/sys/kernel/printk查看当前打印级别(可以通过向这个文件写入数字来修改打印级别。或者在内核启动之前通过uboot 设置参数 setnv bootargs指定loglevel的值)

 

字符设备:以字节流形式顺序访问,例如字符终端(/dev/console)和串口(/dev/ttys0),音频,LCD屏,摄像头和各种传感器等

 

设备号(32位,dev_t类型)由主设备号(高12位)和次设备号(低20位)构成。

主设备号标识对应的驱动程序,次设备号又内核使用确定具体的设备个体。

它们之间的转换可通过如下宏:

       MAJOR(dev_tdev);

       MINOR(dev_t  dev);

       MKDEV(intmajor, int  minor);

 

设备文件的创建

1.       手动创建

mknod    /dev/mydev(设备文件名)   c(字符设备type)   250(主设备号)   0(次)

2.       自动创建

 

分配设备号:

方法1静态申请:int register_chrdev_region(dev_t first, int coun
4000
t, char *name)

(需提前知道尚未被使用的设备号,)

方法2动态:int alloc_chrdev_region(dev_t dev,int firstminor,int count,char*name)

分配成功后可通过cat  /proc/devices 查看,驱动程序应该使用动态分配设备号。

 

释放设备号:

       void unregister_chrdev_region(dev_tfirst, int count);

 

 

字符设备相关数据结构

字符设备结构struct cdev:内核中用该结构来表示一个字符设备。

struct cdev {

       structfile_operatons *ops;

       dev_t  dev;

       int count;

       …

 }

文件操作结构struct file_operations:函数指针的集合,指向驱动中的函数,这些函数定义了能够对设备进行的操作。

struct file_operations {

       struct module*owner;

       ssize_t  (*read) (…….)

       int (*open) (…..)

       ….

}

文件结构struct file :用来描述设备文件打开后状态信息,系统中每个打开的文件在内核空间都有一个关联的struct file。
 inode结构structinode:用于记录文件的物理信息,一个文件可以有多个file结构,但只有一个inode结构

四个结构体的关系

 


字符设备的分配:

       structcdev *my_cdev = cdev_alloc();

       my_cdev->ops= &my_fops;    //或者直接定义静态全局变量

初始化:

       voidcdev_init(struct cdev* cdev,struct file_operations *fpos)

注册:

       intcdev_add(struct cdev*dev,dev_t num,unsigned int count)

(字符设备的注册即将my_cdev添加的内核的cdev散列表)

移除:

       voidcdev_del(struct cdev *dev)

 

应用程序访问字符设备驱动

1.     首先安装一个设备对应的驱动

2.     安装过程如:

1)       驱动程序首先分配一个硬件操作方法struct file_operations

2)       然后分配以一个字符设备对象struct cdev

3)       初始化分配的结构体cdev_init

4)       将字符设备注册到内核中cdev_add,以设备号为索引添加到内核的cdev散列表中。

3.     创建对应的设备节点,内核此时会创建这个设备节点对应的inode ,并把设备文件的设备号保存在inode.i_rdev中。根据inode.i_rdev设备号信息,在内核的cdev散列表中找到对应的cdev,然后将这个字符设备结构的指针赋值给inode->i_cdev(用于缓存),以后其他的应用程序打开文件时,直接从inode->i_cdev取出对应的驱动。

4.     应用程序就可以通过open调用打开设备文件

5.     open调用C库的open实现,C库的open保存系统调用号到寄存器R7中。

6.     open实现最后调用swi指令,触发一个软中断异常。CPU跳转至内核初始化时定义好的的一个异常向量表的入口(0xFFFF0000),软中断的入口是vector_swi,取出open对应的系统调用号,然后在内核定义的系统调用表里找到对应的函数sys_open,执行这个sys_open函数。

7.     sys_open函数创建file对象,从驱动中定义的cdev结构中取出fops,将文件操作结构赋值给file的f_op。

8.     判断file->f_op中有没有open实现。有则调用底层驱动的open实现,没有用户空间永远返回为真。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: