閒來無事,整理一下驅動入門知識!
大部分與網上整理的差不多,我主要想說的有兩個特別的地方,剛入門的人看別人整理的肯定都不知道怎麼測試。或者測試結果不像他們所寫的那樣!
第一點就是用mknod創建的設備名,設備號不能隨便寫,必須你所寫的源文件命名的一致。
比如你在c文件中定義
#define DEV_NAME "chardev"
那麼設備名就是chardev
設備號可以通過 cat /proc/devices |grep chardev 得到主設備號。
root@lll-virtual-machine:/home/lll/share/linuxdev/chardev# cat /proc/devices |grep chardev
249 chardev
第二點要注意的就是通過echo 或cat來直接操作 /dev/chardev字符文件不能得到正確的內容。使用命令:
echo “123”>/dev/glaobalvar
向字符設備寫入命令,然後使用cat命令:
cat /dev/globalvar
讀取字符數據的時候,出現了無限輸出的情況.
原因
首先,read()函數的返回值設定不正確,
static ssize_t chardev_read(struct file *filp, char *buf, size_t len, loff_t *off)
{
…
return sizeof(int); //此處不應該是一個固定數值,如果是固定值,在終端中進行cat 時,系統默認當前終端爲獨立的一個進程,讀取未結束,會一直/反覆的讀取這裏面內容,
}
cat的內部大概是這樣
while (1) {
int n = read(0, buf, size);
if (n <= 0) break;
write(1, buf, n);
}
所以,要讓cat退出,必須保證後續的read能夠返回0(即讀到EOF)。
文件偏移指針,也就是fops->read的第四個參數,loff_t off,就是起這個作用的。
如果off >= sizeof(int),就可以直接返回0了(表示EOF)。
下面的代碼是cat echo 會亂碼。可以先看看對比一下
–> 這是chardev.c 文件
- 字符驅動大體也有一個模板。建立mychar.c的代碼如下,
#include <linux/init.h> //指定初始化和清除函數
#include <linux/module.h> //模塊所需的大量符號和函數定義
#include <linux/kernel.h> //內核所需要的函數
#include <linux/fs.h> //文件系統相關的函數和頭文件
#include <asm/uaccess.h> //在內核和用戶空間中移動數據的函數
#include <linux/cdev.h> //cdev結構的頭文件
MODULE_LICENSE("GPL");
MODULE_AUTHOR("lll");
#define DEV_NAME "chardev"
static ssize_t chardevRead(struct file *,char *,size_t,loff_t*);
static ssize_t chardevWrite(struct file *,const char *,size_t,loff_t*);
static int char_major = 0;
static int chardevData = 12345; //"chardev" device’s global variable.
//initialize char device’s file_operations struct
struct file_operations chardev_fops =
{
.read = chardevRead,
.write = chardevWrite
};
static int __init chardev_init(void)
{
int ret;
ret = register_chrdev(char_major,DEV_NAME,&chardev_fops);
if(ret<0)
{
printk(KERN_ALERT "chardev Reg Fail!\n");
}
else
{
printk(KERN_ALERT "chardev Reg Success!\n");
char_major = ret;
printk(KERN_ALERT "Major = %d\n",char_major);
}
return 0;
}
static void __exit chardev_exit(void)
{
unregister_chrdev(char_major,DEV_NAME);
printk(KERN_ALERT "chardev is dead now!\n");
return;
}
static ssize_t chardevRead(struct file *filp,char *buf,size_t len,loff_t *off)
{
printk("chardev is read now!\n");
chardevData -= 1;
if (copy_to_user(buf,&chardevData,sizeof(int)))
{
return -EFAULT;
}
printk("chardev is read: buf=%d\n",*buf);
printk("chardev is read end: len=%d\n",len);
return *buf;
}
static ssize_t chardevWrite(struct file *filp,const char *buf,size_t len,loff_t *off)
{
printk("chardev is write now!\n");
printk("chardev is write: buf=%d\n",*buf);
if (copy_from_user(&chardevData,buf,sizeof(int)))
{
return -EFAULT;
}
printk("chardev is write end: len=%s\n",len);
return *buf;
}
module_init(chardev_init);
module_exit(chardev_exit);
–>下面是Makefile文件
root@lll-virtual-machine:/home/lll/share/linuxdev/chardev# cat Makefile
obj-m:=chardev.o
testmodule-objs:=module
KDIR:=/lib/modules/3.11.0-15-generic/build #這是地方每個人的系統不一樣,請不要照搬。
#KDIR=/lib/modules/$(shell uname -r)/build #可以寫成這樣哈
MAKE:=make
default:
$(MAKE) -C $(KDIR) SUBDIRS=$(PWD) modules
clean:
$(MAKE) -C $(KDIR) SUBDIRS=$(PWD) clean
rmmod chardev
rm -f /dev/chardev
上面兩個文件都有了之後。直接make 生成chardev.ko
–>然後insmod chardev.ko 可以通過dmesg|tail -n 10 看到log
–>然後cat /proc/devices |grep chardev 查看設備號
root@lll-virtual-machine:/home/lll/share/linuxdev/chardev# cat /proc/devices |grep chardev
249 chardev
–>mknod -m 666 /dev/chardev 249 0
下面還得通過應用層的app來操作這個設備。
//root@lll-virtual-machine:/home/lll/share/linuxdev/chardev# cat testchardev.c
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#define DEV_NAME "/dev/chardev"
int main(void)
{
int fd;
int num=9999;
char buf1[10] = {0, 1};
char buf2[10];
fd = open(DEV_NAME,O_RDWR);
if(fd < 0)
{
printf("Open /dev/mychar error\n");
return -1;
}
read(fd,&num,sizeof(int));
printf("The myChar is %d\n",num);
printf("Please input a number written to myChar: ");
scanf("%d",&num);
write(fd,&num,sizeof(int));
read(fd,&num,sizeof(int));
printf("The myChar you input is %d\n",num);
close(fd);
return 0;
}
–> gcc testchardev.c -o test 生成應用
./tes t
root@lll-virtual-machine:/home/lll/share/linuxdev/chardev# ./test
The myChar is 926298623
Please input a number written to myChar: 35
The myChar you input is 34
root@lll-virtual-machine:/home/lll/share/linuxdev/chardev# ./test
The myChar is 33
Please input a number written to myChar: 23
The myChar you input is 22
---->下面使用echo cat 測試 結果如下。可以看到出來的結果並不正確!
root@lll-virtual-machine:/home/lll/share/linuxdev/chardev# echo “2” >/dev/chardev
root@lll-virtual-machine:/home/lll/share/linuxdev/chardev# cat /dev/chardev
1
670
67/
67.
67-
67,
67+
67"
67!
67
67
67
67
67
67
6
下面是修改後完美解決cat echo 字符設備亂碼的問題的代碼
root@lll-virtual-machine:/home/lll/share/linuxdev/chardev# cat mychar.c
#include <linux/module.h>
#include <linux/init.h>
#include <linux/fs.h>
#include <asm/uaccess.h>
MODULE_LICENSE("GPL");
MODULE_AUTHOR("lll");
#define DEV_NAME "chardev"
#define DEV_SIZE 0x400 //定義字符設備的大小,400字節
static ssize_t chardevRead(struct file *,char *,size_t,loff_t*);
static ssize_t chardevWrite(struct file *,const char *,size_t,loff_t*);
static int char_major = 0;
static char chardevData[1024] = "hello world\n"; //"chardev" device’s global variable.
//initialize char device’s file_operations struct
struct file_operations chardev_fops =
{
.read = chardevRead,
.write = chardevWrite
};
static int __init chardev_init(void)
{
printk(KERN_ALERT "=====================init====================\n");
int ret;
ret = register_chrdev(char_major,DEV_NAME,&chardev_fops);
if(ret<0)
{
printk(KERN_ALERT "chardev Reg Fail!\n");
}
else
{
printk(KERN_ALERT "chardev Reg Success!\n");
char_major = ret;
printk(KERN_ALERT "Major = %d\n",char_major);
}
return 0;
}
static void __exit chardev_exit(void)
{
unregister_chrdev(char_major,DEV_NAME);
printk(KERN_ALERT "chardev is dead now!\n");
return;
}
static ssize_t chardevRead(struct file *filp,char *buf,size_t len,loff_t *off)
{
printk(KERN_ALERT "-------------------Read------------------\n");
unsigned long offset = *off;
unsigned int count = len;
int ret = 0;
/*分析和獲取有效的寫長度*/
if (offset > DEV_SIZE)
{
printk("offset > DEV_SIZE\n");
return count ? - ENXIO: 0;
}
else if(offset == DEV_SIZE)
{
printk("offset = DEV_SIZE\n");
return 0; // 防止測試cat /dev/chardev 時 文件尾出現錯誤提示
}
if (count > DEV_SIZE - offset)
{
printk("count > DEV_SIZE -offset\n");
count = DEV_SIZE - offset;
}
printk("read char_len=%ld\n",strlen(chardevData));
printk("read chardevData=%s\n",chardevData);
if (!copy_to_user(buf,(char*)(chardevData), strlen(chardevData)+1))
{
*off += count;
printk(KERN_INFO "read %d bytes(s) from %ld addr\n", count, offset);
ret = count;
}
else
return -EFAULT;
//}
printk("chardev is read: buf=%s\n",buf);
printk("chardev is read: ref=%d\n",ret);
return ret;
}
static ssize_t chardevWrite(struct file *filp,const char *buf,size_t len,loff_t *off)
{
printk(KERN_ALERT "--------Write-------------\n");
printk("chardev is write now!\n");
printk("chardev is write: buf=%s\n",buf);
unsigned long offset = *off;
offset=0;
unsigned int count = len;
int ret = 0;
memset(chardevData,0,strlen(chardevData));
/*分析和獲取有效的寫長度*/
if (offset > DEV_SIZE)
{
printk("offset > DEV_SIZE\n");
return count ? - ENXIO: 0;
}
else if(offset == DEV_SIZE)
{
printk("offset = DEV_SIZE\n");
return 0; // 防止測試echo /dev/chardev 時 文件尾出現錯誤提示
}
if (count > DEV_SIZE - offset)
{
count = DEV_SIZE - offset;
}
printk("count= %d\n",count);
printk("need len:%ld, offset:%ld\n",len,offset);
if (copy_from_user((char*)(chardevData),buf,count))
{
printk("copy error! chardevData=%s\n",chardevData);
return -EFAULT;
}
else
{
*off += count;
printk(KERN_INFO "read %d bytes(s) from %ld addr\n", count, offset);
ret = count;
}
printk("chardevData=%s\n",chardevData);
return ret;
}
module_init(chardev_init);
module_exit(chardev_exit);
補充資料:
二.主設備號
字符設備通過字符設備文件來存取。字符設備文件由使用ls -l 的輸出的第一列的”C”標識;
如果使用ls –l 命令,會看到在設備文件項中有2個數(由一個逗號分隔)這些數字就是設備文件的主次設備編
1>.主設備號用來標識與設備文件相連的驅動程序。
次設備號被驅動程序用來辨別操作的是哪個設備。
主設備號用來反映設備類型*
次設備號用來區分同類型的哪一個設備
內核中如何描述設備號?
dev_t 其實質爲unsigned int 32位整數,其中高12位爲主設備號,低20位爲次設備號。
安裝驅動後,從/proc/devices中查詢設備號
字符設備驅動操作接口
在編寫Linux內核設備驅動時,需要使用內核提供的設備驅動接口,向內核提供具體設備的操作方法。應用程序可以像操作普通文件一樣操作字符設備。常見的串口、調制解調器都是字符設備。編寫字符設備驅動需要使用內核提供的(1)**register_chrdev()**函數註冊一個字符設備驅動。函數定義如下:
int register_chrdev(unsigned int major, const char *name, struct file_operations *fops);
參數major是主設備號,name是設備名稱,fops是指向函數指針數組的結構指針,驅動程序的入口函數都是包括在這個指針內部。該函數的返回值如果小於0表示註冊設備驅動失敗,如果設置major爲0,表示由內核動態分配主設備號,函數返回值是主設備號。
當用register_chrdev()函數成功註冊一個字符設備後,會在/proc/devices文件中顯示出設備信息
(2) register_chrdev()函數中有一個fops參數,該參數指向一個file_operations結構,該結構包含了驅動上所有操作。隨着內核功能的不斷增加,file_operations結構的定義也越來越複雜,內核2.6.18版本的定義如下:
struct file_operations
{
struct module *owner;
loff_t (*llseek) (struct file *, loff_t, int);
ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);
ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);
ssize_t (*aio_read) (struct kiocb *, const struct iovec *, unsigned long, loff_t);
ssize_t (*aio_write) (struct kiocb *, const struct iovec *, unsigned long, loff_t);
int (*readdir) (struct file *, void *, filldir_t);
unsigned int (*poll) (struct file *, struct poll_table_struct *);
int (*ioctl) (struct inode *, struct file *, unsigned int, unsigned long);
long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);
long (*compat_ioctl) (struct file *, unsigned int, unsigned long);
int (*mmap) (struct file *, struct vm_area_struct *);
int (*open) (struct inode *, struct file *);
int (*flush) (struct file *, fl_owner_t id);
int (*release) (struct inode *, struct file *);
int (*fsync) (struct file *, struct dentry *, int datasync);
int (*aio_fsync) (struct kiocb *, int datasync);
int (*fasync) (int, struct file *, int);
…
};
結構中每個成員都是一個函數指針,指向不同功能的函數,大部分驅動都沒有提供所有的函數。對於字符設備來說,常用的成員及解釋如下:
**int (*open) (struct inode *, struct file *);**
儘管這常常是對設備文件進行的第一個操作, 不要求驅動聲明一個對應的方法. 如果這個項是 NULL, 設備打開一直成功, 但是你的驅動不會得到通知。
**ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);**
用來從設備中獲取數據. 在這個位置的一個空指針導致 read 系統調用以-EINVAL(”Invalid argument”) 失敗. 一個非負返回值代表了成功讀取的字節數( 返回值是一個 “signed size” 類型, 常常是目標平臺本地的整數類型)。
**ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);**
發送數據給設備. 如果 NULL, -EINVAL 返回給調用write 系統調用的程序. 如果非負, 返回值代表成功寫的字節數。
**int (*release) (struct inode *, struct file *);**
在文件結構被釋放時引用這個操作. 如同 open, release 可以爲 NULL。
**int (*ioctl) (struct inode *, struct file *, unsigned int, unsigned long);**
ioctl 系統調用提供了發出設備特定命令的方法(例如格式化軟盤的一個磁道, 這不是讀也不是寫)。另外, 幾個ioctl 命令被內核識別而不必引用 fops 表。如果設備不提供ioctl 方法, 對於任何未事先定義的請求(-ENOTTY,”設備無這樣的ioctl”), 系統調用返回一個錯誤。
(3)與註冊相反,內核提供了unregister_chrdev()函數卸載設備驅動,定義如下:
**int unregister_chrdev(unsigned int major, const char *name);**
major是想要卸載的主設備號,name是欲卸載的設備驅動名稱。內核會比較設備驅動名稱和設備號是否相同,不同則返回-EINVAL。錯誤地卸載設備驅動可能會帶來嚴重後果,因此在卸載驅動的時候應該對函數返回值進行判斷。