C2内核模块,分配设备号,字符驱动,/设备节点,设备读写,/同步和互斥,ioctl函数,进程休眠,时间和延时,延缓,/proc文件系统,内存分配,数据类型,/内核中断,通过IO内存访问外设

Posted 码农编程录

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了C2内核模块,分配设备号,字符驱动,/设备节点,设备读写,/同步和互斥,ioctl函数,进程休眠,时间和延时,延缓,/proc文件系统,内存分配,数据类型,/内核中断,通过IO内存访问外设相关的知识,希望对你有一定的参考价值。

文章目录


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内核模块,分配设备号,驱动模块,/设备节点,设备读写

Linux驱动开发基础

字符设备驱动详解(主次设备号注册/卸载字符设备驱动创建设备节点地址映射)

06 字符设备

虚拟字符设备驱动开发

字符设备驱动 —— 设备号设备节点概念辨析(转)