Linux字符設備驅動


 

一、字符設備基礎

字符設備:是指只能一個字節一個字節進行讀寫操作的設備,不能隨機讀取設備中的某一數據、讀取數據要按照先后數據。字符設備是面向流的設備,常見的字符設備有鼠標、鍵盤、串口、控制台和LED等。

一般每個字符設備或者塊設備都會在/dev目錄(可以是任意目錄,這樣是為了統一)下對應一個設備文件。linux用戶層程序通過設備文件來使用驅動程序操作字符設備或塊設備。

二、字符設備驅動與用戶空間訪問該設備的程序三者之間的關系

字符設備是3大類設備(字符設備、塊設備、網絡設備)中較簡單的一類設備、其驅動程序中完成的主要工作是初始化、添加和刪除 struct cdev 結構體,申請和釋放設備號,以及填充 struct file_operations 結構體中斷的操作函數,實現 struct file_operations 結構體中的read()、write()和ioctl()等函數是驅動設計的主體工作。

 

如圖,在Linux內核代碼中:

  • 使用struct cdev結構體來抽象一個字符設備;
  • 通過一個dev_t類型的設備號(分為主(major)、次設備號(minor))一確定字符設備唯一性;
  • 通過struct file_operations類型的操作方法集來定義字符設備提供個VFS的接口函數。 

 

 三、字符設備模型

 

 

1、Linux內核中,使用 struct cdev 來描述一個字符設備

<include/linux/cdev.h>  

struct cdev {   
  struct kobject kobj;                  //內嵌的內核對象.  
  struct module *owner;                 //該字符設備所在的內核模塊(所有者)的對象指針,一般為THIS_MODULE主要用於模塊計數  
  const struct file_operations *ops;    //該結構描述了字符設備所能實現的操作集(打開、關閉、讀/寫、...),是極為關鍵的一個結構體
  struct list_head list;                //用來將已經向內核注冊的所有字符設備形成鏈表
  dev_t dev;                            //字符設備的設備號,由主設備號和次設備號構成(如果是一次申請多個設備號,此設備號為第一個)
  unsigned int count;                   //隸屬於同一主設備號的次設備號的個數
  ...
};  

 對於struct cdev內核提供了一些操作接口:

 頭文件linux/cdev.h

動態申請(構造)cdev內存(設備對象)

struct cdev *cdev_alloc(void);  
/* 返回值:
    成功 cdev 對象首地址
    失敗:NULL */

初始化cdev的成員,並建立cdev和file_operations之間關聯起來 

void cdev_init(struct cdev *p, const struct file_operations *p);  
/* 參數:
    struct cdev *p - 被初始化的 cdev對象
    const struct file_operations *fops - 字符設備操作方法集 */

注冊cdev設備對象(添加到系統字符設備列表中)

int cdev_add(struct cdev *p, dev_t dev, unsigned count);
/* 參數:
    struct cdev *p - 被注冊的cdev對象
    dev_t dev - 設備的第一個設備號
    unsigned - 這個設備連續的次設備號數量
   返回值:
    成功:0
    失敗:負數(絕對值是錯誤碼)*/

將cdev對象從系統中移除(注銷 )

void cdev_del(struct cdev *p);
/*參數: 
    struct cdev *p - 要移除的cdev對象 */

釋放cdev內存

void cdev_put(struct cdev *p);
/*參數:
    struct cdev *p - 要移除的cdev對象 */

 

2、設備號申請/釋放

一個字符設備或塊設備都有一個主設備號和一個次設備號。主設備號用來標識與設備文件相連的驅動程序,用來反映設備類型。次設備號被驅動程序用來辨別操作的是哪個設備,用來區分同類型的設備。

linux內核中,設備號用dev_t來描述:

typedef u_long dev_t;  // 在32位機中是4個字節,高12位表示主設備號,低20位表示次設備號。 

內核也為我們提供了幾個方便操作的宏實現dev_t: 

#define MAJOR(dev)    ((unsigned int) ((dev) >> MINORBITS))  // 從設備號中提取主設備號
#define MINOR(dev)    ((unsigned int) ((dev) & MINORMASK))  // 從設備號中提取次設備號
#define MKDEV(ma,mi)    (((ma) << MINORBITS) | (mi))</span>  // 將主、次設備號拼湊為設備號
/* 只是拼湊設備號,並未注冊到系統中,若要使用需要競態申請 */

頭文件 linux/fs.h 

a - 靜態申請設備號

int register_chrdev_region(dev_t from, unsigned count, const char *name);
/* 參數:
    dev_t from - 要申請的設備號(起始)
    unsigned count - 要申請的設備號數量
    const char *name - 設備名
   返回值:
    成功:0
    失敗:負數(絕對值是錯誤碼)*/

b - 動態分配設備號

int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const char *name);
/* 參數:
    dev_t *dev - 用於保存分配到的第一個設備號(起始)
    unsigned baseminor - 起始次設備號
    unsigned count - 要分配設備號的數量
    const char *name - 設備名
   返回值:
    成功:0
    失敗:負數(絕對值是錯誤碼)*/

c - 釋放設備號

void unregister_chrdev_region(dev_t from, unsigned count);
/* 參數:
    dev_t from - 要釋放的第一個設備號(起始)
    unsigned count - 要釋放的次設備號數量 */

 

d、創建設備文件:

利用cat /proc/devices查看申請到的設備名,設備號。

  1. 使用mknod手工創建:mknod filename type major minor
  2. 自動創建設備節點:利用udev(mdev)來實現設備文件的自動創建,首先應保證支持udev(mdev),由busybox配置。在驅動初始化代碼里調用class_create為該設備創建一個class,再為每個設備調用device_create創建對應的設備。

詳細解析見: Linux設備文件自動生成

 

3、struct cdev 中的 file_operations *fops成員

Linux下一切皆是“文件”,字符設備也是這樣,file_operations結構體中的成員函數是字符設備程序設計的主題內容,這些函數實際會在用戶層程序進行Linux的open()、close()、write()、read()等系統調用時最終被調用。

標准化:如果做到極致,應用層僅僅需要一套系統調用接口函數。

"文件"的操作接口結構:

struct file_operations {
  struct module *owner;  
    /* 模塊擁有者,一般為 THIS——MODULE */   ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);  
    /* 從設備中讀取數據,成功時返回讀取的字節數,出錯返回負值(絕對值是錯誤碼) */   ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);   
    /* 向設備發送數據,成功時該函數返回寫入字節數。若為被實現,用戶調層用write()時系統將返回 -EINVAL*/   int (*mmap) (struct file *, struct vm_area_struct *);  
    /* 將設備內存映射內核空間進程內存中,若未實現,用戶層調用 mmap()系統將返回 -ENODEV */   long (*unlocked_ioctl)(struct file *filp, unsigned int cmd, unsigned long arg);  
    /* 提供設備相關控制命令(讀寫設備參數、狀態,控制設備進行讀寫...)的實現,當調用成功時返回一個非負值 */   int (*open) (struct inode *, struct file *);  
    /* 打開設備 */   int (*release) (struct inode *, struct file *);  
    /* 關閉設備 */   int (*flush) (struct file *, fl_owner_t id);  
    /* 刷新設備 */   loff_t (*llseek) (struct file *, loff_t, int);  
    /* 用來修改文件讀寫位置,並將新位置返回,出錯時返回一個負值 */   int (*fasync) (int, struct file *, int);  
    /* 通知設備 FASYNC 標志發生變化 */   unsigned int (*poll) (struct file *, struct poll_table_struct *);  
    /* POLL機制,用於詢問設備是否可以被非阻塞地立即讀寫。當詢問的條件未被觸發時,用戶空間進行select()和poll()系統調用將引起進程阻塞 */   ... };

 四、簡單字符設備實例

cdev_module.c

#include <linux/init.h>
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/cdev.h>
#include <linux/fs.h>
#include <linux/errno.h>
#include <asm/current.h>
#include <linux/sched.h>
MODULE_LICENSE("GPL");
static int major = 0; 
static int minor = 0;
const int count = 3;
#define DEVNAME "demo"
static struct cdev *demop = NULL;
/
/打開設備
static int demo_open(struct inode *inode, struct file *filp)
{
  
//get command and pid
  printk(KERN_INFO "(%s:pid=%d), %s : %s : %d\n", current->comm, current->pid, __FILE__, __func__, __LINE__);
  
//get major and minor from inode
  printk(KERN_INFO "(major=%d, minor=%d), %s : %s : %d\n", imajor(inode), iminor(inode), __FILE__, __func__, __LINE__);
  
return 0;
}
//關閉設備
static int demo_release(struct inode *inode, struct file *filp)
{
  
//get command and pid
  printk(KERN_INFO "(%s:pid=%d), %s : %s : %d\n", current->comm, current->pid, __FILE__, __func__, __LINE__);
  
//get major and minor from inode
  printk(KERN_INFO "(major=%d, minor=%d), %s : %s : %d\n", imajor(inode), iminor(inode), __FILE__, __func__, __LINE__);
  
return 0;
}
//讀設備
static ssize_t demo_read(struct file *filp, char __user *buf, size_t size, loff_t *offset)
{
  
struct inode *inode = filp->f_path.dentry->d_inode;
  
//get command and pid
  printk(KERN_INFO "(%s:pid=%d), %s : %s : %d\n", current->comm, current->pid, __FILE__, __func__, __LINE__);
  
//get major and minor from inode
  printk(KERN_INFO "(major=%d, minor=%d), %s : %s : %d\n", imajor(inode), iminor(inode), __FILE__, __func__, __LINE__);
  return 0;
}
//寫設備
static ssize_t demo_write(struct file *filp, const char __user *buf, size_t size, loff_t *offset)
{
  
struct inode *inode = filp->f_path.dentry->d_inode;
  
//get command and pid
  printk(KERN_INFO "(%s:pid=%d), %s : %s : %d\n", current->comm, current->pid, __FILE__, __func__, __LINE__);
  //get major and minor from inode
  printk(KERN_INFO "(major=%d, minor=%d), %s : %s : %d\n", imajor(inode), iminor(inode), __FILE__, __func__, __LINE__);
  
return 0;
}
//操作方法集
static struct file_operations fops = {
  .owner
= THIS_MODULE, .open = demo_open,
  .release
= demo_release,
  .read
= demo_read,
  .write
= demo_write,
};
//cdev設備模塊初始化
static int __init demo_init(void)
{
  dev_t devnum;
int ret;
  
//get command and pid
  printk(KERN_INFO "(%s:pid=%d), %s : %s : %d\n", current->comm, current->pid, __FILE__, __func__, __LINE__);
  
//1. alloc cdev obj
  demop = cdev_alloc();
  
if(NULL == demop) {
    
return -ENOMEM;
  }
//2. init cdev obj cdev_init(demop, &fops);
ret
= alloc_chrdev_region(&devnum, minor, count, DEVNAME); if(ret){ goto ERR_STEP; } major = MAJOR(devnum); //3. register cdev obj ret = cdev_add(demop, devnum, count); if(ret){ goto ERR_STEP1; } //get command and pid printk(KERN_INFO "(%s:pid=%d), %s : %s : %d - ok.\n", current->comm, current->pid, __FILE__, __func__, __LINE__); return 0; ERR_STEP1: unregister_chrdev_region(devnum, count); ERR_STEP: cdev_del(demop); //get command and pid printk(KERN_INFO "(%s:pid=%d), %s : %s : %d - fail.\n", current->comm, current->pid, __FILE__, __func__, __LINE__); return ret; } static void __exit demo_exit(void) { //get command and pid printk(KERN_INFO "(%s:pid=%d), %s : %s : %d - leave.\n", current->comm, current->pid, __FILE__, __func__, __LINE__); unregister_chrdev_region(MKDEV(major, minor), count); cdev_del(demop); } module_init(demo_init); module_exit(demo_exit);

 test.c

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

int main(int num, char *arg[])
{
    if(2 != num){
        printf("Usage: %s /dev/devfile\n", arg[0]);
        return -1;
    }
    int fd = open(arg[1], O_RDWR);
    if(0 > fd){
        perror("open");
        return -1;
    }
    getchar();
    int ret = read(fd, 0x321, 0);
    printf("read: ret = %d.\n", ret);
    getchar();
    ret = write(fd, 0x123, 0);
    printf("write: ret = %d.\n", ret);
    getchar();
    close(fd);
    return 0;
}

Makefile 

ifneq ($(KERNELRELEASE),)
	obj-m = demo.o
else
	KERNELDIR :=  /lib/modules/$(shell uname -r)/build
	PWD       := $(shell pwd)
modules:
	$(MAKE) -C $(KERNELDIR) M=$(PWD) modules
endif

clean:
	rm -rf .tmp_versions Module.symvers modules.order .tmp_versions .*.cmd *.o *.ko *.mod.c

編譯成功后,使用 insmod 命令加載:

然后用cat /proc/devices 查看,會發現設備號已經申請成功;


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM