操作系统-5.3 设备驱动

174 阅读4分钟

驱动程序与应用程序

  • 驱动程序运行在内核层,应用程序运行在应用层
  • 驱动程序没有main函数, 包含提供给驱动开发人员搭建驱动的统一框架module_init module_exit,提供给应用程序使用的功能函数(这些函数包含在struct file_operations, 这个结构体中有很多的函数指针,一个函数指针对应一个功能函数) 所以,驱动程序可以给应用程序屏蔽硬件细节
  • 内核是c语言写的,60%的代码是驱动程序
  • 应用程序的标准头文件在/usr/include文件夹下,内核的标准头文件在/usr/src/linux-headers-3.13.0-24-generic/include下

设备类型

字符设备 char device: 大部分驱动程序都是字符设备,以字符为单位进行输入输出 块设备 block device: 硬盘 内存等,以块(通常为4kb)为单位进行输入输出 网络设备 net device: 网卡

驱动程序相关的命令

  • 编译命令make,生成内核驱动程序文件.ko
  • 将驱动程序加载到内核: insmod xxx.ko
  • 查看内核中的驱动程序: lsmod
  • 删除内核驱动程序: rmmod xxx.ko
  • 创建设备文件:mknod 文件名 c 主设备号 次设备号

Makefile: hello.o为驱动程序代码文件

ifeq ($(KERNELRELEASE),)

KERNELDIR ?= /lib/modules/$(shell uname -r)/build
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 m* Mo*

.PHONY: modules modules_install clean

else
    obj-m := hello.o
endif

第一个简单的驱动程序

hello.c

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

// 开源协议声明
MODULE_LICENSE ("GPL");

static int  __init hello_2_init (void)
{
	printk (KERN_INFO "hello driver init.\n");
	return 0;
}

static void  __exit hello_2_exit (void)
{
	printk (KERN_INFO "hello driver exit.\n");
}

module_init (hello_2_init);// module_init  insmod时执行的功能函数
module_exit (hello_2_exit);// module_exit  rmmod时执行的功能函数
make
insmod hello.ko
rmmod hello.ko
dmesg -T

字符设备驱动

描述字符设备的结构体cdev /usr/src/linux-headers-3.13.0-24-generic/include/linux/cdev.h

struct cdev {
	struct kobject kobj;
	struct module *owner;
	const struct file_operations *ops;  //open close read write函数
	struct list_head list;
	dev_t dev;  //设备号 typedef unsigned int dev_t
	unsigned int count;
};

设备号就是一个无符号的整数 unsigned int, 32bit 设备号由两部分组成: 主设备号(设备的类型)占用高12位、次设备号(同类型设备采用次设备号来区别)占用低20位

file_operations

  • open
  • close
  • read
  • write
#include "linux/kernel.h"
#include "linux/module.h"
#include "linux/init.h"
#include "linux/cdev.h"
#include "linux/kdev_t.h"
#include "linux/fs.h"
#include "asm/uaccess.h"
MODULE_LICENSE ("GPL");

struct cdev mydev;
dev_t dev;
//主设备号100, 次设备号1
int major = 100, minor = 1;
//内核缓冲区
char kernel_buf[128] = {0};

int myopen(struct inode *pnode,struct file *pfile)
{
	printk(KERN_INFO "myopen function exec\n");
	return 0;
}
int myclose(struct inode *pnode,struct file *pfile)
{
	
	printk(KERN_INFO "myclose function exec\n");
	return 0;
}
ssize_t mywrite (struct file *pfile, char __user *buf, size_t len, loff_t * plofft)
{
    int ret;
    if (len > 128)
    {
        len = 128;
    }    
    printk(KERN_INFO "mywrite function exec, %s, len = %d\n", buf, len);
    ret = copy_from_user(kernel_buf, buf, len);//返回还未拷贝的字节数
    printk(KERN_INFO "ret = %d\n", ret);
	return len - ret;//返回内核拷贝了多少字节数
}
ssize_t myread(struct file *pfile, char __user *buf, size_t len, loff_t * plofft)
{
    int ret;
    char buffer[] = "this is kernel data.";
    ret = copy_to_user(buf, buffer, sizeof(buffer)-1);
    printk(KERN_INFO "myread function exec, ret = %d\n", ret);
	return sizeof(buffer)-1 - ret;
}
struct file_operations myops=
{
	.open  = myopen,
	.release = myclose,
	.write = mywrite,
	.read = myread,
};

static int __init myinit(void)
{
	int ret;
    //( (major & 0xFFF) << 20 ) | minor & 0xFFFFF;
	dev = MKDEV(major,minor);
    //向内核申请检测设备号是否可以使用,第一个参数为设备号,第二个参数为设备个数,第三个参数为设备名称
    //成功返回0,失败返回-1
	ret = register_chrdev_region(dev,1,"hello"); 
	if(ret < 0)
	{
       // 向内核申请动态分配设备号,dev接收内核动态分配设备号指针,第二个参数为次设备号
       // 成功返回0,失败返回-1
       ret = alloc_chrdev_region(&dev,1,1,"hello");
       if (ret < 0)
       {
         printk(KERN_INFO "alloc dev id error\n");
         return 0;
       }
	}
    major = MAJOR(dev);
	minor = MINOR(dev);
    printk(KERN_INFO "hello.ko device major = %d,minor = %d\n",major,minor);
    //初始化cdev结构体file_operations
    cdev_init(&mydev,&myops);
    // 初始化cdev结构体成员设备号dev,将struct dev对象加载到内核中, 等同于mydev.dev = dev_id;
	cdev_add(&mydev,dev,1);
	printk(KERN_INFO "myinit function exec success.\n");
	return 0;
}
static void __exit myexit(void)
{
    cdev_del(&mydev);
    // 向内核注销设备号
	unregister_chrdev_region(dev,1);
	printk(KERN_INFO "myexit function exec\n");
	return ;
}
module_init(myinit);
module_exit(myexit);

应用程序调用驱动函数

加载驱动程序,创建设备文件

make && insmod hello.ko && mknod /dev/hello c 100 1

写应用代码,调用驱动程序

#include "unistd.h"
#include "fcntl.h"
#include "stdio.h"
#include "stdlib.h"
#include "string.h"
int main()
{
	int fd;
    char buf[128]={0};
    int len;
    
	fd = open("/dev/hello",O_RDWR,0777);
	if(fd < 0)
	{
		printf("open hello error\n");
		_exit(-1);
	}
    
    //从内核驱动中读取数据到buffer
    len = read(fd, buf, strlen(buf));
    printf("len = %d, recv from kernel = %s\n", len, buf);
    memset(buf, 0, 128);
    
    while(1)
    {
        printf("please input write to kernel data\n");
        scanf("%s", buf);
        //将终端输入数据写入内核缓冲区
        len = write(fd, buf, strlen(buf));
        printf("len = %d\n", len);
        memset(buf, 0, 128);
    }
    
	close(fd);
	return 0;
}

应用程序与内核的交互

应用程序的write与内核的write函数的交互

ssize_t mywrite (struct file *pfile, char __user *buf, size_t len, loff_t * plofft)
  • 内核的write函数的参数 第二个参数是应用程序的数据的首地址,第三个参数是应用程序的数据的长度(应用程序想写多少个字节数据到内核)

  • 内核的write函数的返回值会发送给应用程序,即是应用write函数实际写的字节数

  • 应用程序的存储空间与内核的地址是不一样的 应用层存储空间与内核层存储空间的相互复制函数 copy_from_user 将数据从应用程序空间复制到内核空间 copy_to_user 将数据从内核空间复制到应用程序空间

应用程序的read与内核的read函数的交互

ssize_t myread(struct file *pfile, char __user *buf, size_t len, loff_t * plofft)
  • 非阻塞 内核的read函数是一个非阻塞方式
  • 阻塞

内核的阻塞机制

内核的阻塞机制:当应用程序从内核读取数据时,若此时内核没有数据(硬件没有穿送数据给内核),则会让应用程序等待(睡眠),等有数据时会唤醒应用程序。 进程阻塞是通过链式队列来实现,这个队列称为等待队列。Linux内核提供了有关等待队列来实现的接口:

  • wait_queue_head_t my_queue 定义一个等待队列头,相当于链式队列的头结点

  • init_waitqueue_head(queue) 初始化队列头,相当于生成了一个空队列

  • DECLARE_WAITQUEUE(queue, tsk) 建立新节点,新节点的数据是应用程序的进程, 此时该节点还未放入等待队列 第一个参数是定义的等待队列变量,第二个参数是当前使用应用程序进程,一般使用current,current是一个默认的指针,代表的是当前用户进程

  • add_wait_queue(&queue,&node) 将新节点加入等待队列

  • wait_event_interruptible(queue, condition) 使等待队列中的进程睡眠,该睡眠可以被信号打断 第一个参数为等待队列头变量,第二个参数condition为睡眠条件,如果condition为假则一直睡眠,反之则退出睡眠 wait_event(queue, condition) 不可以被打断,kill -9也无法结束

  • wake_up_interruptible(&queue) 唤醒可被信号打断的等待队列上的睡眠进程

#include "linux/kernel.h"
#include "linux/module.h"
#include "linux/init.h"
#include "linux/cdev.h"
#include "linux/kdev_t.h"
#include "linux/fs.h"
#include "asm/uaccess.h"
#include "linux/sched.h"
MODULE_LICENSE ("GPL");

struct cdev mydev;
dev_t dev;
//主设备号100, 次设备号1
int major = 100, minor = 1;
//内核缓冲区
char kernel_buf[128] = {0};
//定义等待队列头结点
wait_queue_head_t my_queue;
//定义等待队列新节点
wait_queue_t new_node;
int flag = 1;

int myopen(struct inode *pnode,struct file *pfile)
{
	printk(KERN_INFO "myopen function exec\n");
	return 0;
}
int myclose(struct inode *pnode,struct file *pfile)
{
	
	printk(KERN_INFO "myclose function exec\n");
	return 0;
}
ssize_t mywrite (struct file *pfile, char __user *buf, size_t len, loff_t * plofft)
{
    flag = 0;
    wake_up_interruptible(&my_queue);
    return 0;
}
ssize_t myread(struct file *pfile, char __user *buf, size_t len, loff_t * plofft)
{
    printk(KERN_INFO "myread function exec\n");
    DECLARE_WAITQUEUE(new_node, current);
    add_wait_queue(&my_queue, &new_node);
    wait_event_interruptible(my_queue, flag != 1);
    return 0;
}
struct file_operations myops=
{
	.open  = myopen,
	.release = myclose,
	.write = mywrite,
	.read = myread,
};

static int __init myinit(void)
{
	int ret;
    //( (major & 0xFFF) << 20 ) | minor & 0xFFFFF;
	dev = MKDEV(major,minor);
    //向内核申请检测设备号是否可以使用,第一个参数为设备号,第二个参数为设备个数,第三个参数为设备名称
    //成功返回0,失败返回-1
	ret = register_chrdev_region(dev,1,"hello"); 
	if(ret < 0)
	{
       // 向内核申请动态分配设备号,dev接收内核动态分配设备号指针,第二个参数为次设备号
       // 成功返回0,失败返回-1
       ret = alloc_chrdev_region(&dev,1,1,"hello");
       if (ret < 0)
       {
         printk(KERN_INFO "alloc dev id error\n");
         return 0;
       }
	}
    major = MAJOR(dev);
	minor = MINOR(dev);
    printk(KERN_INFO "hello.ko device major = %d,minor = %d\n",major,minor);
    //初始化cdev结构体file_operations
    cdev_init(&mydev,&myops);
    // 初始化cdev结构体成员设备号dev,将struct dev对象加载到内核中, 等同于mydev.dev = dev_id;
	cdev_add(&mydev,dev,1);
    // 初始化等待队列头,相当于生成了一个空队列
    init_waitqueue_head(&my_queue);
	printk(KERN_INFO "myinit function exec success.\n");
	return 0;
}
static void __exit myexit(void)
{
    cdev_del(&mydev);
    // 向内核注销设备号
	unregister_chrdev_region(dev,1);
	printk(KERN_INFO "myexit function exec\n");
	return ;
}
module_init(myinit);
module_exit(myexit);

内核的并发处理

同一个驱动程序,可以同时被多个应用程序所使用,若不做处理,则有可能会使得驱动程序数据无序 怎样解决这个问题: 在一段时间内,一个驱动程序只为一个应用程序服务 可以类似于多线程中的锁机制,此时的共有资源就是驱动程序

  • 原子变量 驱动开发人员不能直接操作原子变量,必须使用内核提供的相关函数接口去操作它:
  atomic_t v;
  atomic_set(atomic_t *v, int value); //*v = value
  atomic_inc(atomic_t *v) // (*v)++
  atomic_dec(atomic_t *v) //(*v)--
  atomic_dec_test(atomic_t *v) //原子变量减1操作后测试其是否为0,若为0则返回非0(真),否则返回0(假)

原子变量的使用模板:

static atomic_t xxx_available = ATOMIC_INIT(1); // 定义原子变量
static init xxx_open(struct inode *inode, struct file *flip)
{
  if (!atomic_dec_and_test(&xxx_available))
  {
    atomic_inc(&xxx_available);
    return -EBUSY; // 已经打开
  }
  return 0;
}
static init xxx_release(struct inode *inode, struct file *flip)
{
  atomic_inc(&xxx_available);//释放设备 
  return 0;
}
#include "linux/kernel.h"
#include "linux/module.h"
#include "linux/init.h"
#include "linux/cdev.h"
#include "linux/kdev_t.h"
#include "linux/fs.h"
#include "asm/uaccess.h"
MODULE_LICENSE ("GPL");

struct cdev mydev;
dev_t dev;
//主设备号100, 次设备号1
int major = 100, minor = 1;
//内核缓冲区
char kernel_buf[128] = {0};
atomic_t v;

int myopen(struct inode *pnode,struct file *pfile)
{
    if (!atomic_dec_and_test(&v))
    {
      atomic_inc(&v);
      return -1;
    }
	printk(KERN_INFO "myopen function exec\n");
	return 0;
}
int myclose(struct inode *pnode,struct file *pfile)
{
	atomic_inc(&v);
	printk(KERN_INFO "myclose function exec\n");
	return 0;
}
ssize_t mywrite (struct file *pfile, char __user *buf, size_t len, loff_t * plofft)
{
    int ret;
    if (len > 128)
    {
        len = 128;
    }    
    printk(KERN_INFO "mywrite function exec, %s, len = %d\n", buf, len);
    ret = copy_from_user(kernel_buf, buf, len);//返回还未拷贝的字节数
    printk(KERN_INFO "ret = %d\n", ret);
	return len - ret;//返回内核拷贝了多少字节数
}
ssize_t myread(struct file *pfile, char __user *buf, size_t len, loff_t * plofft)
{
    int ret;
    char buffer[] = "this is kernel data.";
    ret = copy_to_user(buf, buffer, sizeof(buffer)-1);
    printk(KERN_INFO "myread function exec, ret = %d\n", ret);
	return sizeof(buffer)-1 - ret;
}
struct file_operations myops=
{
	.open  = myopen,
	.release = myclose,
	.write = mywrite,
	.read = myread,
};

static int __init myinit(void)
{
	int ret;
    //( (major & 0xFFF) << 20 ) | minor & 0xFFFFF;
	dev = MKDEV(major,minor);
    //向内核申请检测设备号是否可以使用,第一个参数为设备号,第二个参数为设备个数,第三个参数为设备名称
    //成功返回0,失败返回-1
	ret = register_chrdev_region(dev,1,"hello"); 
	if(ret < 0)
	{
       // 向内核申请动态分配设备号,dev接收内核动态分配设备号指针,第二个参数为次设备号
       // 成功返回0,失败返回-1
       ret = alloc_chrdev_region(&dev,1,1,"hello");
       if (ret < 0)
       {
         printk(KERN_INFO "alloc dev id error\n");
         return 0;
       }
	}
    major = MAJOR(dev);
	minor = MINOR(dev);
    printk(KERN_INFO "hello.ko device major = %d,minor = %d\n",major,minor);
    //初始化cdev结构体file_operations
    cdev_init(&mydev,&myops);
    // 初始化cdev结构体成员设备号dev,将struct dev对象加载到内核中, 等同于mydev.dev = dev_id;
	cdev_add(&mydev,dev,1);
    
    atomic_set(&v, 1);
	printk(KERN_INFO "myinit function exec success.\n");
	return 0;
}
static void __exit myexit(void)
{
    cdev_del(&mydev);
    // 向内核注销设备号
	unregister_chrdev_region(dev,1);
	printk(KERN_INFO "myexit function exec\n");
	return ;
}
module_init(myinit);
module_exit(myexit);