linux字符设备驱动实验

linux字符设备驱动实验

一、Linux device driver 的概念

系统调用是操作系统内核和应用程序之间的接口,设备驱动程序是操作系统内核和机器硬件之间的接口。设备驱动程序为应用程序屏蔽了硬件的细节,这样在应用程序看来,硬件设备只是一个设备文件,应用程序可以象操作普通文件一样对硬件设备进行操作。

设备驱动程序是内核的一部分,它完成以下的功能:

1. 对设备初始化和释放。
2 .把数据从内核传送到硬件和从硬件读取数据。
(3. 读取应用程序传送给设备文件的数据和回送应用程序请求的数据。)
4.检测和处理设备出现的错误。




为什么要建立设备文件


我们的linux操作系统跟外部设备(如磁盘、光盘等)的通信都是通过设备文件进行的,应用程序可以打开、关闭、读写这些设备文件,从而对设备进行读写,这种操作就像读写普通的文件一样easy。linux为不同种类的设备文件提供了相同的接口,比如read(),write(),open(),close()。

所以在系统与设备通信之前,系统首先要建立一个设备文件,这个设备文件存放在/dev目录下。




linux设备文件类型

在Linux操作系统下有三类主要的设备文件类型:

一是字符设备,二是块设备,三是网络设备。

在linux中设备文件在/dev的目录下(要分清这些设备文件的类型)
在这里插入图片描述



linux设备文件类型判断方法
Linux文件类型和文件的文件名所代表的意义是两个不同的概念,在linux中文件类型与文件扩展名没有关系。它不像Windows那样是依靠文件后缀名来区分文件类型的,在linux中文件名只是为了方便操作而的取得名字。Linux文件类型常见的有:普通文件、目录、字符设备文件、块设备文件、符号链接文件等。


1,普通文件类型

  Linux中最多的一种文件类型, 包括 纯文本文件(ASCII);二进制文件(binary);数据格式的文件(data);各种压缩文件。第一个属性为 [-] 。

2,目录文件类型


  在linux中,它的思想是一切皆是文件,目录文件也就是Windows中的目录,也就是能用 cd 命令进入的。第一个属性为 [d],例如 [drwxr-xr-x]。

3,字符设备文件


  即串行端口的接口设备,例如键盘、鼠标等等。第一个属性为 [c]。

4,块设备文件


  即存储数据以供系统存取的接口设备,简单而言就是硬盘。例如一号硬盘的代码是 /dev/hda1等文件。第一个属性为 [b]。

5,套接字文件


  这类文件通常用在网络数据连接。可以启动一个程序来监听客户端的要求,客户端就可以通过套接字来进行数据通信。第一个属性为 [s],最常在 /var/run目录中看到这种文件类型。

6,管道文件


  FIFO也是一种特殊的文件类型,它主要的目的是,解决多个程序同时存取一个文件所造成的错误。FIFO是first-in-first-out(先进先出)的缩写。第一个属性为 [p]。

7,链接文件


  类似Windows下面的快捷方式。第一个属性为 [l],例如 [lrwxrwxrwx]。



mknod命令创建设备文件

其实系统默认情况下就已经生成了很多设备文件,但有时候我们需要自己手动新建一些设备文件,这个时候就会用到像mknod这样的命令。

   mknod 的标准形式为:       mknod DEVNAME {b | c}  MAJOR  MINOR

   1,DEVNAME是要创建的设备文件名,如果想将设备文件放在一个特定的文件夹下,
   就需要先用mkdir在dev目录下新建一个目录;

   2, b和c 分别表示块设备和字符设备:

              b表示系统从块设备中读取数据的时候,
              直接从内存的buffer中读取数据,而不经过磁盘;

              c表示字符设备文件与设备传送数据的时候是以字符的形式传送,
              一次传送一个字符,比如打印机、终端都是以字符的形式传送数据;

   3,MAJOR和MINOR分别表示主设备号和次设备号:
		
         为了管理设备,系统为每个设备分配一个编号,一个设备号由主设备号和次设备号组成。
         主设备号标示某一种类的设备,次设备号用来区分同一类型的设备。
         linux操作系统中为设备文件编号分配了32位无符号整数,
         其中前12位是主设备号,后20位为次设备号,
         所以在向系统申请设备文件时主设备号不好超过4095,次设备号不好超过2^20 -1。

下面,我们就可以用mknod命令来申请设备文件了。

       mknod /dev/test c 128 512





二、最简单的字符设备驱动程序

需要用到make命令
安装GCC、G++后才有make命令

首先介绍一下头文件

#include <asm/uaccess.h>

#include <linux/module.h>

#define THIS_MODULE (&__this_module)
上面这一段定义了一些版本信息,虽然用处不是很大,但也必不可少。
#include <linux/types.h> //基本的类型定义

#include <linux/fs.h> //文件系统

#include <linux/mm.h> //内存管理

#include <linux/errno.h> //错误码

#include <asm/segment.h>// 汇编语言的一段程序

编写驱动程序testDriver.c

#include <linux/types.h> //基本的类型定义
#include <linux/fs.h> //文件系统
#include <linux/mm.h> //内存管理
#include <linux/errno.h> //错误码
#include <asm/segment.h>// 汇编语言的一段程序
#include <asm/uaccess.h>
#include <linux/module.h>
#define THIS_MODULE (&__this_module)
unsigned int test_major = 0; //定义主设备号(通过系统自动获取主设备号)
//定义一个读函数,对应read系统调用。(static表示该函数在本文件有效)
char temp[64]= {
    
    0};
static int read_test(struct file *file, char *buf, int count, loff_t *offt)
{
    
    
	int left; 

	if (access_ok(VERIFY_WRITE, buf, count) == -EFAULT ) //验证是否可以向buf中写入数据。
		return -EFAULT; 
		
	if(strlen(temp)==0){
    
    
	printk("temp is empty");
	return count;
	} 

	if(copy_to_user(buf,temp,count)){
    
    
	printk("copy_to_user is OK ");
	printk(KERN_EMERG "test: OK %d\n",count);
	}
	else{
    
    
	return count;}

	return count; 
}



static int write_test( struct file *file, const char *buf, int count ,struct inode *inode)
{
    
    
	if (access_ok(4, buf, count) == -EFAULT ) //验证buf是否可以读。
		return -EFAULT;
	else printk("read_ok\n");

	if(copy_from_user(temp,buf,count))
		return -EFAULT;
	else {
    
    
		printk("kernelSpace temp is : %s\n",temp);
		printk(KERN_EMERG "testwriter: OK %d\n",count);
	}
	return count;
}

static int open_test(struct inode *inode, struct file *file )
{
    
    
	//MOD_INC_USE_COUNT; //注册到内核后,模块计数加一
	try_module_get(THIS_MODULE);
	return 0;
}



static void release_test(struct inode *inode, struct file *file )
{
    
    
	//MOD_DEC_USE_COUNT; //模块数减一
	module_put(THIS_MODULE);
}



struct file_operations test_fops =
{
    
    
	.owner = THIS_MODULE,
	.read = read_test,
	.write = write_test,
	.open  = open_test,
	.release = release_test
};



int init_module1(void)
//int module_iit(void)
{
    
    
	int result;
	result = register_chrdev(0, "test", &test_fops); //注册字符型设备到内核中
	if (result < 0)
	{
    
    
		printk(KERN_INFO "test: can't get major number\n"); //KERN_INFO打印信息
		return result;
	} else {
    
    
		printk("test get success major\n");
	}
	if (test_major == 0)
		test_major = result; /* 获取系统默认的主设备号 */
	if(test_major == 0) {
    
    
		printk("test_major == 0\n");
	} else {
    
    
		printk("test_major != 0 test_major = %d \n",test_major);
	}
	return 0;
}
void cleanup_module1(void)
//void module_exit(void)
{
    
    
	unregister_chrdev(test_major,"test");
//	printk(“%u has been relased”,test_major);
}
module_init(init_module1);
module_exit(cleanup_module1);
每个程序都有入口和出口,我们写的C语言程序入口函数是main函数,那么驱动程序也有自己的入口和出口; module_init(init_module1);
module_exit(cleanup_module1);
这两个函数就本别是驱动程序的入口和出口函数;
入口函数调用了init_module1()函数它的作用是初始化驱动程序,其做了两件事情:
1.注册字符型设备到内核中
2. 获取系统默认的主设备号





linux的设备驱动程序工作的基本原理

由于用户进程是通过设备文件同硬件打交道,对设备文件的操作方式不外乎就是一些系统调用,如 open,read,write,close…, 注意,不是fopen, fread,但是如何把系统调用和驱动程序关联起来呢?这需要了解一个非常关键的数据结构:
struct file_operations test_fops =
{
.owner = THIS_MODULE,
.read = read_test,
.write = write_test,
.open = open_test,
.release = release_test
};
这个结构的每一个成员的名字都对应着一个系统调用。用户进程利用系统调用在对设备文件进行诸如read/write操作时,系统调用通过设备文件的主设备号找到相应的设备驱动程序,然后读取这个数据结构相应的函数指针,接着把控制权交给该函数。这是linux的设备驱动程序工作的基本原理。




编写设备驱动程序的功能

既然是这样,则编写设备驱动程序的主要工作就是编写子函数,并填充file_operations的各个域。


init_module1(初始化功能)

我们查看(dmesg查看内核日志)获取到的系统默认主设备号来mknod相应的设备文件; 这里注册的字符设备叫“test”
int init_module1(void)
//int module_init(void)
{
    
    
	int result;
	result = register_chrdev(0, "test", &test_fops); //注册字符型设备到内核中
	if (result < 0)
	{
    
    
		printk(KERN_INFO "test: can't get major number\n"); //KERN_INFO打印信息
		return result;
	} else {
    
    
		printk("test get success major\n");
	}
	if (test_major == 0)
		test_major = result; /* 获取系统默认的主设备号 */
	if(test_major == 0) {
    
    
		printk("test_major == 0\n");
	} else {
    
    
		printk("test_major != 0 test_major = %d \n",test_major);
	}
	return 0;

}




read_test功能

read是用户把内核空间的内容读到用户空间中需要用到put_user函数
static int read_test(struct file *file, char *buf, int count, loff_t *offt)
{
    
    
	int left; 

	if (access_ok(VERIFY_WRITE, buf, count) == -EFAULT ) //验证是否可以向buf中写入数据。
		return -EFAULT; 
		
	if(strlen(temp)==0){
    
    
	printk("temp is empty");
	return count;
	} 

	if(copy_to_user(buf,temp,count)){
    
    
	printk("copy_to_user is OK ");
	printk(KERN_EMERG "test: OK %d\n",count);
	}
	else{
    
    
	return count;}

	return count; 
}

linux中printf函数是把内容输出到标准输出文件中,printk则是把内容输出到内核日志中 ;
我们可以用dmesg命令查看内核日志的信息

Linux dmesg命令

语法

dmesg [-cn][-s <缓冲区大小>]

参数说明:

-c  显示信息后,清除ring buffer中的内容。

-s<缓冲区大小>  预设置为8196,刚好等于ring buffer的大小。

-n  设置记录信息的层级。





write_test功能
write是写,把用户空间的内容写到内核空间里,这里我没有用get_user函数,用的是copy_from_user(temp,buf,count)函数,其含义是把用户空间的buf的count个字符拷贝到内核空间的temp中。

static int write_test( struct file *file, const char *buf, int count ,struct inode *inode)
{
    
    
	if (access_ok(4, buf, count) == -EFAULT ) //验证是否可以向buf中写入数据。
		return -EFAULT;
	else printk("read_ok\n");
	if(copy_from_user(temp,buf,count))
		return -EFAULT;
	else {
    
    
		printk("kernelSpace temp is : %s\n",temp);
		printk(KERN_EMERG "testwriter: OK %d\n",count);
	}
	return count;
}




三、测试驱动程序:testRead.c


这里的open打开的是字符设备驱动文件test文件

gcc 编译testRead.c生成可执行程序testRead

gcc testRead.c -o testRead

#include <stdio.h> 

#include <sys/types.h> 

#include <sys/stat.h> 

#include <fcntl.h> 

int main() 

{
    
     

	int testdev; 

	int i; 

	char buf[20]; 

	testdev = open("/dev/test",O_RDWR| O_NONBLOCK); 

	if ( testdev == -1 ) 

	{
    
     

		printf("Cann't open file \n"); 
		perror("error is :");

		exit(0); 

	} 
	memset(buf,0,sizeof(buf));

	read(testdev,buf,20); 

	printf("%s\n",buf); 

	close(testdev); 

	return 0;

} 





四、测试驱动程序:testWrite.c


这里的open打开的是字符设备驱动文件test文件
gcc 编译testWrite.c生成可执行程序testWrite

gcc testWrite.c -o testWrite

#include <stdio.h> 
#include <sys/types.h> 
#include <sys/stat.h> 
#include <fcntl.h> 
int main() 
{
    
     
	int testdev; 
	int i; 
	char buf[20] = "Hello World!";
	testdev = open("/dev/test",O_RDWR); 
	if ( testdev == -1 ) 
	{
    
     
		perror("error is :");
		exit(0); 
	} 
	write(testdev,buf,strlen(buf)); 
	close(testdev); 
	return 0;
} 





五、Makefile文件


#KERNELDIR := /lib/modules/$(shell uname -r)/build
PWD := $(shell pwd)

obj-m := -DEXPORT_SYMTAB
obj-m := testDriver.o

#在编绎内核本身export-objs中的文件时,make会增加一个"-DEXPORT_SYMTAB"编绎标志,它使源文件嵌入mo#dversions.h文件,将EXPORT_SYMBOL宏展开中的函数名字符串进行版本名扩展;同时,它也定义_set_ver()宏#为一空操作,使代码中的函数名不受其影响。  
#在编绎模块时,make会增加"-include=linux/modversion.h -DMODVERSIONS"编绎标志,使模块中代码的函##数名得到相应版本扩展。

all:
	make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules

clean:
	make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean

obj-m := testDriver.o可以自己寻找到testDriver.c但是要保证.c和.o的文件名字一样,并且Makefile和testDriver.c在同一目录下,不然make不会成功;
make编译完成以后会生成testDriver.ko 文件使用insmod命令把它挂到系统中,再创建字符驱动文件test,之后要chmod修改test的文件权限777;


六、RedHat演示

如果第一次做则不需要做第一步和第三步



1. 首先用rmmod把我之前insmod挂载的驱动testDriver.ko 卸载掉


在这里插入图片描述

2. 把内核日志清空dmesg -c

在这里插入图片描述
会打印出来很多日志信息
在这里插入图片描述

3. 用rm -f把我之前mknod的字符设备文件test删除

查看test: ls /dev
在这里插入图片描述

删除它
在这里插入图片描述

5. make编译Makefile生成testDriver.ko文件

在Makefile和testDriver所在的目录中打开终端,或者cd到这个目录下
在这里插入图片描述
make编译
在这里插入图片描述
如果有警告不用理会

在这里插入图片描述

6. insmod挂载驱动testDriver.ko

在这里插入图片描述

7. lsmod查看挂载的驱动

在这里插入图片描述

8. dmesg查看内核日志的主设备号test_major

在这里插入图片描述

9. mknod创建字符设备驱动文件test

在这里插入图片描述

10. chmod修改字符设备驱动文件test权限为777

在这里插入图片描述

11. 测试testRead -> gcc编译testRead.c生成testRead

在这里插入图片描述

12. 运行testRead,并查看内核日志信息

在这里插入图片描述

13. 测试testWrite -> gcc编译testWrite.c生成testWrite

在这里插入图片描述

14. 运行testWrite,并查看内核日志dmesg

在这里插入图片描述

15. 卸载驱动、删除设备文件

在这里插入图片描述

到这里就全部演示完毕
在这里插入图片描述

猜你喜欢

转载自blog.csdn.net/Lazy_Goat/article/details/116398067