C2内核模块,分配设备号,字符驱动,/设备节点,设备读写,/同步和互斥,ioctl函数,进程休眠,时间和延时,延缓,/proc文件系统,内存分配,数据类型,/内核中断,通过IO内存访问外设
Posted 码农编程录
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了C2内核模块,分配设备号,字符驱动,/设备节点,设备读写,/同步和互斥,ioctl函数,进程休眠,时间和延时,延缓,/proc文件系统,内存分配,数据类型,/内核中断,通过IO内存访问外设相关的知识,希望对你有一定的参考价值。
文章目录
- 12.字符设备驱动:编译做的事:预处理(语法检查),编译(.c->.s汇编文件),汇编(.s->.o二进制文件),链接(多个.o合并成1个执行文件)
- 1.内核模块:必须包含module.h
- 2.内核模块参数:权限位是用在sysfs文件系统里,只读权限用S_IRUGO设置
- 3.设备号:主设备号对应驱动程序,所有具有相同主设备号设备使用相同驱动程序,次设备号用来标识连接系统中相同的设备
- 4.字符设备的驱动程序:控制外部设备都是通过读写dev下设备文件实现
- 5.驱动加载后自动生成设备节点:让系统udev自动生成/dev目录下的设备节点,udevadm info
- 6.实现具体的设备读写:container_of、copy_to_user、copy_from_user
- 7.同步和互斥:避免多进程同时操作设备问题
- 8.实现设备驱动的ioctl函数: nr=number
- 9.实现进程休眠:条件不够歇一歇,把CPU让给其他进程
- 10.内核表示时间和实现延时:linux中有一个时钟会周期性产生中断,linux将这中断作为时间基准
- 11.内核实现延缓操作:内核定时器,tasklet,workqueue
- 12.创建proc文件系统接口:之前调试内核时都是通过prink打印内核信息,通过dmesg查看输出的信息。新调试方法:利用proc文件系统在pro文件夹下创建接口,读写这个接口就可实现对内核的调试
- 13.内核内存分配函数:top,free,cat /pro/meminfo查看内存使用情况,cat /pro/slabinfo,cat /pro/buddyinfo,proc/sys/vm/下文件(虚拟内存更详细信息)
- 14.内核基础数据类型,移植性,数据对齐:页大小为PAGE_SIZE,不要假设4K,保证可移植性
- 15.内核中断的使用,顶半部和底半部:使用中断可实现内核和外设的异步处理,提高通讯效率,降低系统功耗
- 16.通过IO内存访问外设:有的外设将自己的寄存器映射到了物理内存某个区域,那这个区域叫做io内存区域,linux内核访问这个区域能实现对外设访问和读写
- 17.PCI设备驱动:pci是一种标准总线,基于它可以实现块设备,网络设备,字符设备
12.字符设备驱动:编译做的事:预处理(语法检查),编译(.c->.s汇编文件),汇编(.s->.o二进制文件),链接(多个.o合并成1个执行文件)
1.字符设备
:串口,键盘,鼠标,帧缓存设备(LCD)。字节流形式串行顺序进行,至少要实现open,close,read,write等系统调用。应用程序可通过/dev下的文件系统结点访问字符设备,如:/dev/led,/dev/ttySAC0。音频芯片将数字信号转化为模拟信号得到声音,数据有顺序,也是字符设备。
2.块设备
:硬盘,nandflash,SD卡,U盘。设备对数据处理按照若干块进行,一个块有固定大小,如4k(flash,页),512字节(硬盘扇区)。
3.网络设备
:网卡中以帧传播,大小变动。链路层,ip层等层层解剖,最后数据报文变数据包(loopback设备即127.0.0.1,ping 127.0.0.1 -c 1 ,1为1次,从应用层转到协议栈再转到loopback设备,协议栈功能正常,返回完整包。loopback设备还能实现同一台设备不同进程通信,通过TCP Socket绑定127.0.0.1。ifconfig lo时可看到RX和TX接在一起)。如下开发板只要连电源,显示器HDMI,串口三个就能串口登录调试。
如下是应用程序led_test.c:向设备文件(/dev/myled0)写1灯亮,写0灯灭。打开一个文件会返回文件描述符,用数组记录。
如下是编译服务器,rootfs通过NFS网络共享挂载到开发板上。进入开发板rootfs文件夹里执行./a.out(先insmod 驱动.ko)。
如下是hello.c,make后生成hello.ko,insmod
hello.ko会执行hello_init函数和main函数一样,hello_init函数里申请资源,如向内核申请内存,初始化gpio控制器,硬件初始化
(关闭看门狗即定时器
即3s没人关会重启整个系统,初始化时钟,初始化SDRAM)。应用程序退出有内核帮你释放你申请的资源,驱动程序需要hello_exit释放资源,rmmod
hello时执行hello_exit函数。
如下在内核源码中。
1.内核模块:必须包含module.h
如下查看内核版本。
编译模块之前要如下安装内核源码树目录。
如下相当于Makefile里KERNELDIR目录,其实是个软连接。
如下make后显示的第一行就是make命令。
lsmod
就是查看/pro/modules
里文件。/sys是一个基于RAM的FS,和 /proc差不多。
2.内核模块参数:权限位是用在sysfs文件系统里,只读权限用S_IRUGO设置
module_param表示声明。
如下0是cnt,1-6是数组里值。
如下nums:3是插入模块时修改了3个数组里的值。
2.1 extern:声明来自另一个模块
有时候想要在一个模块中引用另一个模块的变量或函数。
如下同一个Makefile,make命令执行。
如下insmod分先后且只能一个。
如下可以判断printp模块能不能正常被卸载。
3.设备号:主设备号对应驱动程序,所有具有相同主设备号设备使用相同驱动程序,次设备号用来标识连接系统中相同的设备
3.1 字符设备申请主设备号:三个函数,三个宏
如下还有Block devices块设备。
3.2 块设备申请主设备号:hello_major为0是系统自动分配的主设备号即register_blkdev函数分配,将ret返回值赋给hello_major
if (!hello_major)指hello_major为0。
4.字符设备的驱动程序:控制外部设备都是通过读写dev下设备文件实现
如下白色背景是申请字符设备的设备号。kzalloc:字符结构大小*分配个数(=2)。
cdev_add最后一个参数通常为1,cdev_add执行后设备就真正添加到内核中,就可以对它进行操作了。c语言不推荐goto,但内核goto灵活。
cdev_del将添加到内核的字符设备移除。
5.驱动加载后自动生成设备节点:让系统udev自动生成/dev目录下的设备节点,udevadm info
linux系统中有一个后台程序udev(用来自动生成设备节点的软件),当一个设备接入到系统中时会发送一个事件到udev
。udev接到这事件后就会获取这些设备信息,这些信息大部分保存在/sys/class/xx文件系统(sysfs)中。udev获得到这些信息后就会根据获得到的信息以及udev规则在/dev下生成对应设备节点。
如下测试读写。修改udev规则目的是让普通用户可操作设备节点。先判断SUBSYSTEM是不是=hc_dev,=的话执行后面GROUP赋值,udev根据GROUP或MODE生成节点。
6.实现具体的设备读写:container_of、copy_to_user、copy_from_user
hc_devp[n]就是定义的字符设备结构,container_of函数:通过成员(cdev)地址获取整个结构地址。
hc_read函数将字符设备中字符串给到用户空间中。
如下一个进程写没问题,但是有两进程写的话:1.
第一个进程运行到kzalloc时,第二个进程也执行了kzalloc,这种情况下只有第二个进程地址保存在c中,第一个进程分配的内存空间地址就丢失了,造成了内存泄漏。
2.
第一个进程运行到kzalloc时(第77行),第二个进程调用了kfree,这时第一个进程执行到copy_from_user会出现问题。(这两种情况统称为同步问题)。
7.同步和互斥:避免多进程同时操作设备问题
7.1 hello_chr_locked.c:读写锁、自旋锁、seqlock、原子变量同步机制
/*
信号量: sema_init() 初始化信号量
down_interruptible() 获取信号量
up() 释放信号量
互斥锁: mutex_init()
mutex_lock_interruptible() 获取
mutex_unlock()
*/
#include<linux/module.h>
#include<linux/fs.h>
#include<linux/cdev.h>
#include<linux/slab.h>
#include<linux/uaccess.h>
#include<linux/jiffies.h>
#include<linux/sched.h>
#include<linux/semaphore.h> //信号量
#include<linux/mutex.h> //互斥锁
#define LOCK_USE 1 //0:semaphore,1:mutex
#define HELLO_MAJOR 0
#define HELLO_NR_DEVS 2
int hello_major = HELLO_MAJOR;
int hello_minor = 0;
dev_t devt; //高12位是主设备号,低20位是次设备号
int hello_nr_devs = HELLO_NR_DEVS;
module_param(hello_major, int, S_IRUGO);
module_param(hello_minor, int, S_IRUGO);
module_param(hello_nr_devs, int, S_IRUGO);
struct hello_char_dev //实际的字符设备结构,类似于面向对象的继承
struct cdev cdev;
char *c;
int n;
struct semaphore sema; //信号量
struct mutex mtx; //互斥锁 ,实际两者中一个就行
;
struct hello_char_dev *hc_devp;
struct class *hc_cls;
//11111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111
int hc_open(struct inode *inode, struct file *filp)
struct hello_char_dev *hc_dev;
printk(KERN_INFO "%s open \\n",current->comm);
hc_dev = container_of(inode->i_cdev,struct hello_char_dev,cdev); //获取设备结构体的地址
filp->private_data = hc_dev; //将设备结构地址放到文件描述符结构的私有数据中
return 0;
ssize_t hc_read(struct file *filp, char __user *buf, size_t count,loff_t *f_pos)
ssize_t retval=0;
struct hello_char_dev *hc_dev=filp->private_data;
printk(KERN_INFO "read hc_dev %p\\n",hc_dev);
if(*f_pos >= hc_dev->n)
goto out;
if(*f_pos + count > hc_dev->n)
count = hc_dev->n - *f_pos;
if(copy_to_user(buf,hc_dev->c,count))
retval = -EFAULT;
goto out;
*f_pos += count;
return count;
out:
return retval;
ssize_t hc_write(struct file *filp, const char __user *buf, size_t count,loff_t *f_pos)
struct hello_char_dev *hc_dev=filp->private_data;
int retval = -ENOMEM;
unsigned long jiff1;
printk(KERN_INFO "%s write begin\\n",current->comm);
#if (LOCK_USE==0)
if(down_interruptible(&hc_dev->sema)) //-EINTR down_interruptible获得信号量或互斥锁,成功获取返回0,执行后面操作。获取不到进行等待,让进程进入休眠状态。如果我们给它发送一个信号,它就会返回一个非0值,这个值就是EINTR,如果接到EINTR就返回ERESTARTSYS,重新获取信号量。如果接到信号后退出,下行就返回EINTR。
return -ERESTARTSYS;
printk(KERN_INFO "%s get sema\\n",current->comm);
#endif
#if (LOCK_USE==1)
if(mutex_lock_interruptible(&hc_dev->mtx)) //-EINTR mutex_lock_interruptible获得互斥量,参数同样是互斥量地址,获得到互斥量返回0(不进这个if里)执行后面操作,无法获得互斥量就会让进程进入休眠状态,接收到一个信号返回非0值即-EINTR
return -ERESTARTSYS;
printk(KERN_INFO "%s get mutex\\n",current->comm);
#endif
//获得到信号量或互斥锁后就可正常执行如下写操作。
kfree(hc_dev->c);
hc_dev->c=NULL;
hc_dev->n=0;
printk(KERN_INFO"%s 1",current->comm);
jiff1=jiffies;
while(jiffies-jiff1<HZ);
hc_dev->c = kzalloc(count,GFP_KERNEL);
if(!hc_dev->c)
goto out;
printk(KERN_INFO"%s 2 addr:%p",current->comm,hc_dev->c);
jiff1=jiffies;
while(jiffies-jiff1<HZ);
printk(KERN_INFO"%s 3 addr:%p",current->comm,hc_dev->c);
if(copy_from_user(hc_dev->c,buf,count))
retval = -EFAULT;
goto fail_copy;
hc_dev->n = count;
//如上写操作执行完后,如下我们需要释放掉互斥量或互斥锁。
#if (LOCK_USE==0)
up(&hc_dev->sema); //up释放信号量,参数是信号量地址
printk(KERN_INFO "%s up sema\\n",current->comm);
#endif
#if (LOCK_USE==1)
mutex_unlock(&hc_dev->mtx); //mutex_unlock释放互斥锁,参数是互斥锁地址
printk(KERN_INFO "%s unlock mutex\\n",current->comm);
#endif
printk(KERN_INFO"%s write done",current->comm);
return count;
fail_copy:
kfree(hc_dev->c);
out:
#if (LOCK_USE==0)
up(&hc_dev->sema);
#endif
#if (LOCK_USE==1)
mutex_unlock(&hc_dev->mtx);
#endif
return retval; //不能返回0,否则会不停的写
int hc_release(struct inode *inode, struct file *filp)
printk(KERN_INFO "%s release\\n",current->comm);
return 0;
struct file_operations hc_fops = //字符设备的操作函数
.owner = THIS_MODULE,
.read = hc_read,
.write = hc_write,
.open = hc_open,
.release = hc_release,
;
//1111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111
static int __init hello_init(void)
int ret,i;
printk(KERN_INFO "---BEGIN HELLO LINUX MODULE---\\n");
if(hello_major)
devt=MKDEV(hello_major,hello_minor);
ret=register_chrdev_region(devt,hello_nr_devs,"hello_chr"); //使用指定的设备号分配
else
ret = alloc_chrdev_region(&devt,hello_minor,hello_nr_devs,"hello_chr");//动态分配主设备号
hello_major = MAJOR(devt);
if (ret < 0)
printk(KERN_WARNING "hello: can't get major %d\\n", hello_major);
goto fail;
hc_devp = kzalloc(sizeof(struct hello_char_dev)*hello_nr_devs,GFP_KERNEL); //给字符设备分配空间,这里hello_nr_devs为2
if(!hc_devp)
printk(KERN_WARNING "alloc mem failed");
ret = -ENOMEM;
goto failure_kzalloc; // 内核常用goto处理错误
for(i=0;i<hello_nr_devs;i++)
#if (LOCK_USE==0)
sema_init(&hc_devp[i].sema,1); // 初始化信号量,第一个参数:信号量地址。第二个参数:信号量个数,这里设为1,1个资源,当一个进程获得,其他进程等待。
// 信号量可有多个,有一个进程来获取,信号量个数减1。直到信号量减到0,进程无法获取信号量。当其他进程释放信号量后,被休眠的进程才可以获得信号量。
#elif (LOCK_USE==1)
mutex_init(&hc_devp[i].mtx); // 初始化互斥量, 没有个数问题,因为只有一个,一个进程获得互斥锁,其他进程只能等待。上面信号量初始化为1和这里互斥锁类似。
#endif
cdev_init(&hc_devp[i].cdev,&hc_fops); // 初始化字符设备结构
hc_devp[i].cdev.owner = THIS_MODULE;
ret = cdev_add(&hc_devp[i].cdev,MKDEV(hello_major,hello_minor+i),1);
if(ret)
printk(KERN_WARNING"fail add hc_dev%d",i);
hc_cls = class_create(THIS_MODULE,"hc_dev");
if(!hc_cls)
printk(KERN_WARNING"fail create class");
ret = PTR_ERR(hc_cls);
goto failure_class;
for(i=0;i<hello_nr_devs;i++)
device_create(hc_cls,NULL,MKDEV(hello_major,hello_minor+i),NULL,"hc_dev%d",i);
printk(KERN_INFO "---END HELLO LINUX MODULE---\\n");
return 0;
failure_class:
kfree(hc_devp);
failure_kzalloc:
unregister_chrdev_region(devt,hello_nr_devs);
fail:
return ret; //返回错误,模块无法正常加载
static void __exit hello_exit(void)
int i;
for(i=0;i<hello_nr_devs;i++)
device_destroy(hc_cls,MKDEV(hello_major,hello_minor+i));
class_destroy(hc_cls);
for(i=0;i&l以上是关于C2内核模块,分配设备号,字符驱动,/设备节点,设备读写,/同步和互斥,ioctl函数,进程休眠,时间和延时,延缓,/proc文件系统,内存分配,数据类型,/内核中断,通过IO内存访问外设的主要内容,如果未能解决你的问题,请参考以下文章
Notes13内核模块,分配设备号,驱动模块,/设备节点,设备读写