字符設備驅動
驅動分類:字符設備,塊設備,網絡接口設備。
什麼是字符設備:按字節來訪問的設備,驅動通常實現open,read,write等系統調用。
字符設備驅動模型
字符設備程序設計
設備號
(1)設備號是什麼:字符設備通過字符設備文件來存取。輸入 ls -l輸出的第一列是‘c’,這就是字符設備文件的標識。還有逗號分隔的兩個數,他們分別是字符設備文件的主次設備號。
主設備號:用來標識與設備文件相連的驅動程序,用來反應設備是什麼類型。
次設備號:被驅動程序用來辨別操作的是哪個設備,用來區分同類型的設備。
描述:dev_t:實質是unsigned int 32位整數,其中高12位是主設備號,低20位是次設備號。
主設備號:MAJOR(dev_t dev)
次設備號:MINOR(dev_t dev)
(2)分配設備號:靜態申請和動態分配。
1. 靜態申請:使用函數register_chrdev_region註冊設備號,優點是簡單,缺點是一旦驅動被廣泛使用,這個隨機的主設備號可能會導致設備號衝突,導致驅動程序無法註冊。
函數原型:int register_chrdev_region(dev_t from, unsigned count, const char* name);
功能:申請使用從from開始的count個設備號(主設備號不變,次設備號增加)
參數解析:
from:希望申請使用的設備號
count:希望申請使用的設備號數目
name:設備名(體現在/proc/device)
2. 動態分配:使用函數alloc_chrdev_region分配設備號,優點是簡單,易於驅動推廣,缺點是無法在安裝驅動前創建設備文件(因爲在安裝前還沒有分配到設備號)
函數原型:int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const char *name);
功能:請求內核動態分配count個設備號,且次設備號從baseminor開始。
參數解析:
dev:分配到的設備號
baseminor:起始的次設備號
count:需要分配的設備號數目
name:設備名
(3)註銷設備號:不論哪種方法分配設備號,不用的時候都應該釋放設備號。
函數原型:void unregister_chrdev_region(dev_t from, unsigned count);
功能:釋放從from開始的count個設備號。
創建設備文件:命令mknod手動創建
使用:mkmod filename type major minor
設備註冊
(1)描述:struct cdev
(2)註冊:3個步驟
1. 分配cdev
struct cdev的分配可使用cdev_alloc函數完成
使用:struct cdev* cdev_alloc(void)
2. 初始化cdev
struct cdev的初始化使用cdev_init函數完成
使用:void cdev_init(struct cdev* cdev, const struct file_operations* fops)
參數解析:
cdev:待初始化的cdev結構
fops:設備對應的操作函數
3. 添加cdev
struct cdev的註冊使用cdev_add函數完成
使用:int cdev_add(struct cdev* p, dev_t dev, unsigned count)
參數解析:
p:待添加到內核的字符設備驅動結構
dev:設備號
count:添加的設備個數
3種重要數據結構
struct file, struct inode, struct file_operations
(1)struct file:代表打開的文件。Linux系統中每一個打開的文件在內核空間都有一個關聯的struct file。他由內核在打開文件時創建,在文件關閉後釋放。
重要成員:loff_t f_pos 文件讀寫位置
(2)struct file_operations* f_op:一個函數指針的集合,定義可以在設備進行的操作。結構中的成員指向驅動中的函數,這些函數實現一個特別的操作,對於不支持的操作保留爲NULL
(3)struct inode:用來記錄文件的物理上的信息。和file結構不同,一個文件可以對應多個file結構,但是隻有一個inode結構。
重要成員:dev_t i_rdev 設備號
設備操作
open:設備文件的第一個操作
函數原型:int (open)(struct inode, struct file*)
並不要求驅動程序一定要實現這個操作。如果該項是NULL,設備的打開操作永遠成功的。
在大部分驅動程序中,open完成的工作是:初始化設備,標明次設備號。
release:設備文件關閉時調用這個操作
函數原型:void (release)(struct inode, struct file*)
與open類似,release也可以沒有。作用與open相反:關閉設備。
read:從設備中讀數據到用戶空間
函數原型:ssize_t(read)(struct file, char __user*, size_t, loff_t*);
write:向設備發數據,將數據傳遞給驅動程序
函數原型:ssize_t(write)(struct file, char __user*, size_t, loff_t*);
讀寫類似:ssize_t xxx_read(struct file* filp, char __user* buff, size_t count, loff_t* offp);
ssize_t xxx_write(struct file* filp, char __user* buff, size_t count, loff_t* offp);
參數解析:
1. filp:文件指針
2. count:請求傳輸的數據量
3. buff:用戶空間指針,指向數據緩存。不能被內核代碼直接調用,因爲用戶空間指針在內核空間可能根本是無效的,沒有地址映射。內核提供了專門的函數用於訪問用戶空間指針:
int copy_from_user(void* to, const void __user* from, int n);
int copy_to_user(void __user* to, const void* from, int n);
4. offp:文件當前的訪問位置
poll:對應select系統調用
函數原型:unsigned int(poll)(struct file, struct poll_table_struct*)
ioctl:控制設備
函數原型:int (ioctl)(struct inode, struct file*, unsigned int, unsigned long)
mmap:將設備映射到進程虛擬地址空間中
函數原型:int (mmap)(struct file, struct vm_area_struct*)
llseek:修改文件當前位置,並將新位置作爲返回值
函數原型:off_t (llseek)(struct file, loff_t, int)
cdev_del:字符設備註銷
函數原型:int cdev_del(struct cdev* p)
參數解析:p是要註銷的字符設備
範例設計:簡單字符驅動程序
開發一個基本的字符設備:建立一個名爲GlobalChar的虛擬設備,設備內部只有一個全局變量供用戶操作。
設備提供功能:
1. 讀函數讀取設備內部全局變量的值返回給用戶
2. 寫函數把用戶設定的值寫入全局變量
步驟如下:
編寫代碼:GlobalCharDev.c
#include<linux/module.h>
#include<linux/init.h>
#include<linux/kernel.h>
#include<cdev.h>
#include<linux/fs.h>
#include<linux/kdev_t.h>
#include<asm/uaccess.h>
#include<linux/device.h>
#define DEV_NAME "GlobalChar"
static ssize_t GlobalRead(struct file *, char *, size_t, loff_t *);
static ssize_t GlobalWrite(struct file *, const char *, size_t, loff_t *);
static int char_major = 0;
static int GlobalData = 0;
static const struct file_operations globalchar_fops =
{
.read = GlobalRead,
.write = GlobalWrite
};
static int __init GlobalChar_init(void)
{
int ret;
ret = register_chrdev(char_major, DEV_NAME, &globalchar_fops);
if(ret < 0)
{
printk(KERN_ALERT "GlobalChar register failed!\n");
}
else
{
printk(KERN_ALERT "GlobalChar register sucess!\n");
char_major = ret;
printk(KERN_ALERT "major = %d\n", char_major);
}
return ret;
}
static void __exit GlobalChar_exit(void)
{
unregister_chrdev(char_major, DEV_NAME);
return;
}
static ssize_t GlobalRead(struct file *filp, char *buf, size_t len, loff_t *off)
{
if(copy_to_user(buf, &GlobalData, sizeof(int)))
{
return -EFAULT;
}
return sizeof(int);
}
static ssize_t GlobalWrite(struct file *filp, const char *buf, size_t len, loff_t *off)
{
if(copy_from_user(&GlobalData, buf, sizeof(int)))
{
return -EFAULT;
}
return sizeof(int);
}
module_init(GlobalChar _init);
module_exit(GlobalChar _exit);
編寫Makefile
obj-m := GlobalCharDev.o
KDIR := /lib/modules/$(shell uname -r)/bulid
SRCPWD := $(shell pwd)
all:
make -C $(KDIR) M=$(SRCPWD) modules
編譯並加載內核模塊
查看內核分配的主設備號:dmesg | tail -n 10和cat /proc/devices | grep GlobalChar
使用mknod命令創建一個設備文件:sudo mknod -m 666 /dev/GlobalChar c 249 0
到此爲止,已經成功添加了一個字符設備到內核,下面是測試驅動程序能否正常工作。
測試代碼:GlobalCharTest.c
#include<stdio.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#define DEV_NAME "/dev/GlobalChar"
int main()
{
int fd, num;
fd = open(DEV_NAME, O_RDWR, S_IRUSR | S_IWUSR);
if(fd < 0)
{
printf("open device fail!\n");
return -1;
}
read(fd, &num, sizeof(int));
printf("the GlobalChar is %d\n", num);
printf("please input a number written to GlobalChar: ");
scanf("%d", &num);
write(fd, &num, sizeof(int));
read(fd, &num, sizeof(int));
printf("the GlobalChar is %d\n", num);
close(fd);
return 0;
}