http://blog.csdn.net/jamestaosh/article/details/4348385
OK6410一键烧写的时候,SD卡上放入的文件系统应该是FORLINX_6410_touch_nand2g.yaffs2更名为rootfs.yaffs2,按照官方指导书的做法放入FORLINX_6410_touch.cramfs更名为cramfs会存在问题。
-------------从Linux内核配置和编译开始记笔记-------------
内核编译中,make menuconfig 带"*"的会编译进Image,带“M”的会编译成.o文件,但是不会被编译进Image,没有任何选中标志的不会被编译。
内核代码中/arch/arm/config目录下有很多参考配置,将你要的配置文件(比如s3c6400_defconfig)拷贝到内核代码根目录下并且改名为.config,然后再make menuconfig就可以在你要的这个参考配置下修改。
编译内核:make zImage ,make bzImage 区别:在X86平台,zImage只能用于小于512K的内核
如需要获取详细编译信息,可使用:make zImage V=1 , make bzImage V=1
编译好的内核位于:/arch/ <cpu>/boot/目录下
编译内核模块:make modules
安装内核模块:make modules_install 该命令会将编译好的内核模块从内核源代码目录copy到/lib/modules/<内核版本>目录下
制作init ramdisk:mkinitrd initrd-$version $version 例:mkinitrd initrd-2.6.29 2.6.29
$version可能通过查询/lib/modules/下的目录得到<内核版本>,这个命令会把 /lib/modules/<内核版本>/ 该目录做成一个文件,放在当前目录(内核源代码目录)下。
在X86平台下安装编译好的内核 :
1.cp arch/X86/boot/bzImage /boot/vmlinuz-$version //拷贝bzImage
2.cp $initrd /boot/ //拷贝ramdisk
3.修改 /etc/grub.conf 或者 /etc/lilo.conf:
![图片](https://i-blog.csdnimg.cn/blog_migrate/2b4c38c38103e7845a48af0d9e0ca946.jpeg)
$version为所以编译的内核版本号
内核模块的特点:
1.模块本身并不被编译进内核文件(zImage或bzImage)
2.可以根据要求,在内核运行期间动态的安装或卸载
简单的内核模块:
![图片](https://i-blog.csdnimg.cn/blog_migrate/28f19196d37616202e8bca1da660642f.jpeg)
makefile:
![图片](https://i-blog.csdnimg.cn/blog_migrate/6ab372f19c6b800261783c82ddec0eeb.jpeg)
使用KDIR内核代码里面的Makefile编译我们的内核模块{$(KDIR)},编译的内核模块放在当前目录下{M=$(PWD)},编译的是内核模块(modules)
有二个文件main.c ,add.c时,makefile的编写(有更多个时同样的方式):
![图片](https://i-blog.csdnimg.cn/blog_migrate/25ed108b0f0cfde68a6f6388ff3a26f7.jpeg)
安装与卸载模块:
加载:insmod (insmod hello.ko)
卸载:rmmod (rmmod hello.ko)
查看:lsmod
加载:modprobe (modprobe hello)
insmod 和 modprobe 的目的一样都是加载模块,区别在于modprobe会根据/lib/modules/<$version>/modules.dep来查看要加载的模块,看要加载的模块是否还依赖于其它模块,如果是modprobe会首先找到这些模块,把它们先加载进内核
模块声明:MODULE_LICENSE("GPL"),没有这样的声明,加载该模块时内核会抱怨
作者声明:MODULE_AUTHOR() ,可选
模块描述:MODULE_DESCRIPTION() ,可选
模块别名:MODULE_ALIAS(),可选
![图片](https://i-blog.csdnimg.cn/blog_migrate/d9eb2f1b138d6404f519a692ba96da1a.jpeg)
模块参数:
module_param(age,int,S_IRUGO);定义模块参数
![图片](https://i-blog.csdnimg.cn/blog_migrate/55bc68a0fc0b355276e8d5f69acf42fc.jpeg)
使用模块参数:在安装模块的时候insmod hello.ko age=12
模块函数导出,这样其它模块可以访问本模块的函数:
EXPORT_SYMBOL(add_integar),这样其它模块extern int add_integar(int a,int b);后就可以直接调用下面这个模块中的add_integar函数
EXPORT_SYMBOL_GPL(functionname),这个只能用于包含GPL许可证的模块
![图片](https://i-blog.csdnimg.cn/blog_migrate/cad41c96d131f42f0560ee6ff83950fe.jpeg)
加载模块时,模块的版本必须和所要加载进的内核的版本一致。否则加载模块时会报错(disagrees about version of symbol struct_module)。可以使用modprobe --force-modversion 强制加载模块。可以用"uname -r"命令查看当前内核版本
printk在内核中使用,允许根据严重程度,通过附加不同的优先级,来对消息分类 。如:printk(KERN_EMERG "hello world!\n");KERN_EMERG就是优先级,其它的还有KERN_WARNING等
printf在应用程序中使用
Bootloader,GRUB也是一个Bootloader。 引导程序的主要任务是将内核从硬盘上读取到内存中,然后跳转到内核的入口点去执行。
反编译工具:arm-linux-objdump ,arm-linux-objdump -D -S hello
ELF文件查看工具:arm-linux-readelf , arm-linux-readelf -a hello 查看hello使用的动态库(只看动态库:将-a换成-d 选项)等
U-Boot命令:
printenv,打印环境变量
setentv ,设置或修改环境变量:setenv test 123 删除变量:setenv test
saveenv ,保存环境变量,修改了环境变量后如果不保存,重启后相应的环境变量不会被保存。
文件下载:tftp ,tftp c0800000 uImage 将uImage下载到开发板的内存c0800000开始处
显示内存区的内容:md [.b, .w, .l] address
修改内存: mm [.b, .w, .l] address
显示nand的信息:nand info
擦除nand:nand erase start Length ,擦除从start开始的Length长度的区域,如nand erase 10000 70000
向nand写入数据的命令:nand write [内存地址] [flash地址] length,例:nand write.i c0800000 100000 600000
执行程序: go addr [args...] 或者 bootm [addr [args...] ],[]表示该参数可有可无 bootm要求程序要有固定的头
显示开发板信息: bdinfo
设置自动启动:
setenv bootdelay 5
setenv bootcmd tftp c0800000 uImage \; bootm c0800000
saveenv
嵌入式Linux内核制作:
X86:make menuconfig arm:make menuconfig ARCH=arm
X86:make bzImage arm:make uImage ARCH=arm CROSS_COMPILE=arm-linux-
根文件系统创建:
1.创建根文件系统的目录:
mkdir rootfs
cd rootfs
mkdir bin dev etc lib proc sbin sys usr mnt tmp var
mkdir usr/bin usr/lib usr/sbin lib/modules
2.创建设备文件
cd dev/
mknod -m 666 console c 5 1
mknod -m 666 null c 1 3
3.安装etc
tar zxvf etc.tar.gz -c /***/rootfs/
4.编译内核模块
进入Linux内核目录(Linux2.6.36)
make modules ARCH=arm CROSS_COMPILE=arm-linux-
5.安装内核模块
make modules_install ARCH=arm INSTALL_MOD_PATH=/***/rootfs
Busybox:
必须设置的配置:
Busybox setting--->Build options---> 选中Build Busybox as a static library ,Cross compiler prefix设置编译为交叉工具链:arm-linux-
Busybox setting--->Installation options---> 选中Don't use /usr ,Busybox installation prefix设置相应目录:/***/rootfs
制作根文件系统的时候可以用它来在/bin目录下生成所需要的命令。这些命令全被链接到busybox,执行相应的命令都会调用busybox,Busybox根据链接名称区分是什么命令。
JFFS2:主要用于Nor型Flash,基于MTD驱动层,特点是:可读写,支持数据压缩的日志型文件系统,并提供了崩溃/掉电安全保护等。缺点主要是当文件系统已满或接近满时,因为垃圾收集的关系使JFSS2的运行速度大大放慢。
yaffs/yaffs2:是专为Nand型Flash而设计的一种文件系统。与JFFS2相比它减少了一些功能(如不支持数据压缩),所以速度更快,挂载时间很短,对内存的占用很小,另外它是跨平台的文件系统。
Cramfs:只读的压缩文件系统。基于MDT驱动程序,Cramfs文件系统以压缩方式存储, 在运行时解压缩,所有的应用程序要求被拷贝到RAM中运行,其速度快,效率高,其只读的特点有利于保护文件系统,提高了系统的可靠性。
Ramdisk:它将一部分固定大小的内存当作设备来使用,它并非一个实际的文件系统。将一些经常访问,而又无需修改的文件通过ramdisk放在内存中,可以明显提高系统的性能。
initramfs:是一种基于内存的文件系统。 它的作用不需要创建内存块设备。增加文件到initramfs会自动分配更大的内存。
NFS: 基于网络的文件系统。
配置内核使用NFS文件系统:
1.make menuconfig ARCH=arm
在其中的File systems--->Network File Systems--->选中Root file system on NFS 该项表示允许内核加载NFS
2.然后Boot options--->Default kernel command string 添加下面这行字符串设置启用NFS :
root=/dev/nfs nfsroot=10.0.0.2;/***/rootfs ip=10.0.0.3 rw console=ttySAC0 mem=256 //nfsroot=宿主机IP,ip=开发板IP,rw:读写
3.配置宿主机:vi /etc/exports 加入下面这行:/***/rootfs 10.0.0.*(rw,sync,no_root_squash)
4.重新编译内核:make uImage ARCH=arm CROSS_COMPILE=arm-linux-
5.最后将重新编译好的内核放入开发板运行。
配置内核使用initramfs文件系统:
1.make menuconfig ARCH=arm
General setup--->选中Initial RAM file system and RAM disk (initramfs/initrd)support ;然后指明Initramfs source file(s) (NEW) 指定到文件系统的目录:/***/rootfs/
2.创建一个到busybox的软链接init(这是Initramfs要求的) : ln -s /***/rootfs/bin/busybox init
3. 编译内核,运行内核。
逻辑地址,线性地址,虚拟地址,物理地址 :http://blog.csdn.net/do2jiang/article/details/4512417
在Linux中逻辑地址和线性地址是一样的。Linux所有段的基地址都为0.
Linux系统采用的是虚拟内存管理技术,使得每个进程都有独立的进程地址空间,该空间大小是3G。每个进程都有自己的页目录,页表。
Linux将4G的虚拟地址空间划分为二个部分:用户空间,内核空间。用户空间从0~0xbfffffff,内核空间从3G-4G 。
在应用程序中用malloc分配内存,内核中用kmalloc分配内存。kmalloc分配内存时要指定一个flag,这个flag取值可以是GFP_KERNEL,GFP_ATOMIC,_GFP_DMA,_GFP_HIGHMEM。
需要分配大块的内存时可以按页分配:
get_zeroed_page(unsigned int flag),返回指向新页面的指针,并将页面清零
__get_free_page(unsigned int flag),返回指向新页面的指针,但是不会将页面清零
__get_free_pages(unsigned int flag,unsigned int order)分配若干连续的页面,返回指向新页面的指针,也不会将页面清零
void free_page(unsigned long addr);
void free_pages(unsigned long addr,unsigned long order);
如果释放的和之前分配数目不等的页面,会导致系统错误。
内核空间划分:
1.内核空间的直接映射区(896M):从3G开始最大896M的线性地址空间,该区域的线性地址和物理地址之间存在线性转换关系。线性地址=3G+物理地址。如物理地址0x10000映射到线性地址空间就是3G+0x10000
2.动态映射区(120M):该区域的地址由内核函数vmalloc进行分配 。其特点是线性空间连续,但对应的物理空间不一定连续。但是全在896M~1016M这120M空间内。
3.永久内存映射区(1016M-1020M:4M):对于896M以上的高端内存可以用该区域来访问,访问方法:1.使用alloc_page(_GFP_HIGHMEM)分配高端内存页。2.使用kmap函数将分配到的高端内存映射到该区域。
4.固定映射区(最高端4M):它和4G顶端只有4K的隔离带,固定映射区中每个地址都服务于特定的用途。如ACPI_BASE等。
Linux内核链表:
![图片](https://i-blog.csdnimg.cn/blog_migrate/a81ed3adc291df1a6e0263a854f3ad59.jpeg)
内核链表的使用:
初始化链表头:INIT_LIST_HEAD(list_head * head);
插入节点:
list_add(struct list_head* new ,struct list_head* head)
list_add_tail(struct list_head* new ,struct list_head* head)
list_del(struct list_head* entry)
提取数据结构:
list_entry(ptr,type,member) ptr为type结构里的成员member的指针。 已知数据结构中的节点指针ptr,找出数据结构:list_entry(ptr,struct autofs,list) http://blog.csdn.net/jiatingqiang/article/details/6437496
遍历链表:
list_for_each(struct list_head* pos,struct list_head* head)
例:
![图片](https://i-blog.csdnimg.cn/blog_migrate/f3607de38d2f9cdfb4dae3a5a9081aa7.jpeg)
list_for_each原型:
#define list_for_each(pos, head) \
for (pos = (head)->next, prefetch(pos->next); pos != (head); pos = pos->next, prefetch(pos->next))
它实际上是一个 for 循环利用传入的pos 作为循环变量从表头 head开始逐项向后(next方向)移动 pos ,直至又回到 head 。prefetch() 可以不考虑,用于预取以提高遍历速度。 注意:此宏必要把list_head放在数据结构第一项成员,至此,它的地址也就是结构变量的地址
list_entry是一个宏,展开后是 #define list_entry(ptr, type, member) ((type *)((char *)(ptr)-(unsigned long)(&((type *)0)->member))) 这个宏根据ptr的地址和ptr在结构体type内的名称:member算出结构体type的地址:http://blog.csdn.net/jiatingqiang/article/details/6437496
内核时间中断: 利用jiffies延时jit_delay秒:unsigned long j = jiffies + jit_delay*HZ; while(jiffies<j){/*do nothing*/}
操作定时器:初始化定时器队列结构 void init_timer(struct timer_list* timer);
启动定时器:void add_timer(struct timer_list* timer);
在定时器超时前删除:int del_timer(struct timer_list* timer); 定时器超时后,系统会自动将它删除,
示例:
struct timer_list timer;
init_timer(&timer);
timer.data = 5;//函数参数
timer.expires = jiffies+(20*HZ);
timer.function = timer_function;//函数指针
add_timer(timer);
进程,线程,内核线程:进程是资源分配的最小单位,线程是调度/执行的最小单位。
![图片](https://i-blog.csdnimg.cn/blog_migrate/fb01a910e07ea723a2123c61a3be7e93.jpeg)
进程状态:TASK_RUNNING,TASK_KILLABLE等。 进程退出时的状态:EXIT_ZOMBIE(僵死进程),EXIT_DEAD(僵死撤销状态)
task_struct内的成员:struct mm_struct*mm;进程用户空间描述指针。unsigned int policy;进程调度策略。int prio;优先级。int static_prio;静态优先级。struct sched_tr_entity rt; tr->time_slice;时间片,进程的缺省时间片跟进程的静态优先级相关,进程的优先级数值越大,分配到的时间片越小。
由于内核不断发展task_struct包含的信息越来越多,task_struct所占空间和系统空间堆栈总共只有8K,这样系统空间堆栈会被挤占。所以2.6内核开始task_struct被thread_info代替,thread_info大小固定,它里面有一个指针指向task_struct.
调试策略:
SCHED_NORMAL (SCHED_OTHER)普通的分时进程,SCHED_FIFO先入先出的实时进程,SCHED_RR时间片轮转的实时进程,SCHED_BATCH批处理进程,SCHED_IDLE只有在系统空闲时才被调度执行的进程。这五种调度策略被分为CFS(公平)调度类(SCHED_NORMAL ,SCHED_BATCH,SCHED_IDLE)和实时调度类(SCHED_FIFO,SCHED_RR)。
调度方式有二种:
1.主动式调度,由内核调用schedule()函数。
2. 被动式(抢占)调度。用户抢占(2.4,2.6)内核抢占(2.6)。用户抢占发生在1.从系统调用返回用户空间2.从中断处理程序返回用户空间。
在不支持内核抢占的系统中,内核空间的进程可以一直运行,直到时间片耗尽或主动放弃,这样一些非常紧急的进程或线程将长时间得不到运行。在支持内核抢占的系统中,更高优先级的进程或线程可以抢占内核空间运行的低优先级的进程/线程。某些特例下是不允许内核抢占的,例如正在执行中断,正在执行schedule()函数等,内核内维护了一个preempt_count计数,内核进入不允许内核抢占的状态时,会将该计数加1,退出时减1并进入可抢占的判断和调度。内核抢占可能发生在:1.中断处理完成时,返回内核空间之前。2.当内核代码再一次具有可抢占性的时候(解锁.使能软中断等)。
TIF_NEED_RESCHED,调度标志,当某个进程耗尽它的时间片时会设置这个标志,当一个优先级更高的进程进入可执行状态时,也会设置这个标志。
添加新的系统调用:
1.添加新的内核函数
2.更新头文件unistd.h
3.针对这个函数更新系统调用表calls.S
调用时syscall(370,1,2); 370是系统调用号,对应unistd.h中的调用号,1,2是系统调用函数的参数。
proc文件系统是一种在用户态检查内核状态的机制。文件的内容都是动态创建的,并不存在于磁盘上。
自己创建proc文件:
struct proc_dir_entry* create_proc_entry(const char* name,mode_t mode,proc_dir_entry* parent);
功能:创建proc文件,参数:name:要创建的文件名,mode:要创建的文件的属性,parent:这个文件的父目录。
struct proc_dir_entry* proc_mkdir(const char* name,proc_dir_entry* parent);
功能:创建proc目录,参数:name:要创建的目录名,parent:这个目录的父目录。
void remove_proc_entry(const char* name,proc_dir_entry* parent);
功能:删除proc目录或文件,参数:name:要删除的目录或文件名,parent:这个目录或文件的父目录。
创建出来的proc(proc_dir_entry)实体要添加其中的read_proc和write_proc成员值,这二个是函数指针:
int read_func(char* buffer,char** stat,off_t off,int count,int *peof,void* data);
参数:buffer:把要返回给用户的信息写在buffer里,最大为PAGE_SIZE,stat:一般不使用,off:偏移量,count:用户要取的字节数,peof:读取到文件尾时要把*peof置1,data:一般不使用。
int write_func(struct file* file,const char* buffer,unsigned long count,void* data);
参数:file:该proc文件对应的file结构,一般忽略。buffer:待写的数据所在的位置。count:待写数据的大小。data:一般不使用。
创建Proc文件流程:
1.调用create_proc_entry创建一个struct proc_dir_entry;
2.对创建的struct proc_dir_entry 进行赋值。read_proc,write_proc,mode,owner,size等。
ARM里面函数前面加__init(void __init func()) 表示这个函数是初始函数,这类函数被放在代码段中的初始化代码段中,运行一次后内存就会被回收。
Linux驱动学习:Linux驱动程序设计模式(40%),内核相关知识(30%),硬件相关知识(30%)
把驱动直接编译进内核:
1.修改内核目录下/drivers/char/Kconfig(这里是字符设备配置文件,同理有块设置和网络设备的),在Kconfig加入
config HELLO
bool "Hello driver"
2.然后make menuconfig ARCH=arm 在字符设备中选中 "Hello driver" ,这时就会在内核根目录下的config中加入一行“CONFIG_HELLO=y”(这个名称由HELLO加一个CONFIG_得到)。
3.然后修改内核根目录下的Makefile,加入obj-$(CONFIG_HELLO) += hello.o (内核规定加入obj-y的是要编译进内核的.o文件)
![图片](https://i-blog.csdnimg.cn/blog_migrate/56a5d92b34cda508463762063645ca6c.jpeg)
主设备号用来关联“设备文件”和相应的“驱动程序”。主设备号区分出了设备类型,次设备号用来区分同类型设备。
dev_t用来保存设备号(高12位为主设备号,低20位为次设备号),拿出主设备号:MAJOR(dev_t dev),拿出次设备号:MINOR(dev_t dev)
分配主设备号:
1.静态申请,根据Documentation/devices.txt确定一个没有使用的主设备号,然后使用register_chrdev_region函数注册设备号。
int register_chrdev_region(dev_t from,unsigned count,const char* name)
功能:申请从from开始的count个设备号(主设备号不变,次设备号增加),from:希望申请的设备号,count:希望申请使用设备号数目,name:设备名(体现在/proc/devices)
2.动态分配,使用alloc_chrdev_region,安装驱动后可以在/proc/devices中查询设备号
int alloc_chrdev_region(dev_t dev,unsigned baseminor,unsigned count,const char* name);
功能:请求内核分配count个设备号,且次设备号从baseminor开始,dev:分配到的设备号,baseminor:起始次设备号,count:希望申请使用设备号数目,name:设备名(体现在/proc/devices)
释放设备号:
不论用何种方法申请到设备号,都应该在不使用它们时释放这些设备号。
void unregister_chardev_region(dev_t from,unsigned count);
功能:释放从from开始的count个设备号。
创建设备文件:
1.使用mknod命令手工创建。mknod filename type major minor (filename设备文件名,type设备文件类型,major,minor主次设备号)
2.自动创建。
struct file:一个打开的文件存在于内存。成员:loff_t f_pos:文件读写位置,struct file_operation *f_op:一个函数指针集合,定义能在设备上执行的操作。 struct file_operation的成员:.owner = THIS_MODULE:如果有某个模块要用这个文件(file)的时候,它会将该模块THIS_MODULE引用计数加1,这样这个模块就不能被卸载,不用这个文件的时候引用计数减1。这个操作由内核自己完成,赋值的时候直接赋值THIS_MODULE就可以了。
struct inode:物理存储介质上的一个文件,一个inode结构可以对应多个file结构。
在Linux2.6中,字符设备使用struct cdev描述。字符设备的注册可以分三步:
1.分配cdev: struct cdev* cdev_alloc(void)
2.初始化cdev: void cdev_init(struct cdev* cdev,const struct file_operation* fops) cdev:待初始化的cdev fops:设备对应的操作函数集
3.添加cdev: int cdev_add(struct cdev* p,dev_t dev,unsigned count) count:添加的设备个数
字符设备的注销:
int cdev_del(struct cdev *p)
file_operation的成员:
int(*open)(struct inode*,struct file*);在设备文件上的第一个操作,并不一定要实现这个方法。
int(*release)(struct inode*,struct file*);当设备文件被关闭时被调用,并不一定要实现这个方法。
ssize_t (*read)(struct file*,char __user*,size_t,loff_t*);从设备中读取数据。
ssize_t (*write)(struct file*,const char __user*,size_t,loff_t*);从设备中读取数据。
unsigned int(*poll)(struct file*,struct poll_table-struct*);对应select系统调用。
int(*ioctl)(struct inode*,struct file*,unsigned int,unsigned long);控制设备。
int(*mmap)(struct file*,struct vm_area_struct*); 将设备映射到进程虚拟地址空间中。
off_t (*lseek)(struct file*,loff_t,int);修改文件当前的读写位置,并将新位置作为返回值。
open方法:初始化设备,标明次设备号。release刚好与之相反。
read,write方法:file*是文件指针 ,size_t:是请求传输的数据量,char __user*:指向数据缓存, loff_t*:指出文件当前的访问位置。
char __user*:是用户空间指针,内核不能直接使用,因为用户空间指针在内核空间时可能根本是无效的--没有那个地址的映射。
内核访问用户空间的指针:
int copy_from_user(void* to,const void __user *from,int n);
int copy_to_user(void __user *to, const void *from,int n);
/proc/devices/中的设备是通过insmod加载到内核的,它可产生一个major供mknod作为参数。
/dev/*.* 是通过mknod加上去的,格式:mknod device1 c/b major minor 如:mknod dr1 c 254 0,用户通过此设备名来访问你的驱动。
虚拟文件系统调用过程:
![图片](https://i-blog.csdnimg.cn/blog_migrate/deb37d48ad85373829f09396411c257d.jpeg)
自己在驱动做个内核调试开关:
![图片](https://i-blog.csdnimg.cn/blog_migrate/b1d318fde57b9bd2f091151b102162fa.jpeg)
然后在Makefile中加入-D PDEBUG
![图片](https://i-blog.csdnimg.cn/blog_migrate/1910875e1c608f06981cab5c7f76e4ff.jpeg)
信号量:
1. 定义信号量:struct semaphore sem;
2.初始化信号量:void sema_init(struct semaphore sem.int val);设置信号量的初值,设置信号量sem的值为val
3.初始化一个互斥锁:void init_MUTEX(struct semaphore sem);把信号量sem的值设置为1
void init_MUTEX_LOCKED(struct semaphore sem); 把信号量sem的值设置为0,即一开始就处于已锁状态。
4.获取信息量:void down(struct semaphore sem);可能会导致进程睡眠,因此不能在中断上下文中使用该函数,该函数将把sem的值减1 ,如果信号量sem的值非负,就直接返回,否则调用者将被挂起,直到别的任务释放该信号量才能继续运行。
int down_interruptible(struct semaphore sem);与down(sem)类型,但是如果信号不可用,进程将被置为可中断类型(T ASK_INTERRUPTIBLE)的睡眠状态。该函数由返回值来区分是正常返回还是被信号中断返回,如果返回0,表示获得信号量正常返回, 如果被信号打断,返回-EINTR
down_killable(struct semaphore sem);与down(sem)类型,但是如果信号不可用,进程将被置为TASK_KILLABLE类型的睡眠状态。
down函数现在已经不建议使用,建议使用 down_in terruptible 或 down_k illable
5.释放信号量:void up(struct semaphore sem);把sem的值为1,如果sem的值为非正数,表明有任务在等待该信号量,因此唤醒这些等待者。
自旋锁:
1.初始化自旋锁x:spin_lock_init(x);自旋锁在使用前必须先初始化。
2.获取自旋锁:spin_lock(x);如果成功,立即获得锁,并马上返回。否则它将一直自旋在那里,直到该自旋锁的保持者释放。
spin_trylock(lock);如果成功,立即获得锁并返回真,否则立即返回假。它不会一直等待被释放。
3.释放自旋锁:spin_unlock(lock);它与spin_lock或spin_unlock配对使用。
![图片](https://i-blog.csdnimg.cn/blog_migrate/9ecc51886f0c8e9faabf199706f0dbf4.jpeg)
ioctl系统调用:
用户空间的ioctl:int ioctl(int fd,unsigned long cmd,...);
内核空间的ioctl:int (*ioctl)(struct inode *inode,struct file *filp,unsigned int cmd ,unsigned long arg);从2.6.36版本开始已经废弃。
long (*unlocked_ioctl)(struct file *filp,unsigned int cmd,unsigned long arg);cmd参数从用户空间传下来,如果cmd命令不涉及数据传输,则第三个参数arg无任何意义。
如何实现unlocked_ioctl:
1.定义命令:ioctl命令被划分为(类型(幻数),序号,传递方向,参数的大小)
内核中提供的定义命令的宏:
_IO(type,nr):没有参数的命令,
_IOR(type,nr,datatype);从驱动中读数据,如_IOR("m",1,int):"m"为幻数
_IOW(type,nr,datatype):写数据到驱动,
_IORW(type,nr,datatype):双向传送,type和nr成员作为参数被传递。
上面这四个宏的type是幻数,nr是命令的序号,datatype是参数类型即要读或写的数据类型。
2.实现命令:
(1).返回值:当传递过来的命令是一个设备不运行的命令时,通常返回-EINVAL(非法参数)
(2).参数使用:参数arg如果是一个整数可以直接使用,如果是指针,在使用前必须要进行正确的检查。不需要检测:copy_from_user,copy_to_user,put_user,get_user(这些内核已经自己做了检测,所以用户自己不用去检测); 需要检测:__put_user,__get_user。
用于检测某一用户空间有效性的函数:int access_ok(int type,const void *addr,unsigned long size); type:VERIFY_READ或者VERIFY_WRITE,addr:要检测的地址,size:要检测的长度。成功返回1,失败返回0,如果该函数返回失败,则ioctl函数应该返回-EFAULT
(3). 命令操作:命令所要实现的功能 。
等待队列:
1.定义等待队列:wait_queue_head_t my_queue;
2.初始化等待队列:init_wait_queue_head(&my_queue);
上面二步合并为一步完成:.定义并初始化等待队列:DECLARE_WAIT_QUEUE_HEAD(my_queue);
3.有条件睡眠:
wait_event(queue,condition),当condition为真时,立即返回,否则让调用wait_event的进程进入TASK_UNINTERRUPTIBLE模式的睡眠,并挂在queue参数所指定的等待队列上。
wait_event_interruptible(queue,condition):当condition为真时,立即返回,否则让调用wiat_interruptible的进程进入TASK_INTERRUPTIBLE模式的睡眠,并挂在queue参数所指定的等待队列上。
wait_event_killable(queue,condition):当condition为真时,立即返回,否则让调用wiat_interruptible的进程进入TASK_KILLABLE模式的睡眠,并挂在queue参数所指定的等待队列上。
4.唤醒进程:
wake_up(wait_queue_t *q):从等待队列q中唤醒状态为 TASK_UNINTERRUPTIBLE, TASK_INTERRUPTIBLE,TASK_KILLABLE的所有进程。
wake_up_interruptible(wait_queue_t *q): 从等待队列q中唤醒状态为 TASK_INTERRUPTIBLE的所有进程。
阻塞型字符设备驱动程序:
当一个设备无法立刻满足用户的读写请求时,驱动程序应该阻塞 (缺省的)发起请求的进程,使它进入睡眠,直到请求可以得到满足。阻塞方式是文件读写操作的默认方式,但是应用程序员可以在打开文件时人为的设置读写操作模式为非阻塞方式(O_NONBLOCK);如果设置了O_NONBLOCK标志,当设置无法立刻满足用户的读写请求时,系统只是简单地返回-EAGAIN,而不会阻塞进程。
![图片](https://i-blog.csdnimg.cn/blog_migrate/982e1fb745651b0c81dc1c73c75926bf.jpeg)
select系统调用:
用户空间中的select对应内核空间中的poll ; select系统调用用于多路监控,当没有一个文件满足要求时,select将阻塞调用进程。
int select(int maxfd,fd_set *readfds, fd_set *writefds, fe_set *exceptfds, const timeval *timeout);
参数:maxfd:文件描述符的范围,readfds:被读监控的文件描述集,writefds:被写监控的文件描述集,exceptfds:被异常监控的文件描述符集。timeout:定时器。
timeout取不同的值,该调用有不同的表现:timeout值为0,不管是否有文件满足要求,都立刻返回,无文件满足要求返回0,有文件满足要求返回一个正值。timeout为null,select将阻塞进程,直到某个文件满足要求。timeout值为正整数,就是等待的最长时间,即select在timeout时间内阻塞进程。
select调用返回时,返回值的情况:1.正常情况下返回满足要求的文件描述符个数。2.经历了timeout时间后仍无文件满足要求返回0,3.如果select被某个信号中断,它返回-1并设error为EINTR。4.如果出错返回-1并设置相应的errno;
select系统调用使用方法:
1.将要监控的文件添加到文件描述符集。
文件描述符集操作的宏:
void FD_SET(int fd,fd_set *fdset):将文件描述符fd添加到文件描述集fdset中。
void FD_CLR(int fd,fd_set *fdset);将文件描述符fd从文件描述集fdset中删除。
void FD_ZERO(fd_set *fdset);清空文件描述符集fdset;
void FD_ISSET(int fd,fd_set *fdset);在调用select后使用FD_ISSET检测文件描述符集fdset中的文件fd发生了变化。
2.调用select开始监控。
3.判断文件是否发生变化。
poll方法:
unsigned int (*poll) (struct file *filp,poll_table *wait);
该方法负责完成:
1.使用poll_wait将等待队列添加到poll_table中。
2.返回描述设备是否可读或可写的掩码。掩码:POLLIN:设备可读,POLLRDNORM:数据可读,POLLOUT:设备可写,POLLWRNORM:数据可写。设备可读通常返回(POLLIN|POLLRDNORM)。设备可写通常返回(POLLOUT|POLLWRNORM)
![图片](https://i-blog.csdnimg.cn/blog_migrate/d736730ab0f273fd131de15c753809d7.jpeg)
/*自动创建设备文件*/
void *mmap(void *addr,sizt_t len, int prot, int flags, int fd, off_t offset);
功能:
负责把文件内容映射到进程的虚拟地址空间。通过直接对这段内存的读取和修改,来实现对文件的读取和修改,而不需要使用read,write等操作,该函数不会影响原有文件的长度,写入的数据超出文件长度时,写入的数据会被截断。
参数:
addr:映射的起始地址,一般写NULL,这样系统自动指定地址。
len:映射到内存的文件长度,
prot:映射区的保护方式(PROT_EXEC可执行,PROT_READ可读,PROT_WRITE可写);
flags:映射区的特性(MAP_SHARED:写入映射区的数据会复制回文件,且允许其它映射该文件的进程共享,MAP_PRIVATER:写入映射区 的数据不会复制回文件)
offset:从文件的offset位置开始映射,即偏移量。
返回:
返回映射后的内存的起始地址。
int munmap(void *start ,int length);
功能:取消参数start所指向的映射内存(该地址由mmap返回),参数length表示欲取消的内存大小。
返回:解除成功返回0,否则返回-1,错误原因存在于errno中。
查看某一进程的虚拟地址区域:命令:cat /proc/<pid>/maps
显示的方式为 start end perm offset major:minor inode
start开始区域 end结束区域 perm访问权限(p私有的 s共享的) offset映射部分在文件的起始位置 major:minor主次设备号 inode索引结点
Linux内核中使用vm_area_struct来记录上面的这些信息。
映射一个设备是指:把用户空间的一段地址关联到设备内存上,当程序读写这段用户空间的地址,它实际上是在访问设备。
int (*mmap)(struct file*, struct vm_area_struct*)
mmap函数内建立页表的方法:
1.使用remap_pfn_range一次建立所有页表。
int remap_pfn_range(struct vm_area_struct *vma, unsigned long addr, unsigned long pfn, unsigned long size, pgport_t prot);
参数:vma:虚拟内存区域指针,addr:虚拟地址的起始值,pfn:要映射的物理地址所在的物理页帧号,可将物理地址>>PAGE_SHIFT得到。size:要映射的区域大小。prot:VMA的保护属性。
2.使用nopage VMA方法,每次建立一个页表。
![图片](https://i-blog.csdnimg.cn/blog_migrate/c31d303f69a7c1326523b4f92f1f9809.jpeg)
对IO端口的操作:
1.申请
struct resource *request_region(unsigned long first,unsigned long n, const char *name);这个函数告诉内核,我要使用first开始的n个 端口,name参数是设备的名字。申请成功返回非null,失败返回null 查看系统中IO端口的分配情况:cat /proc/ioports
2.访问
unsigned inb(unsigned port); 读端口(8位宽)
unsigned outb(unsigned char byte, unsigned port);写字节端口(8位宽)
unsigned inw(unsigned port); 读端口(16位宽)
unsigned outw(unsigned short byte, unsigned port);写端口(16位宽)
unsigned inl(unsigned port); 读端口(32位宽)
unsigned outl(unsigned long byte, unsigned port);写端口(32位宽)
3.释放
void release_region(unsigned long start,unsigned long n);
对IO内存的操作:
1.申请
struct resource *request_mem_region(unsigned long start,unsigned long len, const char *name);申请一个从start开始,长度为len字节的内存区。如果成功,返回非NULL,失败返回NULL。 查看系统中IO内存的映射情况: cat /proc/iomem
2.映射
物理地址到虚拟地址的转换:void *ioremap(unsigned long phys_addr,unsigned long size);
3.访问
读:
unsigned ioread8(void *addr);
unsigned ioread16(void *addr);
unsigned ioread32(void *addr);
写:
unsigned iowrite8(u8 value,void *addr);
unsigned iowrite16(u16 value,void *addr);
unsigned iowrite32(u32 value,void *addr);
4.释放 (分二步)
1.解除映射:void iounmap(void *addr);
2.释放: void release_mem_region(unsigned long addr,unsigned size):
混杂设备:
全是字符设备,且主设备号都是10.
混杂设备用struct miscdevice来描述
注册一个混杂设备:int misc_register(struct miscdevice *misc);
设备模型:
sysfs文件系统是基于内存的文件系统。它把内核的数据结构和它们的属性暴露给用户空间。sysfs被看成是和proc文件系统同类别的文件系统,sysfs文件系统把连接在系统上的设备和总线组织成分组的文件,使其从用户空间可以访问到。
sysfs被加载在/sys/目录下,它的子目录包括:
Block:在系统中发现的每个块设备在该目录下对应一个子目录。每个子目录又包含一些属性文件,它们描述了这个块设备的各方面的属性。(loop块设备是用文件来模拟的)
Bus:在内核中注册的每条总线对应一个子目录。如:IDE,PCI,SCSI,USB,PCMCIA. 其中每个总线目录内又包含二个子目录: devices和drivers devices目录包含了在整个系统中发现的属于该总线类型的设备。drivers目录包含了注册到该总线的所有驱动。
Classc: 将设备按照功能进行的分类。
Devices:包含系统所有的设备。
Kernel:内核中的配置参数。
Modules:系统中所有模块的信息。
Firmware:系统中的固件。
Fs:描述系统中的文件系统。
Power:系统中的电源选项。
同一个设备可能出现在多个目录下,但是在Devices外的设备,最终都是链接到Devices目录下。如Buses->usb->devices----->Devices->pci0->dev 0:10->usb2,如下图:
![图片](https://i-blog.csdnimg.cn/blog_migrate/215413564c7efa0c499bf676bf3ba1b2.jpeg)
Kobject 实现了基本的面向对象管理机制,是构成Linux2.6设备模型的核心结构。它与sysfs文件系统紧密相连,在内核中注册的每个kobject对象对应sysfs文件系统中的一个目录。类似于C++中的基类,Kobject常被嵌入于其他类型(即:容器)中。如bus,devices,drivers都是典型的容器。这些容器通过kobject连接起来,形成了一个树状结构。
structk object {
constchar *name;
structlist_head entry;
struct kobject *parent;//指向父对象
struct kset *kset; //父目录
struct kobj_type *ktype;
structsysfs_dirent *sd;
struct kref kref;//对象引用计数
unsignedint state_initialized:1;
unsignedint state_in_sysfs:1;
unsignedint state_add_uevent_sent:1;
unsignedint state_remove_uevent_sent:1;
};
初始化kobject结构:void kobject_init(struct kobject kobj);
将kobject对象注册到Linux系统:int kobject_add(struct kobject kobj);
初始化并注册kobject对象到Linux(将上面二步合一):int kobject_init_and_add(struct kobject kobj, struct kobj_type *type, struct kobject *parent, const char *fmt, ...);
删除Kobject对象: void kobject_del(struct kobject kobj);
将Kobject对象的引用计数加1,并返回该对象指针:struct kobject *kobject_get(struct kobject *kobj);
将Kobject对象的引用计数减1,如果引用计数降为0,则调用release方法释放该Kobject对象:void kobject_put(struct kobject *kobj);
Kobject的ktype成员是一个指向kobj_type结构的指针, 该结构中记录了kobject对象的一些属性。
structkobj_type {
void(*release)(struct kobject *kobj);
struct sysfs_ops *sysfs_ops;
struct attribute **default_attrs; //指向指针的指针,实际上是一个数组,指向一个或多个文件
};
release:用于释放kobject占用的资源,当kobject的引用计数为0时被调用。
Struct attribute
struct attribute {
char *name; /*属性文件名*/
struct module * owner;
mode_t mode; /*属性的保护位*/
};
struct attribute (属性):对应于kobject的目录下的一个文件,Name成员就是文件名。
struct sysfs_ops
{
ssize_t(*show)(struct kobject *, struct attribute *,char *);
ssize_t(*store)(struct kobject *,struct attribute *,const char *, size_t);
};
1)Show:当用户读属性文件时,该函数被调用,该函数将属性值存入buffer中返回给用户态;
2)Store:当用户写属性文件时,该函数被调用,用于存储用户传入的属性值。
当调用 Kobject后,会有/sys/目录下生成相应的目录,并且在目录下会生成相应的文件,程序中对这个文件的操作对影响到这个文件。
{
![图片](https://i-blog.csdnimg.cn/blog_migrate/3a4b548c58a4b4a8e1531b4d6344fa57.jpeg)
Kset操作
1)int kset_register(struct kset *kset)
在内核中注册一个kset
2)void kset_unregister(struct kset *kset)
从内核中注销一个kset
热插拔事件
在Linux系统中,当系统配置发生变化时,如:添加kset到系统;移动kobject, 一个通知会从内核空间发送到用户空间,这就是热插拔事件。热插拔事件会导致用户空间中相应的处理程序(如udev,mdev)被调用, 这些处理程序会通过加载驱动程序, 创建设备节点等来响应热插拔事件。
4、热插拔事件操作集合
Struct kset_uevent_ops {
int (*filter)(struct kset *kset, struct kobject *kobj);
const char *(*name)(struct kset *kset, struct kobject *kobj);
int (*uevent)(struct kset *kset, struct kobject *kobj,
struct kobj_uevent_env *env);
}
kset_uevent_ops这三个函数什么时候调用?
当该kset所管理的kobject和kset状态发生变化时(如被加入,移动),这三个函数将被调用。(例:kobject_uevent调用)
这三个函数的功能是什么?
1)filter:决定是否将事件传递到用户空间。如果filter返回0,将不传递事件。(例: uevent_filter)
2)name:用于将字符串传递给用户空间的热插拔处理程序。
3)uevent:将用户空间需要的参数添加到环境变量中。(例:dev_uevent)
5、 kset实例分析
#include <linux/device.h>
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/string.h>
#include <linux/sysfs.h>
#include <linux/stat.h>
#include <linux/kobject.h>
MODULE_AUTHOR("yinjiabin");
MODULE_LICENSE("GPL");
struct kset *kset_p;
struct kset kset_c;
/* 函数声明 */
void obj_test_release(struct kobject *);
ssize_t kobj_test_show(struct kobject *,struct attribute *,char *);
ssize_t kobj_test_store(struct kobject *,struct attribute *,const char *,size_t);
static struct attribute test_attr =
{
.name = "kobj_config",
.mode = S_IRWXUGO,
};
static struct attribute *def_attrs[] =
{
&test_attr,
NULL,
};
static struct sysfs_ops obj_test_sysops =
{
.show = kobj_test_show,
.store = kobj_test_store,
};
static struct kobj_type ktype =
{
.release = obj_test_release,
.sysfs_ops = &obj_test_sysops,
.default_attrs = def_attrs,
};
void obj_test_release(struct kobject *kobject)
{
printk("[kobj_test: release!]\n");
}
ssize_t kobj_test_show(struct kobject *kobject,struct attribute *attr,char *buf)
{
printk("Have show -->\n");
printk("attrname: %s.\n",attr->name);
sprintf("buf,%s\n",attr->name);
return strlen(attr->name) + 2;
}
ssize_t kobj_test_store(struct kobject *kobject,struct attribute *attr, const char *buf,size_t size)
{
printk("Have store -->\n");
printk("write: %s.\n",buf);
return size;
}
static int kset_filter(struct kset *kset,struct kobject *kobj)
{
printk("Filter: kobj %s.\n",kobj->name);
return 1;
}
static const char *kset_name(struct kset *kset,struct kobject *kobj)
{
static char buf[20];
printk("Name kobj %s.\n",kobj->name);
sprintf(buf,"%s","kset_name");
return buf;
}
static int kset_uevent(struct kset *kset,struct kobject *kobj, struct kobj_uevent_env *env)
{
int i = 0;
printk("uevent: kobj %s.\n",kobj->name);
while(i < env->envp_idx)
{
printk("%s.\n",env->envp[i]);
i ++;
}
return 0;
}
static struct kset_uevent_ops uevent_ops =
{
.filter = kset_filter,
.name = kset_name,
.uevent = kset_uevent,
};
static int __init kset_test_init(void)
{
int ret = 0;
printk("kset test init!\n");
/* 创建并注册 kset_p */
kset_p = kset_create_and_add("kset_p",&uevent_ops,NULL);
/* 添加 kset_c 到 kset_p */
kobject_set_name(&kset_c.kobj,"kset_c");
kset_c.kobj.kset = kset_p;
/* 对于较新版本的内核,在注册 kset 之前,需要
* 填充 kset.kobj 的 ktype 成员,否则注册不会成功 */
kset_c.kobj.ktype = &ktype;
ret = kset_register(&kset_c);
if(ret)
kset_unregister(kset_p);
return 0;
}
static void __exit kset_test_exit(void)
{
printk("kset test exit!\n");
kset_unregister(kset_p);
kset_unregister(&kset_c);
}
module_init(kset_test_init);
module_exit(kset_test_exit);
总线是处理器和设备之间有通道,在设备模型中,所有的设备都通过总线相连,甚至是内部的虚拟“platform”总线,在Linux设备模型中,总线由bus_type结构表示,定义在</Linux/device.h>中。
总线的注册:
bus_register(struct bus_type *bus);若成功,新的总线将被添加进系统,并可以在sysfs的/sys/bus下看到。
总线的删除:
void bus_unregister(struct bus_type *bus);
设备与驱动的匹配:
int (*match)(struct device *dev, struct device_driver *drv); 当一个新设备或驱动被加添加到这个总线时,该方法被调用 ,用于判断指定的驱动程序是否能处理指定的设备,若可以则返回非0值。
添加环境变量:
int (*uevent)(struct device *dev, char **envp, int num_envp, char *buffer, int buffer_size):在为用户空间产生热插拔之前,这个方法允许总线添加环境变量。\
总线属性结构:
struct bus_attribute{
struct attribute attr;
ssize_t (*show)(struct bus_type *type, char *buf); //当读属性的时候show会被调用
ssize_t (*store)(struct bus_type *type, const char *buf, size_t count); //当写属性的时候strore会被调用
}
创建总线属性文件:
int bus_create_file(struct bus_type *type, struct attribute attr);
删除总线属性文件:
void bus_remove_file(struct bus_type *type, struct attribute attr);
创建总线的例子:
Linux系统中每一个设备由一个struct device描述:
struct device{
... ... ... ... ... ... ...
struct kobject kobj;
char bus_id[BUS_ID_SIZE]; //在总线上唯一标识该设备的字符串
struct bus_type bus; //设备所在总线
struct device_driver driver; //管理该设备的驱动
void *driver data; //该设备驱动使用的私有数据成员
struct klist_node knode_class;
struct class *class;
struct attribute_group **groups;
void (*release)(struct device *dev);
}
注册设备:
int device_register(struct device *dev);
注销设备:
int device_unregister(struct device *dev);
//一条总线也是一个设备,也必须按设备注册
Linux系统中每一个驱动由一个struct device_driver描述:
struct device_driver{
const char *name; //驱动程序的名称
struct bus_type bus; //驱动程序所在的总线
struct module *owner;
const char *mod_name;
int (*probe)(struct device *dev); //当这个驱动程序在相应总线上找到了它能处理的设备的时候,该函数被调用
int (*remove)(struct device *dev); //当这个驱动程序能处理的设备被删除的时候,该函数被调用
void (*shutdown)(struct device *dev);
int (*suspend)(struct device *dev, pm_messae_t state);
int (*resume)(struct device *dev);
struct attribute_group **groups;
struct dev_pm_ops *pm;
struct driver_private *p;
}
注册驱动 :
int driver_register(struct device_driver *drv);
注销驱动:
void driver_unregister(struct device_driver *drv);
驱动属性结构:
struct bus_attribute{
struct attribute attr;
ssize_t (*show)(struct device_driver *drv, char *buf); //当读属性的时候show会被调用
ssize_t (*store)(struct device_driver *drv, const char *buf, size_t count); //当写属性的时候strore会被调用
}
创建驱动属性文件:
int driver_create_file(struct device_driver *type, struct attribute attr);
删除驱动属性文件:
void driver_remove_file(struct device_driver *type, struct attribute attr);
"plarform"总线是Linux2.6新加入的一种虚拟总线,由二部分组成: platform_device和 platform_driver.
Platform驱动与传统的设备驱动模型相比,优势在于Platform机制将设备本身的资源注册进内核,由内核统一管理,在驱动程序使用这些资源时使用统一的接口,这样提高了程序的可移植性。
通过Platform机制开发底层设备驱动的流程:
![图片](https://i-blog.csdnimg.cn/blog_migrate/c400b767b0ab88e134491a729f9ec9a6.jpeg)
Linux系统中一个平台设备由一个struct device_driver描述:
struct platform_device{
const char *name; //设备名
int id; //设备编号 ,配合设备名使用
struct device dev;
u32 num_resources;
struct resource *resource; //设备资源
}
平台设备分配 :
struct platform_device *platform_device_alloc(const char *name, int id ); name:设备名称 id:设备ID,一般为-1
注册平台设备:
int platform_device_add(struct platform device *pdev);
Linux系统中平台设备资源由struct resource描述:
struct resource{
resource_size_t start; //资源的起始物理地址
resource_size_t end; //资源的结束物理地址
const char *name; //资源的名称
unsigned long flags; //资源的类型,比如MEM,IO,IRQ类型
struct resource *parent, *sibling, *child; //资源链表指针
}
strucrt resource s3c_wdt_resource1 = {
.start = 0x44100000; //这里的start和end代表的是物理地址的起始和结束地址(IORESOURCEMEM)。
.flag = IORESOURCEMEM;
}
strucrt resource s3c_wdt_resource1 = {
.start = 20; //这里的start和end代表的是中断号(IORESOURCEIRQ)。
.flag = IORESOURCEIRQ;
}
获取资源:
struct resource *platform_get_resource(struct platform_device dev, unsigned int type, unsigned int num);
参数:dev:资源所属的设备,type:获取资源的类型,num:获取的资源数
平台驱动使用struct platform_driver描述:
struct platform_driver{
int(*probe)(struct platform_device*);
int(*remove)(struct platform_device*);
void (*shutdown)(struct platform_device*);
int (*suspend)(struct platform_device *,pm_message_t state);
int(*suspend_late)(struct platform_device*, pm_message_t state);
int(*resume_early)(struct platrorm_device*);
int(*resume)(struct platform_device*);
struct device_driver driver;
}
平台驱动注册:
int platrform_driver_register(struct platform_driver *);
在Linux驱动程序中为设备实现一个中断包含二个步骤:
1.向内核注册中断。int request_irq(unsigned int irq, void(*handler)(int,void*,struct pt_regs*),unsigned long flags,const char *devname,void *dev_id);返回0表示成功,否则返回一个错误码。irq:中断号,handler:中断处理函数,flags:与中断管理有关的各种选项,devname:设备名,dev_id:共享中断时使用。
flags:IRQF_DISABLE(2.4中:SA_INTERRUPT):如果设置该位表示是一个“快速”中断处理程序,如果没有设置该位表示是一个“慢速”中断处理程序。IRQF_SHARED(2.4中:SA_SHIRQ):该位表明中断可以在设备间共享。
快速中断保证中断处理程序 不被打断(实际上快速中断会关闭中断使能位,此时所有的其它中断都不能被处理了),而慢速中断则不保证。
共享中断就是将不同的设备挂到同一个中断信号线上,Linux对共享的支持主要是为PCI设备服务。共享中断也是也是通过request_irq函数来注册的,但是有三个不同之处:1.申请共享中断时必须在flags参数指定IRQF_SHARED,2.dev_id参数必须是唯一的.3.共享中断的处理程序中,不能使用disable_irq(unsigned int irq),如果使用了这个函数,共享中断信号线的其它设备将无法使用中断,也就无法正常工作了。
2.实现中断处理函数:中断处理程序中1.不能向用户空间发送或接收数据,2.不能使用可能引起阻塞的函数,3.不能使用可能引起调度的函数。
3.释放中断, 当设备不再需要使用中断时(通常在驱动卸载时),应该把它们返还给系统:
void free_irq(unsigned int irq, void *dev_id);
中断处理程序流程:
void short_sh_interrupt(int irq, void *dev_id, struct pt_regs *regs){
//判断是否本设备发生了中断
value = inb(short_base);
if(!(value & 0x80)) return;
//清除中断位
outb(value & 0x7f, short_base);
//中断处理,通常是数据接收
。。。。。。。。。。。
//唤醒等待中断的进程
wake_up_interruptible(&short_base);
}
如果是共享中断的话,该共享中断线发生了中断,比如五号,这时内核只知道是五号中断线发生了中断,但是不能知道到底是该中断线上哪个设备发生了中断。这时内核会把中断线上的所有设备中断处理程序全调用一遍。所以设备中断处理程序中必须判断是否是本设备发生了中断。
Linux系统中网络栈一般分为四层的Internet模:
一、协议栈层次对比
![图片](https://i-blog.csdnimg.cn/blog_migrate/9ad807473803ea0b4c933bb5eb2125d9.jpeg)
二.Linux网络子系统
Linux网络子系统的顶部是系统调用接口层。它为用户空间提供的应用程序提供了一种访问内核网络子系统的方法(socket)。位于其下面是一个协议无关层,它提供一种通用的方法来使用传输层协议。然后是具体协议的实现,在Linux中包括内核的协议TCP,UDP,当然还有IP。然后是设备无关层,它提供了协议与设备驱动通信的通用接口,最下面是设备的驱动程序。
每一个网络设备都由struct net_device来描述,该结构可使用如下内核函数进行动态分配:
struct net_device *alloc_netdev(int sizeof_priv, const char *mask, void(*setup)(struct net_device *))
sizeof_priv是私有数据区大小;mask是设备名,setup是初始化函数,在注册该设备时,该函数被调用。也就是net_deivce的init成员。
struct net_device *alloc_etherdev(intsizeof_priv)
这个函数和上面的函数不同之处在于内核知道会将该设备做一个以太网设备看待并做一些相关的初始化。
net_device结构可分为全局成员、硬件相关成员、接口相关成员、设备方法成员和公用成员等五个部分
主要全局成员:
char name[INFAMSIZ] 网卡设备名,如:eth%d (加一个%d内核会自动填设备号如 eth2)
unsigned long state 设备状态
unsigned long base_addr I/O基地址
unsigned int irq 中断号
int (*init)(struct net_device *dev);初始化函数, 该函数在register_netdev时被调用,来完成对net_device的初始化。
打开接口:
int (*open)(struct net_device *dev);
ifconfig激活时,接口将被打开,1.注册中断,DMA等。2.设置寄存器,启动设备,3.启动发送队列 netif_start_queue(dev)。字符设备一般在模块初始化函数里面注册中断处理程序,但是网卡设备一般在open注册中断,因为网卡关闭的时候没必要占有中断。
网络设备open例子:
int net_open(struct net_device *dev){
//申请中断
request_irq(dev->irq, &net_interrupt, SA_SHIRQ, "dm9000", dev);
//设置寄存器,启动设备
...... ...... ...... ......
//启动发送队列
netif_start_queue(dev);
}
停止接口 :
int (*stop)(struct net_device *dev);
ifconfig eth% down时调用
要注意的是ifconfig是interface config的缩写,通常我们在用户空间输入:
ifconfig eth0 up 会调用这里的open函数。
在用户空间输入:
ifconfig eth0 down 会调用这里的stop函数。
在使用ifconfig向接口赋予地址时,要执行两个任务。首先,它通过ioctl(SIOCSIFADDR)(Socket I/O Control Set Interface Address)赋予地址,然后通过ioctl(SIOCSIFFLAGS)(Socket I/O Control Set Interface Flags)设置dev->flag中的IFF_UP标志以打开接口。这个调用会使得设备的open方法得到调用。类似的,在接口关闭时,ifconfig使用ioctl(SIOCSIFFLAGS)来清理IFF_UP标志,然后调用stop函数。
数据发送函数:
int (*hard_start_xmit)(struct sk_buf*skb,struct net_device *dev)
ioctl命令处理函数:
int (*do_ioctl)(struct net_device *dev, struct ifmap *map);
用来实现自定义的ioctl命令,如果不需要可以为NULL。
改变mac地址:
int (*set_mac_address)(struct net_device *dev, void *addr);
如果硬件支持mac地址改变,则可以实现该函数来改变mac地址。
int (*hard_header)(struct sk_buff *skb, struct net_device *dev, unsigned short type, void *daddr, void *saddr, unsigned len); 该方法根据先前检索到的源和目的硬件地址建立硬件头
int (*rebuild_header)(struct sk_buff *skb);以太网的mac地址是固定的,为了高效,第一个包去询问mac地址,得到对应的mac地址后就会作为cache把mac地址保存起来。以后每次发包不用询问了,直接把包的地址拷贝出来。
void (*tx_timeout)(struct net_device *dev); 如果数据包发送在超时时间内失败,这时该方法被调用,这个方法应该解决失败的问题,并重新开始发送数据。
struct net_device_stats *(*get_stats)(struct net_device *dev); 当应用程序需要获得接口的统计信息时,这个方法被调用
int (*set_config)(struct net_device *dev, struct ifmap *map); 改变接口的配置,比如改变I/O端口和中断号等,现在的驱动程序通常无需该方法。
int (*do_ioctl)(struct net_device *dev, struct ifmap *map); 用来实现自定义的ioctl命令,如果不需要可以为NULL。
void (*set_multicast_list)(struct net_device *dev); 当设备的组播列表改变或设备标志改变时,该方法被调用。
int (*set_mac_address)(struct net_device *dev, void *addr); 如果接口支持mac地址改变,则可以实现该函数。
设备驱动接口层:
net_device结构体的成员(属性和函数指针)需要被设备驱动功能层的具体数值和函数赋予。对具体的设置xxx,工程师应该编写设备驱动功能层的函数,这些函数型如xxx_open(),xxx_stop(),xxx_tx(),xxx_hard_header(),xxx_get_stats(),xxx_tx_timeout()等。
网络设备注册方式与字符驱动不同之处在于它没有主次设备号,并使用下面的函数注册:
int register_netdev(struct net_deivce*dev)
网络设备的注销:
void unregister_netdev(struct net_device*dev)
Linux内核中每一个网络数据包都由一个套接字缓冲结构struct sk_buff描述,即一个struct sk_buff就是一个包,指向sk_buff的指针通常被称作skb,该结构包含如下重要成员:
struct device *dev; //处理该包的设备
__u32 saddr; //IP源地址
__u32 daddr; //IP目的地址
__u32 raddr; //IP路由器地址
unsigned char *head; //分配空间的开始
unsigned char *data; //有效数据的开始
unsigned char *tail; //有效数据的结束
unsigned char *end; //分配空间的结束
unsigned long len; //有效数据的长度
用来操作sk_buff的内核函数:
struct sk_buff *alloc_skb(unsigned len,int priority);分配一个sk_buff结构,供协议栈代码使用。
struct sk_buff *dev_alloc_skb(unsigned len); 分配一个sk_buff结构,供驱动代码使用。
unsigned char *skb_push(struct skb_buff *skb, int len);向后移动skb的tail指针,返回移动之前的tail值。目的是在原数据后面添加新数据
unsigned char *skb_put(struct skb_buff *skb, int len);向前移动skb的head指针,并返回移动之后的head值。 目的是在原数据之前添加新数据
kfree_skb(struct skb_buff *skb);释放一个sk_buff结构,供协议栈代码使用。
dev_kfree_skb(struct skb_buff *skb); 释放一个sk_buff结构,供驱动代码使用。
当内核需要发送一个数据包时,它会调用hard_start_transmit函数,该函数最终调用到net_device结构中的hard_start_xmit函数。当要发送数据之前要先调用netif_stop_queue(dev)把发送队列关闭,意思是告诉内核:我现在正在发送数据,不要再往我这发送数据了。
当接到一个数据包,触发中断处理函数后,在函数中:1.分配skb包,skb=dev_alloc_skb(pkt->datalen+2),这里多分二字节是因为数据包头是四字节对齐的,而IP头中的目的地址,源地址,数据字段长度总和是14,所以要加2让其四字节对齐。见下图; 2.从硬件中读取数据到skb. 3.调用netif_rx将数据交给协议栈netif_rx(skb)。
网络接口通常支持3种类型的中断:新报文到达中断,报文发送完成中断和出错中断。中断处理程序可通过查看网卡中的中断状态寄存器,来分辨出中断类型。