驱动初步学习
linux内核组成:进程调度,内存管理,虚拟文件系统VFS,网络接口,进程间通信机制ipc
进程调度:
在linux内核中,使用task_struct结构体来描述进程,包含进程的内存、文件系统、文件、tty资源还有信号处理等的指针。
当创建线程时,内核创建一个新的task_struct,然后将新的task_struct所有资源指针指向创建它的那个task_struct的那个资源指针。
绝大所数进程和进程的线程是由用户空间的应用创建的,当他们存在对底层资源的需求时,通过系统调用进入内核空间。在内核编程中,当有几个并发任务需要执行时,可以启动内核线程,这些线程没有用户空间,函数为:
pid_t kernel_thread(......);
内存管理:
控制多个进程安全的共享内存区域。
虚拟文件系统:
隐藏各种设备的具体细节,为所有的设备提供统一的接口。它独立于各个具体的文件系统,是对各种文件系统的抽象。
网络接口:
提供对各种网络标准的存取和对各种网络硬件的支持。
进程间通信:
信号量,共享内存,消息队列,管道,套接字...
这些机制协助多个进程,多资源的互斥访问,进程间的同步和消息传递,安卓内核新增了Binder进程间通信的方式。
内核编译及加载:
make menuconfig
编译内核和模块:
make ARCH=arm zImage
make ARCH=arm modules
在内核中添加程序:
将代码添加到内核源代码的相应目录中,然后在目录的Kconfig文件中添加关于新源代码的配置,在目录的makefile中增加对源代码的编译条目,
Makefile:
obj -y 编译链接进内核
obj -m 文件作为模块编译
obj -n 目标不会被编译
Kconfig。。。
linux内核引导:
上电时,cpu0上,botroom去引导bootloader,其余的cpu判断自己是不是cpu0。当bootloader引导linux内核时,cpu0会引发中断唤醒cpu,之后所有的cpu都投入使用。cpu0导致用户空间的init程序被调用,之后init程序再去派生其他的进程,cpu共同负载。
linux内核模块:
简单的内核模块包括加载,卸载和GPL许可的声明。
static int __init module_init(void)
static void __exit module_exit(void)
MODULE_LICENES(“GPL”);
编译模块文件:
Makefile:
obj -m = module.c
all:
make -C /lib/modules/linux版本/bulid 文件绝对路径 modules
clean:
make -C /lib/modules/linux版本/bulid 文件绝对路径 clean
加载模块:
insmod module.ko /rmmod module
modproble module.ko/ modproble -r module
modindo 模块名 查看模块的信息
定义模块参数:
static char *name = “linux device driver”;
module_param(name,charp,S_IRUGO);
static int num =400;
module_param(num,int,S_IRUGO);
加载模块后,通过/var/log/messages 看到内核输出
当加载内核时,传递参数时:insmod module.ko name=”linux” num=5000时,显示的时传递的参数
导出符号:
linux的/proc/kallsyms文件对应着内核符号表
模块使用宏定义到内核符号表中:
EXPORT_SYMBOL(符号名)
EXPORT_SYMBOL_GPL(符号名):只适用于包含GPL许可权的模块
linux文件系统与设备文件
linux文件操作:
创建文件:int creat (filename,mode)
打开文件:int open (pathname,flags)/ int open (pathname,flags,mode)
文件打开标志:O_RDONLY;O_WRONLY;O_RDWR;
若是创建文件并且打开,还需要指定mode(可以用五个数字去描述)
分别是设置用户id,设置组id,自己权限,组权限,其他权限(11 777)
打开文件后返回一个文件描述符fd
文件读写:
int read (fd,buf,sizeof(buf));
int write(fd,buf,sizeof(buf));
定位:
int lseek(fd,offset,whence);相对于whence移动offset个字节
whence:SEEK_SET相对于文件开头,SEEK_END相对于文件结尾,SEEK_CER相对于文件读写指针的当前位置。
关闭:
int close(fd)
C库文件操作(C库文件操作独立于具体的操作平台)
FIle *fopen(name,mode)
mode:r,w,a;只读只写追加,w和a当文件不存在时,创建文件
r+,w+,a+:读写,读写文件不存在时创建文件,读和追加,不存在时创建文件。
字符设备驱动
cdev结构体:
dev_t 定义了32位设备号,前12位为主设备号,后20位为次设备号
主设备号:MAJOR(dev) 次设备号:MINOR(dev)
通过主次设备号生成dev_t MKDEV(major,minor)
操作cdev结构体:
初始化cdev成员并建立cdev与file_operation的连接:
void cdev_init(struct cdev *, struct file_operations *(文件操作结构体))
动态申请一个cdev内存:
struct cdev *cdev_alloc(void)
向系统中添加或删除一个cdev:
int cdev_add(struct cdev *,dev_t ,unsigned)
void cdev_del(struct cdev *)
分配和申请设备号:
已知起始设备号:
int register_chrdev_region(dev_t from,unsigened count,const char *name);
设备号未知,动态申请未被占用的设备号,并把设备号放到第一个参数dev中
int alloc_chrdev_region(dev_t *dev,unsigned baseminor,unsigned count,const char *name);
file_operations结构体
file_operation中的成员函数是字符设备驱动程序设计的主要内容,这些函数会在应用程序在linux的open,read,write,close等系统调用时最终被内核调用。
llseek()用来修改文件当前的读写位置,将新位置返回,出错返回负值
read()用来读取设备文件的数据,成功返回读取的字节数,失败返回负值。与用户空间对应的read()和fread()相对应。
write()向设备发送数据,成功返回写入的字节数,若失败,用户进行系统调用时,会得到-EINVAL返回值。与用户空间的read()和fread()相对应。
如果read()和write()返回0,则表示end-of-file
mmap用于设别内存映射到进程的虚拟空间地址中,若未实现,用户进行mmap()进行系统调用时,返回-ENODEV。此函数对于帧缓冲等设备有意义,帧缓冲被映射到用户空间后,应用程序可以直接访问,不需在内核和应用空间进行内存复制。它与用户空间的void *mmap()相对应。
当用户空间调用系统调用open()打开设备文件时,系统设备的open()函数将会被调用,如果设备驱动没有实现open,这种情况设备的打开永远成功。open对应release。
poll()函数用于询问设备是否可被非阻塞的立即读写,用户空间的select()和poll()会造成进程的阻塞。
字符设备驱动的组成
驱动模块加载与卸载函数:
在字符设备驱动加载函数中应该实现设备号的申请以及cdev的注册,卸载则是设备号的释放和cdev的注销。
定义一个设备相关的结构体,包含设备的cdev,私有数据,还有锁等信息。
字符设备驱动的file_operations结构体中的成员函数
file_operations结构体的成员函数是内核虚拟文件系统与字符设备驱动的接口,是用户空间对linux系统调用最终的实施者。大多数的字符设备驱动会实现read(),write(),ioctl()。
读写函数中,filp指针是文件结构体指针,buf是用户内存空间的地址,count是数据的字节数,ops是相对于文件开头的偏移。
copy_to_user和copy_from_user返回不能被返回的字节数。若是成功,就返回0,不成功就返回负值。
若复制简单变量,可以通过get_user(内核变量,用户空间地址)和 put_user(内核变量,用户空间地址)。
根据主次设备号生成一个设备号devno,然后注册设备号,可以静态或动态分配。用获取的设备号来获取主设备号。调用kzmalloc()申请关于dev结构体的内存。然后初始化cdev,添加cdev到内核。
进行住注销的时候,就逆向消除。
并发控制
原子操作(atomic):只能对整数操作
设置原子变量的值
void atomic_set(atomic_t *v, int i); //设置原子变量的值为i
atomic_t v = ATOMIC_INIT(0);//初始化原子变量为0
获取原子变量的值
atomic_read(atomic_t *v);//返回原子变量的值
原子变量加减
void atomic_add(int i, atomic_t *v);//使原子变量增加i
void atomic_sub(int i, atomic_t *v);//使原子变量减少i
原子变量自加自减
void atomic_inc(atomic_t *v);//原子变量增加1
void atomic_dec(atomic_t *v);//原子变量减少1
操作并测试(自加自减和减后原子变量是否为0,是返回true,不是返回false)
void atomic_inc_and_test(atomic_t *v);
void atomic_dec_and_test(atomic _t *v);
void atomic_sub_and_test(int i, atomic_t *v);
操作并返回(自加自减和加减后原子变量返回新的值)
void atomic_inc_and_return(atomic_t *v);
void atomic_dec_and_return(atomic _t *v);
void atomic_sub_and_return(int i, atomic_t *v);
void atomic_add_and_return(int i, atomic_t *v);
位原子操作
设置位
void set_bit(nr, void *addr);//将addr地址的nr位写为1
清除位
void clear_bit(nr, void *addr);//将addr地址的nr位写为0
改变位
void change_bit(nr, void *addr);//将addr地址的nr位反置
测试位
void test_bit(nr, void *addr);//将addr地址的nr位返回
测试并操作位
void test_and_set_bit(nr, void *addr);
void test_and_clear_bit(nr, void *addr);
void test_and_change_bit(nr, void *addr);
自旋锁
定义自旋锁
spinlock_t lock;
初始化自旋锁
spin_lock_init(lock)
获得自旋锁
spin_lock(lock)//获得自旋锁,若获得马上返回,否则一直自旋,直到锁的保持者释放
spin_trylock(lock)//尝试获得自旋锁,获得返回true,失败返回false
释放自旋锁
spin_lock(lock)//与spin_lock和spin_trylock连用
整套自旋锁机制:
spin_lock_irq() 关闭中断
spin_unlock_irq() 开启中断
spin_lock_irqsave() 关中断保存状态字
spin_unlock_irqrestore() 开中断恢复状态字
spin_lock_bh() 关底半部
spin_unlock_bh() 开底半部
读写自旋锁:
rwlock_t my_rwlock()//定义读写自旋锁
rwlock_init(&my_relock)//初始化读写自旋锁
读写锁定
void read/write_lock(&myrw_lock)
void read/write_lock_irq(&myrw_lock)
void read/write_lock_irqsave(&myrw_lock, unsigned long flags)
void read/write_lock_bh(&myrw_lock)
int write_trylock(&myrw_lock)
读写解锁
void read/write_unlock(&myrw_lock)
void read/write_unlock_irq(&myrw_lock)
void read/write_unlock_irqrestore(&myrw_lock, unsigned long flags)
void read/write_unlock_bh(&myrw_lock)
顺序锁
对读写锁的一种优化,读执行单元在写执行单元对顺序锁保护的共享资源进行写操作时,仍然可以继续读,不必等待,写执行单元也不必等待读执行单元执行完毕再进行操作。
尽管不相互排斥,但如果读执行单元进行操作期间,写执行单元发生写操作,读执行单元必须重新进行写操作,以确保得到的数据是完整的。
seqlock_t *sl;
获得顺序锁:
void write_seqlock(sl);
void write_seqlock_irq/saveqrq/bh(sl);
void write_tryseqlock(sl);
释放顺序锁:
void write_sequnlock(sl);
void wrte_sequnlock_irq/bh/irqrestore(sl);
读复制更新(read-copy-update)
RCU的读端是直接读,只是简单标注读的开头和结束。RCU的写端在访问它的共享空间时,会先复制一个副本,对副本进行写的操作之后,最后使用一个回调机制在合适的时机将指向原来数据的指针重新指向新修改的数据,再释放掉原来的数据。
RCU既可以允许多个读单元同时访问被保护的数据,也允许多个读单元和多个写单元同时访问被保护的数据。但是RCU对读单元性能的提升不能弥补对写单元同步数据损失的性能。当有大量的写单元时,对于写单元的同步开销较大。
RCU操作
读锁定
rcu_read_lock()
rcu_read_lock_bh()
读解锁
rcu_read_unlock
rcu_read_unlock_bh()
同步RCU
synchronize_rcu()
函数由写执行单元执行,会阻塞所有的写执行单元。直到cpu上所有的读执行单元读完临界区,写执行单元才可以执行下一步的操作。
挂接回调
信号量
定义信号量
struct semaphore *sem;
初始化信号量
void sema_init( sem, int val);//设置信号量的初始值为val
获得信号量
void down(sem);//获得信号量,会导致睡眠,不能在中断上下文使用
void interruptible(sem)//与down类似,down的进程不能被信号打断,interruptible会被信号打断,信号也会使函数返回,返回值为0。
void down_trylock(sem);//尝试获得sem,如果能立刻获得就返回0,不能获得,就返回非0值。不会导致调用者睡眠,可以在中断上下文使用。
释放信号量
void up(sem);
互斥体
定义及初始化
struct mutex *my_mutex;
void mutex_init(my_mutex);
获取互斥体
void mutex_lock(my_mutex);
int mutex_lock_interruptible(my_mutex);
int mutex_trylock(my_mutex);
释放互斥体
void mutex_unlock(my_mutex)
互斥体和自旋锁都是解决互斥问题的手段,严格来说,他们属于不同层次的互斥手段,互斥体的实现依赖于自旋锁,自旋锁更属于底层的手段。
互斥体是进程级别的,用于多个进程对资源的互斥,虽然在内核中,但是是以进程的身份来争夺资源。竞争失败就会进行进程上下文的切换,而且进程上下文切换的资源开销也很大,所以对于互斥体来说,适用于占用资源时间比较长的进程。
当要保护资源的占用时间较少时,使用自旋锁比较适合。但是cpu得不到自旋锁就会空转直到获得自旋锁,所以占用自旋锁的进程不可以在临界区的时间过长,否则会降低系统的效率。
如果被保护的共享资源在中断或软中断情况下使用,应该去选自旋锁。
阻塞与非阻塞I/O
当应用程序进行read,write等系统调用时,若设备的资源无法获取,用户希望以阻塞的方式去访问设备,驱动程序就应该在read(),write()中将进程阻塞直到获取资源。当用户以非阻塞的方式去访问设备时,当资源无法获取时,设备的read(),write()立即返回,用户程序也立即返回,read(),write()等系统调用也立即返回,用户程序收到-EAGAIN返回值。
在阻塞访问的时候,当进程不能获取资源,就会进入休眠,并把cpu让给其他进程。当有资源时,需要唤醒休眠的进程,这个唤醒就由中断来做,当硬件资源获得的同时往往会伴随一个中断。而非阻塞的进程就会不断尝试,直到获取I/O。
等待队列
使用等待队列来实现阻塞进程的唤醒。
等待队列是用双向链表来实现的。