欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  网络运营

linux 驱动编写之虚拟字符设备的编写实例详解

程序员文章站 2022-12-27 07:55:02
 linux 驱动编写 前言:  昨天我们说了一些简单模块编写方法,但是终归没有涉及到设备的编写内容,今天我们就可以了解一下相关方面的内容,并且用一...

 linux 驱动编写

前言:

 昨天我们说了一些简单模块编写方法,但是终归没有涉及到设备的编写内容,今天我们就可以了解一下相关方面的内容,并且用一个实例来说明在linux上面设备是如何编写的。虽然我不是专门做linux驱动的,却也经常收到一些朋友们的来信。在信件中,很多做驱动的朋友对自己的工作不是很满意,认为自己的工作就是把代码拷贝来拷贝去,或者说是改来改去,没有什么技术含量。有这种想法的朋友不在少数,我想这主要还是因为他们对自己的工作缺少了解导致。如果有可能,我们可以问问自己这样几个问题:

    (1)我真的搞懂设备的开发驱动流程了吗?我是否可以从0开始,编写一个独立的驱动代码呢?
    (2)我真的了解设备的初始化、关闭、运行的流程吗?
    (3)当前的设备驱动流程是否合理,有没有可以改进的地方?
    (4)对于内核开发中涉及的api调用,我自己是否真正了解、是否明白它们在使用上有什么区别?
    (5)如果我要驱动的设备只是在一个前后台系统中运行,在没有框架帮助的情况下,我是否有信心把它启动和运行起来?

    当然,上面的内容只是我个人的想法,也不一定都正确。但是,知其然,更要知其所以然,熟悉了当前开发流程的优缺点才能真正掌握和了解驱动开发的本质。这听上去有些玄乎,其实也很简单,就是要有一种刨根问底、不断改进的精神,这样才能做好自己的工作。因为我们是在pc linux上学习驱动的,因此暂时没有真实的外接设备可以使用,但是这丝毫不影响我们学习的热情。通过定时器、进程,我们可以仿真出真实设备的各种需求,所以对于系统来说,它是无所谓真设备、假设备的,基本的处理流程对它来说都是一样的。只要大家一步一步做下去,肯定可以了解linux驱动设备的开发工程的。

    下面,为了说明问题,我们可以编写一段简单的char设备驱动代码,文件名为char.c,

#include <linux/module.h> 
#include <linux/kernel.h> 
#include <linux/fs.h> 
#include <linux/cdev.h> 
 
static struct cdev chr_dev; 
static dev_t ndev; 
 
static int chr_open(struct inode* nd, struct file* filp) 
{ 
  int major ; 
  int minor; 
   
  major = major(nd->i_rdev); 
  minor = minor(nd->i_rdev); 
   
  printk("chr_open, major = %d, minor = %d\n", major, minor); 
  return 0; 
} 
 
static ssize_t chr_read(struct file* filp, char __user* u, size_t sz, loff_t* off) 
{ 
  printk("chr_read process!\n"); 
  return 0; 
} 
 
struct file_operations chr_ops = { 
  .owner = this_module, 
  .open = chr_open, 
  .read = chr_read 
}; 
 
static int demo_init(void) 
{ 
  int ret; 
   
  cdev_init(&chr_dev, &chr_ops); 
  ret = alloc_chrdev_region(&ndev, 0, 1, "chr_dev"); 
  if(ret < 0 ) 
  { 
    return ret; 
  } 
   
  printk("demo_init(): major = %d, minor = %d\n", major(ndev), minor(ndev)); 
  ret = cdev_add(&chr_dev, ndev, 1); 
  if(ret < 0) 
  { 
    return ret; 
  } 
   
  return 0; 
} 
 
static void demo_exit(void) 
{ 
  printk("demo_exit process!\n"); 
  cdev_del(&chr_dev); 
  unregister_chrdev_region(ndev, 1); 
} 
 
module_init(demo_init); 
module_exit(demo_exit); 
 
module_license("gpl"); 
module_author("feixiaoxing@163.com"); 
module_description("a simple device example!"); 

    在module_init中的函数是模块加载时处理的函数,而模块卸载的函数则是在module_exit中。每一个设备都要对应一个基本的设备数据,当然为了使得这个设备注册在整个系统当中,我们还需要分配一个设备节点,alloc_chrdev_region就完成这样一个功能。等到cdev_add的时候,整个设备注册的过程就全部完成了,就是这么简单。当然为了编写这个文件,我们还需要编写一个makefile文件,

ifneq ($(kernelrelease),) 
obj-m := char.o 
 
else 
pwd := $(shell pwd) 
kver := $(shell uname -r) 
kdir := /lib/modules/$(kver)/build 
all: 
  $(make) -c $(kdir) m=$(pwd) modules 
clean: 
  rm -rf .*.cmd *.o *.mod.c *.ko .tmp_versions modules.* module.* 
endif 

    这个makefile文件和我们之前编写的makefile基本上没有区别,唯一的区别就是文件名称改成了char.o,仅此而已。为了编写模块,我们直接输入make即可。这时候,char.ko文件就可以生成了。然后,模块需要被注册在系统当中,insmod char.ko是少不了的。如果此时,我们还不确信是否模块已经加入到系统当中,完全可以通过输入lsmod | grep char进行查找和验证。为了创建设备节点,我们需要知道设备为我们创建的major、minor数值是多少,所以dmesg | tail 查找一下数值。在我hp的机器上,这两个数值分别是249和0,所以下面可以利用它们直接创建设备节点了,输入mknod /dev/chr_dev c 249 0即可,此时可以输入ls /dev/chr_dev验证一下。那么,按照这种方法,真的可以访问这个虚拟设备了吗,我们可以编写一段简单的代码验证一下,

#include <stdio.h> 
#include <fcntl.h> 
#include <unistd.h> 
 
#define char_dev_name "/dev/chr_dev" 
 
int main() 
{ 
  int ret; 
  int fd; 
  char buf[32]; 
 
  fd = open(char_dev_name, o_rdonly | o_ndelay); 
  if(fd < 0) 
  { 
    printf("open failed!\n"); 
    return -1; 
  } 
   
  read(fd, buf, 32); 
  close(fd); 
   
  return 0; 
} 

    代码的内容非常简单,就是利用char_dev_name直接打开设备,读写设备。当然。首先还是需要对这个文件进行编译,文件名为test.c,输入gcc test.c -o test,其次就是运行这个文件,直接输入./test即可。如果没有问题的话,那么说明我们的代码是ok的,但是我们还是没有看到任何内容。没关系,我们还是通过dmesg这个命令查看内核中是否存在相关的打印内容,直接输入dmesg | tail即可。此时如果没有意外的话,我们就可以看到之前在chr_open和chr_read中留下的printk打印,这说明我们的代码完全是ok的。

    上面的代码只是一段小例子,真实的内容要比这复杂一下。不过既然我们都已经入门了,那么后面的内容其实也没有什么好怕的了。最后有两个事情补充一下:(1)如果大家在创建节点后想删除设备节点,直接rm -rf /dev/chr_dev即可;(2)上面这段代码的原型来自于《深入linux设备驱动程序内核机制》这本书,稍作修改,如果大家对内核机制的内容感兴趣,可以参考这本书的内容。

 感谢阅读,希望能帮助到大家,谢谢大家对本站的支持!