Linux 字符设备驱动及一些简单的Linux知识

Posted 袁233

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Linux 字符设备驱动及一些简单的Linux知识相关的知识,希望对你有一定的参考价值。

一、linux系统将设备分为3类:字符设备、块设备、网络设备

1、字符设备:是指只能一个字节一个字节读写的设备,不能随机读取设备内存中的某一数据,读取数据需要按照先后数据。字符设备是面向流的设备,常见的字符设备有鼠标、键盘、串口、控制台和LED设备等。
2、块设备:是指可以从设备的任意位置读取一定长度数据的设备。块设备包括硬盘、磁盘、U盘和SD卡等。

每一个字符设备或块设备都在/dev目录下对应一个设备文件。linux用户程序通过设备文件(或称设备节点)来使用驱动程序操作字符设备和块设备。

本文主要是介绍字符设备驱动程序,从驱动程序开始,涉及文件操作,一共四个函数:包括文件的打开,读,写,删除。还有文件的注册和注销。

废话不多说,上源代码,以代码为例开始解释:

1.mydriver.c:

#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/init.h>
#include <linux/uaccess.h>

#if CONFIG_MODVERSIONS == 1
#define MODVERSIONS
#include <linux/version.h>
#endif

#define DEVICE_NUM 0 //随机产生一个设备号

static int device_num = 0; //用来保存创建成功后的设备号
static char buffer[1024] = "mydriver"; //数据缓冲区
static int open_nr = 0; //打开设备的进程数,用于内核的互斥

//函数声明
// inode;linux下文件的管理号。
//file:linux一切皆文件。文件结构体代表一个打开的文件,系统中的每个打开的文件在内核空间都有一个关联的 struct file。它由内核在打开文件时创建,并传递给在文件上进行操作的任何函数。在文件的所有实例都关闭后,内核释放这个数据结构
static int mydriver_open(struct inode *inode, struct file *filp);
static int mydriver_release(struct inode *inode, struct file* filp);
static ssize_t mydriver_read(struct file *file, char __user *buf, size_t count, loff_t *f_pos); //loff-t:long long 型
static ssize_t mydriver_write(struct file *file, const char __user *buf, size_t count, loff_t *f_pos); //__user表明参数是一个用户空间的指针,不能在kernel代码中直接访问。
//size_t:一个基本的无符号整数的C / C + +类型
//填充file_operations结构相关入口
static struct file_operations mydriver_fops = {
.read = mydriver_read,
.write = mydriver_write,
.open = mydriver_open,
.release = mydriver_release,
};

//打开函数
static int mydriver_open(struct inode *inode, struct file *filp)
{
printk("\nMain device is %d, and the slave device is %d\n", MAJOR(inode->i_rdev), MINOR(inode->i_rdev)); //把主从设备号传入
if (open_nr == 0) {
open_nr++;
try_module_get(THIS_MODULE); //尝试打开模块
return 0;
}
else {
printk(KERN_ALERT "Another process open the char device.\n");//进程挂起
return -1;
}
}

//读函数
static ssize_t mydriver_read(struct file *file, char __user *buf, size_t count, loff_t *f_pos)
{
//if (buf == NULL) return 0;
if (copy_to_user(buf, buffer, sizeof(buffer))) //读缓冲 ,第一个参数是to:用户空间的地址,第二个参数是from,内核空间的地址,第三个参数是要从内核空间拷贝的字节数
{
return -1;
}
return sizeof(buffer);
}

//写函数,将用户的输入字符串写入
static ssize_t mydriver_write(struct file *file, const char __user *buf, size_t count, loff_t *f_pos)
{
//if (buf == NULL) return 0;
if (copy_from_user(buffer, buf, sizeof(buffer))) //写缓冲
{
return -1;
}
return sizeof(buffer);
}

//释放设备函数
static int mydriver_release(struct inode *inode, struct file* filp)
{
open_nr--; //进程数减1
printk("The device is released!\n");
module_put(THIS_MODULE); //释放模块
return 0;
}

//注册设备函数
static int __init mydriver_init(void)
{
int result;

printk(KERN_ALERT "Begin to init Char Device!"); //注册设备
//向系统的字符登记表登记一个字符设备
result = register_chrdev(DEVICE_NUM, "mydriver", &mydriver_fops); //第一个参数等于0,则表示采用系统动态分配的主设备号;不为0,则表示静态注册。 第二个参数命名, 第三个参数为其地址

if (result < 0) {
printk(KERN_WARNING "mydriver: register failure\n");
return -1;
}
else {
printk("mydriver: register success!\n");
device_num = result;
return 0;
}
}

//注销设备函数
static void __exit mydriver_exit(void)
{
printk(KERN_ALERT "Unloading...\n");
unregister_chrdev(device_num, "mydriver"); //注销设备
printk("unregister success!\n");
}

//模块宏定义
module_init(mydriver_init); //模块加载函数
module_exit(mydriver_exit); //设备卸载函数

MODULE_LICENSE("GPL"); // "GPL" 是指明了 这是GNU General Public License的任意版本

因为注释内容有限,在后面贴上一些对源代码的注释:

inode;linux下文件的管理号。(静态的)
file:linux一切皆文件。文件结构体代表一个打开的文件,系统中的每个打开的文件在内核空间都有一个关联的 struct file。它由内核在打开文件时创建,并传递给在文件上进行操作的任何函数。在文件的所有实例都关闭后,内核释放这个数据结构。(动态的)
__user:The use of char __user *buf is typically found in the linux kernel...denoting that this address is in the user space. For example when writing to disk the kernel copies the contents of *buf into a kernel space buffer before writing it out to disk. Similarly when a process requests a read operation...the device driver at the behest of the kernel reads the desired disk blocks into a kernel space buffer...and then copies them into the user space buffer pointed to by *buf.
__user表明参数是一个用户空间的指针,不能在kernel代码中直接访问。也方便其它工具对代码进行检查。
file-operations:
用来存储驱动内核模块提供的 设备进行各种操作的函数的指针。该结构体的每个域都对应着驱动内核模块用来处理某个被请求的事务的函数的地址。
MAJOR(inode->i_rdev), MINOR(inode->i_rdev):
如果inode代表一个设备,则i_rdev的值为设备号。为了代码更好地可移植性,获取inode的major和minor号应该使用imajor和iminor函数
#include <linux/module.h> :
写内核驱动的时候 必须加载这个头文件,作用是动态的将模块加载到内核中去
#include <linux/version.h> :
当设备驱动需要同时支持不同版本内核时,在编译阶段,内核模块需要知道当前使用的内核源码的版本,从而使用相应的内核 API
try_module_get(&module), module_put(&module):
灵活的模块计数管理接口
#include <linux/kernel.h> :
kernel.h中包含了内核打印函数 printk函数 等
#include <linux/uaccess.h>:
包含了copy_to_user、copy_from_user等内核访问用户进程内存地址的函数定义。(copy_to_user()完成用户空间到内核空间的复制,函数copy_from_user()完成内核空间到用户空间的复制)
copy_to_user(buf, buffer, sizeof(buffer)//
#include <linux/fs.h> :
包含了文件操作相关struct的定义,例如大名鼎鼎的struct file_operations

#include <linux/init.h> :
内核模块的初始化和注销函数就在这个文件中

2. makefile:

# if KERNELRELEASE is defined, we‘ve been invoked from the

# kernel build system and can use its language.

ifeq ($(KERNELRELEASE),)

    # Assume the source tree is where the running kernel was built

    # You should set KERNELDIR in the environment if it‘s elsewhere

KERNELDIR ?=  /lib/modules/$(shell uname -r)/build

    # The current directory is passed to sub-makes as argument

PWD := $(shell pwd)

modules:

$(MAKE) -C $(KERNELDIR) M=$(PWD) modules

modules_install:

$(MAKE) -C $(KERNELDIR) M=$(PWD) modules_install

clean:

rm -rf *.o *~ core .depend .*.cmd *.ko *.mod.c .tmp_versions

.PHONY: modules modules_install clean

else

    # called from kernel build system: just declare what our modules are

obj-m := mydriver.o

endif

3.

  1. test.c

#include <sys/types.h>

#include <sys/stat.h>

#include <stdlib.h>

#include <string.h>

#include <stdio.h>

#include <fcntl.h>

#include <unistd.h>

#define MAX_SIZE 1024

 

int main(void)

{

int fd;

char buf[MAX_SIZE];

char get[MAX_SIZE];

char devName[20], dir[49] = "/dev/";

system("ls /dev/");

printf("Please input the device‘s name you wanna to use :");

gets(devName);

strcat(dir, devName);

fd = open(dir, O_RDWR | O_NONBLOCK);

if (fd != -1)

{

read(fd, buf, sizeof(buf));

printf("The device was inited with a string : %s\n", buf);

 // 测试写

printf("Please input a string  :\n");

gets(get);

write(fd, get, sizeof(get));

//测试读

read(fd, buf, sizeof(buf));

system("dmesg");

printf("\nThe string in the device now is : %s\n", buf);

close(fd);

return 0;

}

else

{

printf("Device open failed\n");

return -1;

}

}

以上源代码均可直接使用,以下是本人对基础知识的一些拙见,如果您有什么高见请务必指出,多谢各位大佬!

一.什么是Makefile
(1)KERNELRELEASE在linux内核源代码中的顶层makefile中有定义
(2)shell pwd会取得当前工作路径
(3)shell uname -r会取得当前内核的版本号
(4)由于make 后面没有目标,所以make会在Makefile中的第一个不是以.开头的目标作为默认的目标执行。
于是modules成为make的目标
(5)首先-C表示到存放内核的目录(KERNELDIR)执行其makefile,其中保存有内核的顶层makefile,M=选项的作用是,
当用户需要以某个内核为基础编译一个外部模块的话,需要在make modules 命令中加入M=$(PWD),
程序会自动到你所指定的dir目录中查找模块源码,将其编译,生成KO文件。
(6)obj-m是表示该文件要作为模块编译,只是编译得到globalmem.o而不链接进内核
(7)每个Makefile中都应该写一个清空目标文件(.o和执行文件)的规则,这不仅便于重编译
,也很利于保持文件的清洁——clean
(8)make modules_install是把编译好的模块拷贝到系统目录下(一般是/lib/modules/),
拷贝到系统目录下的目的是方便使用,加载驱动就使用modprobe globalmem命令,该命令从系统目录下查找名为globalmem的模块

二.什么是模块化编程?为什么要模块化编程?
(1)内核模块是一些可以让操作系统内核在需要时载入和执行的代码,同时在不需要的时候可以卸载。
这是一个好的功能,扩展了操作系统的内核功能,却不需要重新启动系统,是一种动态加载的技术。
(2)内核模块代码运行在内核空间,而应用程序在用户空间。应用程序的运行会形成新的进程,而内核模块一般不会
。每当应用程序执行系统调用时,Linux执行模式从用户空间切换到内核空间。
答:Linux 内核的整体结构非常庞大,其包含的组件也非常多。我们怎样把需要的部分都包含在内核中呢?
一种方法是把所有需要的功能都编译到 Linux 内核。
这会导致两个问题,一是生成的内核会很大,二是如果我们要在现有的内核中新增或删除功能,将不得不重新编译内核。
有没有一种机制使得编译出的内核本身并不需要包含所有功能,而在这些功能需要被使用的时候,
其对应的代码可被动态地加载到内核中呢?
Linux 提供了这样的一种机制,这种机制被称为模块(Module),可以实现以上效果。模块具有以下特点。
1.模块本身不被编译入内核映像,从而控制了内核的大小。
2.模块一旦被加载,它就和内核中的其他部分完全一样。

三.如何理解设备号,什么是主设备号,什么是次设备号?

一个字符设备或块设备都有一个主设备号和一个次设备号。主设备号用来标识与设备文件相连的驱动程序,用来反映设备类型。次设备号被驱动程序用来辨别操作的是哪个设备,用来区分同类型的设备。
  linux内核中,设备号用dev_t来描述,2.6.28中定义如下:
  typedef u_long dev_t;
  在32位机中是4个字节,高12位表示主设备号,低12位表示次设备号。

四.整个驱动从编译到用测试程序进行测试的全过程,需要掌握相关的各种操作命令
1.在globalmen这个目录里面,先make一下,把globalmem.c编译成模块globalmem.ko
2.lsmod查看这个Linux里面有没有globalmem模块
3.insmod globalmem.ko 加载globalmem模块
4.lsmod查看是否加载成功
5.cat /proc/devices查看加载驱动程序时生成的设备 (proc目录是一个虚拟文件系统,
可以为linux用户空间和内核空间提供交互,它只存在于内存中,而不占实际的flash或硬盘空间)
6.mknod /dev/globalmem c 200 0 给globalmem设备创建设备节点,主设备号为200,次设备号为0
7.cd /dev 进入dev目录
8.ls 查看dev目录下是否有globalmem设备
9.echo ‘老师你真的很帅!‘>/dev/globalmem 向globalmem设备写入一句话
10.cat /dev/globalmem 查看globalmem设备里面的内容
11.gcc -o test.c test 用gcc来编译测试程序
12../test 运行测试程序

五.三个重要的数据结构(结构体):file_operations、innode、file,尤其是file_operations
file文件结构:
struct file,定义于<linux/fs.h>.文件结构代表一个打开的文件.(它不特定给设备驱动;
系统中每个打开的文件有一个关联的 struct file 在内核空间).
它由内核在 open 时创建,并传递给在文件上操作的任何函数, 直到最后的关闭.
在文件的所有实例都关闭后, 内核释放这个数据结构
.struct file的指针常常称为filp("file pointer"),源代码里面没有这个结构体,
但是在函数里面会经常用到它,例如
int globalmem_open(struct inode *inode, struct file *filp);
int globalmem_release(struct inode *inode, struct file *filp);
static ssize_t globalmem_read(struct file *filp, char __user *buf, size_t size, loff_t *ppos);
static ssize_t globalmem_write(struct file *filp, const char __user *buf, size_t size, loff_t *ppos);
static loff_t globalmem_llseek(struct file *filp, loff_t offset, int orig);
static long globalmem_ioctl(struct file *filp, unsigned int cmd, unsigned long arg);

inode结构:
inode 结构由内核在内部用来表示文件. 因此, 它和代表打开文件描述符的文件结构是不同的.
可能有代表单个文件的多个打开描述符的许多文件结构, 但是它们都指向一个单个 inode 结构.
inode 结构包含大量关于文件的信息. 作为一个通用的规则, 这个结构只有 2 个成员对于编写驱动代码有用:
dev_ti_rdev;
对于代表设备文件的节点,这个成员包含实际的设备编号.
structcdev *i_cdev;
structcdev 是内核的内部结构, 代表字符设备; 这个成员包含一个指针, 指向这个结构, 当节点指的是一个字符设备文件时.

file_operations结构:
结构体file_operations在头文件 linux/fs.h中定义,用来存储驱动内核模块提供的对设备进行各种操作的函数的指针。
该结构体的每个域都对应着驱动内核模块用来处理某个被请求的 事务的函数的地址。
如下为C99语法的使用该结构体的方法,并且没有显示声明的结构体成员都被gcc初始化为NULL
static const struct file_operations globalmem_fops = {
.owner = THIS_MODULE,
.llseek = globalmem_llseek,
.read = globalmem_read,
.write = globalmem_write,
.unlocked_ioctl = globalmem_ioctl,/* 在2.6.x的内核版本中,文件操作结构体中,
才会有ioctl的字段,高版本中使用unlocked_ioctl */
.open = globalmem_open,
.release = globalmem_release,
};

 

以上是关于Linux 字符设备驱动及一些简单的Linux知识的主要内容,如果未能解决你的问题,请参考以下文章

4412开发板Linux系统编程实战-字符设备控制

linux基础知识及结构命令

Linux--磁盘管理

linux驱动工程面试必问知识点

深入理解Linux字符设备驱动

深入理解Linux字符设备驱动