嵌入式软件工程师&&太极拳
全部博文(548)
分类: LINUX
2011-02-20 20:23:33
我们一般open一个文件,其实就是在打开硬盘这个设备,因为文件存在硬盘当中,要有硬盘驱动的支持,所有软件都要有硬件的支持,怎么支持就要用驱动 ls -l看文件时,在文件组后面,为主设备次设备号,主设备号对做驱动最重要了,主设备号和内核驱动设备号相对应的 内核也用了面向对象思想,看到结构体就要想到这是一个类或对象 include/linux/fs.h struct file 描述上层文件的对象 struct inode 更详细的上层文件的对象 struct file_operations 对于驱动开发这个最重要的结构体,字符设备 ,描述上层文件的操作 函数主要看:功能,参数和返回值 这两个API是老的,不推荐用了 注册驱动到内核里去: int register_chrdev(unsigned int major, const char *name, struct file_operations *fops); major 主设备号, name 驱动名字 fops 一个函数集,为驱动的操作函数指针 对应注册的卸载函数: int unregister_chrdev(unsigned int major, const char *name); 新的注册驱动到内核里的函数: 初始化函数: void cdev_init(struct cdev *cdev, struct file_operations *fops); struct cdev 描述一个字符设备 在include/linux/cdev.h 注册函数: int cdev_add(struct cdev *, dev_t, unsigned); dev_t 设备号,不叫主设备号,怎么得到 用MKDEV宏,MKDEV(主设备号,次设备号);返回一个设备号 主设备号是描述某一类设备 最后一个参数unsigned 表示一个注册几个设备,一般为1 卸载函数: void cdev_del(struct cdev *); 主设备号: Documentation/devices.txt 看哪些主设备号用了 主设备号具有唯一号,内核有保留给我们用的主设备号240到254 只要注册了就可以用了,但是为了方便内核管理等所以得申请 申请内核我要注册一个什么设备号: void register_chrdev_region(dev_t from, unsigned count, const char name); dev_t from 设备号, unsigned count 要注册几个这类的设备, const char name 设备名,给文件系统看的,没什么事 释放:和申请配对 void unregister_chrdev_region(dev_t from, unsigned count); 申请和注册一般都是一起跟着走 自动申请一个内核没有在用的设备号: int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const char *name); dev_t *dev 传进去给变量赋值,unsigned baseminor次设备号,一般为0, unsigned count 要注册几个这类的设备, const char name 设备名 对应的释放函数还是 void unregister_chrdev_region(dev_t from, unsigned count); mknod /dev/mydev c 254 0 创建一个设备文件,c为字符设备,如果写p为管道文件,254主设备号 0为次设备号 cat /proc/devices 查看注册了多少设备驱动 内核函数,驱动编程来调用内核函数, asm-- 内核函数--驱动编程--posix-- 系统调用 ---C库--APP 应用程序->C库函数->系统调用(POSIX)->驱动编程的函数->内核函数->硬件相关代码(汇编) 驱动编程主要关心内核函数 include/asm_generic/errno_base.h 内核错误码 容错处理包含: include/linux/errno.h 在注册时判断容错,一般在Linux内核源码中错误处理用的最多的是goto结构 file_openations: int open(struct inode *, struct file *); try_module_get(THIS_MODULE); 尝试看看模块能不能操作,一般不用 int release(struct inode *, struct file *); 相当于close open和close一般没有什么实现代码,重要是返回值,为0则返回正常 module_put(THIS_MODULE); 告诉 内核这模块现在不用了,和上面try_module_get相对应 ssize_t read(struct file *, char __user *, size_t, loff_t *); ssize_t write(struct file *, char __user *, size_t, loff_t *); 带检查的拷贝函数: /include/asm/uaccess.h 返回值为还有多少没有拷贝,一般为0 unsigned long copy_to_user(void __user *to, const void *form, usnigned long n); 对应read unsigned long copy_from_user(void *to, const void __user *form, unsigned long n); 对应write int ioctl(struct inode *node, struct file *filp, unsigned int cmd, unsigned long args); 来一个命令然后执行一段代码 unsigned int cmd 命令,unsigned long args 看作指针,做为参数 struct file_operations 结构注册函数时 owner = THIS_MODULE; 这句为固定 同步通知: 实现阻塞I/O: 生产者消费者模型: read 为消费者, write 生产者 睡眠机制: init_waitqueue_head(wait_queue_head_t *q); wait_event_interruptible(wait_queue_head_t rq, condition); rq 队列 condition 条件 wake_up(wait_queue_head_t *rq); 唤醒整个等待队列 阻塞和轮询的区别: 都是为了有事件发生再执行,不同为轮询为忙等待,即一直占用CPU,而阻塞为在等待着会睡眠,这样就不会占用CPU 最常用的轮询为select() ,做驱动网卡方面才用的最多,字符设备一般不会用轮询 实现select : includeunsigned int poll(struct file * filp, struct poll_table_struct *table); poll_wait(struct file *filp, wait_queue_head_t *, struct poll_table table); 注册队列 返回mask即下面的宏: POLLIN read 不空就读 POLLOUT write 空的话提示写 看看file_operations里有没有相对应的函数指针,如果有把函数指针拷贝过来,再在file_operations结构体里加入函数,再把头文件加进来 异步通知: signal 工作中不要用这个函数,是个老函数,可重入性很差,用sigaction()函数 int sigaction(int signum, const struct sigaction * act, struct sigaction *oldact); signum 信号号, act 要一个结构体 oldact 要上一操作,可重入性 memset(&act, 0, sizeof(struct sigaction)); act.sa_handler = 信号调用的函数; act.flag = 0 ;设置标志 fcntl(fd, F_SETOWN, getpid()); 设置属注 对应内核file_operations的owner成员赋值 fcntl(fd, F_SETL, (fcntl(fd, F_GETFL) | FASYNC)); 设置标志 对应内核file_operations的fasync函数指针 应用层:注册信号,设置属注,设置标志 驱动层:一个数据结构两个函数 fs/fcntl.c int fasync_helper(int fd, struct file * filp, int on, struct fasync_struct **fapp); 相对应于注册函数,注册到内核,并且把file->owner注册进去 int fasync(int, struct file *, int); file_operations 里的函数指针 void kill_fasync(struct fasync_struct **fapp, int signum, POLLIN); 发送信号 调用自己的fasync函数 test_fasync(-1, filp, 0); 在release 函数里移出自己的注册的信号 MMC 是SD卡 test: version magic '2.6.13-Qt2440 ARMv4 gcc-3.4' should be '2.6.13-Qt2440 preempt ARMv4 gcc-3.4' insmod: cannot insert `test.ko': Invalid module format (-1): Exec format error 用抢占式内核程序插入到不支持抢占式内核,就会出错误,解决办法用不支持抢占式内核编译程序就可以了 并发和竞态(互斥): 竞态就是在两个进程同时修改一个数据,所得数据不是用户想要的 会发生竞态: 单核: 进程与进程之间 用的最多 preempt_disable(); preempt_enable(); 进程和中断之间 local_irq_disable(); local_irq_enable(); 中断和中断之间 可重入性好,会保护现场 local_irq_save(flag); local_irq_restore(flag); flag为一 个int值 多核: 进程与进程之间 抢占时间 CPU轮询空转,没有睡眠:自旋锁 头文件: include/splock.h spin_lock(lock); 干的事就是调用 下面函数 参数lock为spinlock_t 的结构体指针 preempt_disable; 先防止CPU1抢占时间片 _raw_write_lock(lock); 先CPU2自旋轮询 spin_unlock(); 解锁 _raw_spin_lock(lock); preempt_enable; 进程和中断之间 spin_lock_irq(lock); local_irq_disable; spin_lock(); spin_unlock_irq(lock); local_irq_enable; spin_unlock(); 中断和中断之间 spin_lock_irqsave(lock) local_irq_save(); 关中断,用这个可重入性好 spin_lock(); 防止时间片和让CPU自旋 spin_lock_irqrestore() local_irq_restore(); spin_unlock(); spin_lock: 占CPU资源,比较危险,时实性要求比较高的才用 spinlock_t 结构体为spin_lock的一个结构体 spin_lock_init(spinlock_t *); 初始化spin_lock 注意: 单核 就是 preempt 在多核 spin_lock 和 spin_unlock 之间千万不要睡眠, 因为CPU自旋用了睡眠就没有其它唤醒它,会引起睡眠的函数: copy_to_user和copy_from_user sleep等, 慢I/O就是用户态和内核态之间的I/O, 还有kmalloc semaphore: 头文件: include/asm/semaphore.h struct semaphore sem; 结构体 init_MUTEX(struct semaphore *sem); 初始化 用法 : down(struct semaphore *sem); 上锁 不可被信号中断,比较少用 down_interruptible(struct semaphore *sem); 可被信号中断,为了被打断后能回来,所以一般都用下面这句判断 if (down_ifterruptible(struct semaphore *sem)) return -ERESTARTSYS; up(struct semaphore *sem); 解锁 semaphore和spin_lock 在spin_lock和spin_unlock里不能用semaphore, 因为semaphore会引起睡眠 spin_lock效率高,占CPU资源 要互斥就要用semaphore 内核知识,理论: (书) Linux内核设计与实现第二版 陈莉君 (总括,内核里的框架) 深入理解Linux内核第三版 陈莉君 (深入细看,要有操作系统的概念) Linux内核源码情景分析 毛德操 (当字典,很细,缺点讲2.4内核) 操作系统 驱动: Linux设备驱动 第三版 <ldd3> 魏永明 (实践性强, x86, 标准) edd3 纯英文的 Linux设备驱动详解 宋宝华 (实践,arm) 硬件: 嵌入式硬件设计 O‘Relly 模块 字符设备 并发与竞态 分配内存 操作硬件 中断 时间延时,底半部,推迟执行 Linux设备模型 驱动实例的讲解 分配内存: 一个进程跑在操作系统有4G地址空间,不是内存空间,用户态在底地址3G空间,内核态在高地址占1G空间 在内核态的1G中,分两种地址: 一种叫逻辑地址 又叫线性地址,和内存地址一一对应 kmalloc 分配小空间用,一定要判断返回值,经常会申请失败,可能会睡眠要注意 一种叫虚拟地址,在内存中零碎分配再对应到虚拟地址 vmalloc 分配大空间用 mmu 粗小页为4K,linux内核里是以页管理内存,用的是粗小页,所以用的时候要以4K对齐(是和内核打交道时要4K对齐) #include struct page kmalloc的头文件: #include void * kmalloc(size_t size, unsigned int __nocast flags); size 分配的空间的大小 flags GFP_ATOMIC 不会睡眠,告诉kmalloc如果没有那么大的空间,不要等待,直接返回失败,所以绝对要判断返回值,这样就不会睡眠,可以在中断里用了 GFP_KERNEL 最常用的分配空间的标志 判断返回值为NULL失败 配对释放空间函数: kfree(); 设计程序时全局变量少用,全局结构体也要少用,可以全局指针 I/O 访问: 头文件 : #include 拿到物理地址映射的虚拟地址: unsigned long ioremap(unsigned long phs, int size); 映射物理地址到虚拟地址 phs 为要映射的物理地址, size 为要映射多大,返回值为一个虚拟地址 iounmap() 和ioremap 相配对,参数要ioremap的返回值 主要是可以在/proc/iomem里看到自己映射物理地址情况: struct resource * request_mem_region(struct resource *parent, unsigned long start, unsigned ing size, unsigned long name); parent 没有什么大太用处,可以不写 start 硬件物理地址 , size ioremap申请时的大小 name 可以在/proc/iomem 看到自己映射的情况 要判断返回值是否为NULL,为NULL失败 release_resource(struct resource *parent); 相对应的,退出时告诉内核内存地址不用了 静态映射: 就是内核一起来就已经映射好了,ioremap是自己的程序运行起来才映射 arch/arm/mach-s3c2410/mach-bit2440.c static struct map_desc bit2440_iodesc[] __initdata 找到这个结构,加上一组映射关系,虚拟地址,物理地址,长度,固定设备类型 操作I/O: ioread8(p); ioread16(p); ioread32(p); iowrite8(v, p); v 为值, P 为地址 iowrite16(v, p); iowrite32(v, p); cat /proc/cpuinfo 查看CPU信息 cat /proc/devices 查看哪些设备注册了 cat /proc/iomem 查看内存映射情况 cat /proc/interrupts 查看注册了哪些中断 arch/arm/common/rtctime.c 分层结构,回调函数 cdev_add这个函数要放在init函数的最后 中断: 中断处理函数,要注册进内核, 中断号跟板相关,头文件在 include/asm-arm/arch-s3c2410/irqs.h 头文件 : include asm/irq.h linux/interrupt.h int request_irq(IRQ_EINT0, do_irq, unsigned long irq_flags, const char * devname, void *dev_id); IRQ_ENIT0 为中断源, do_irq为函数指针, devname 为中断名,可以在/proc/下看到 dev_id为给函数指针传参 函数指针原型: irqreturn_t do_irq(int irq, void *dev_id, struct pt_regs *regs); int irq 为中断号 struct pt_regs *reg 为调试用的, dev_id为注册中断时接参数 返回值为 IRQ_HANDLED或0 因为0不正规 unsigned long irq_flags 有几个flags 最常用的SA_INTERRUPT SA_SHIRQ 共享中断flag 必须要传参 注册中断一定要判断返回值 free_irq(int irq, void *dev_id); 注册中断与之对应释放函数 set_irq_type(unsigned int irq, unsigned int type); 设置按键类型, 在includeasm/irq.h里有宏 注册时要判断返回时,看注册成功没,如果没有成功,看看/proc/interrputs 里有没有被占用,然后搜索占用的是不是模块,然后移出来 oops 中断 start_kernel() 是内核第一个执行的函数: 上锁 初始化中断等等,接收U-boot的参数 最后干的事:rest_init(); 启动守护进程init,是文件系统的 第一条线: 找到异常向量表 执行的过程 start_kernel -> trap_init->vector_start(arch/arm/kernel/entry-arm.S)->__stubs_start(arch/arm/kernel/entry-armv.S)-> _irq_svc ->irq_handler(本文件里的 .macro irq_handler)->asm_do_IRQ(C语言的)里的desc->handle(irq, desc, regs);(跟irq处理相关的) 然后找到struct irqdesc *desc; 这个结构体数组全局的,是arm linux的中断体系最重要的结构体 成员: 以中断号为索引的数组 irq_handler_t handler; 这个是中断来了的处理函数 struct irqchip *chip; 和清中断和开关中断 struct irqaction *action; 存我们的函数等 ,指向一个链表 第二条线: 找到了struct irqdest r handler成员,看看哪修改了它 初始化过程 start_kernel-> init_IRQ->init_arch_irq();(全局的函数指针,要全局查找)->setup.c(给init_arm_irq赋值)->找到mdesc这个结构体,再找这个在哪初始化->setup_machine()这个函数返回的结构体->lookup_machine_type这个函数的返回值,这个函数在汇编实现的,在Head.S里-> 找到MACHINFO_TYPE这个段,跟CPU的machine相关,在arm/mach-s3c2410/mach-bit2440.c里MACHINE_START这个宏:看它的定义可以看到 machine_desc这个结构体,然后这个宏把这个结构体初始化, 再看 init_irq初始化成什么 (bit2440_init_irq)这个函数, 即init_arch_irq这个函数执行就是bit2440_init_irq这个函数 然后看bit2440_init_irq是什么,调用了s3c24xx_init_irq 这个函数清中断 跟全局的struct irqdesc desc对应并赋值, 找到 do_edge_IRQ->__do_irq() 会调到action的handler 最后找 struct irqaction *action 在哪被初始化就能找到我们注册的handler 第三条线:注册的过程 从request_irq() 最后找到request_irq() 初始化了action 再调用setup_irq函数 把我们的do_irq函数加进一个链表(这一步是将我们的函数加到链表里,这样就可以发生一个中断,可以触发这整个链表的函数) 共享标志中断: 就是换个标志,可以注册多个函数,一般共享中断传参,有多少个request_irq就要有对应的多少个free_irq 中断资源比较珍贵,所以中断处理函数不能太长,就引出了顶半部和底半部之说,即顶半部开出另一个进程来处理,处理的部分就叫做底半部 tasklet_init(struct tasklet_struct *, void (* func)(unsigned long val), unsigned long data); 初始化,注册函数和传参 中断里常用的 kill wake up compltion 不能睡眠和kmalloc; tasklet_schedule(struct tasklet_struct *); 用tasklet,就是调用tasklet_init注册的函数,像多线程,不在中断的上下文里执行, 这个就叫底半部, 用的比较少 INT_WORK(struct work_struct *, void (*func)(void *val), void *data); 初始化的宏,和tasklet一样 schedule_work(struct work_struct *work); 调度用,这个最常用了 重点: tasklet_init 不可以睡眠,在软中断上下文 work_struct 可以睡眠,在进程的下下文,最常用 kernel_thread(int (*fn)(void *), void *arg, unsigned long flags); 内核态多线程函数 内核时间: Hz 频率 ,一秒多少次 jiffies 滴答数, 多少次一秒,内核独有的 内核一运行jiffies为0,开始累加,1HZ运行多少次,jiffies就加多少 HZ和jiffies是内核全局宏,可以直接用 kernel/time.c do_gettimeofday(); 可以获取时间 延时: linux/delay.h 里面有延时函数: 到这里来查 mdelay() 延时微秒,mdelay(1000)为延时一秒 ndelay() 纳秒级 msleep() 等 等 定时: linux/timer.h 一个结构体两个函数 struct timer_list; 结构体为主角 struct list_head entry; 内核管理用的,一般我们不用管