linux 设备驱动总结,linux设备驱动归纳总结(三):3面向对象思想和lseek
linux設備驅動歸納總結(三):3.設備驅動面向對象思想和lseek的實現
一、結構體struct
file和struct inode
在之前寫的函數,全部是定義了一些零散的全局變量。有沒有辦法整合成到一個結構體當中?這樣的話,看起來和用起來都比較方便。接下來就要說這方面的問題。
不過先要介紹一下除了fops以外的兩個比較重要的結構體:
1)struct
file
在內核中,file結構體是用來維護打開的文件的。每打開一次文件,內核空間里就
會多增加一個file來維護,當文件關閉是釋放。
所以,在內核中可以存在同一個文件的多個file,因為該文件被應用程序打開被打
開。
在struct
file中有幾個重要的成員:
1)loff_tf_pos;
這是用來記錄文件的偏移量。在應用程序中,打開文件時偏移量為0,每次的讀寫操作都會使偏移量增加。
從這個原因可以看出為什么每打開一次文件就新建一個file結構體了。不然的話,每個打開文件的讀寫操作都修改同一個偏移量,那讀寫豈不是亂套了嗎?
2)void*private_data;
這是空類型的指針可以用于存放任何數據,我會用這個指針來存放待會要定義的結構體指針。
回想一下,文件操作結構體fops中所有的函數成員里面都有一個參數是file結構體,所以每個函數都可以在file->private_data中拿到我自己定義的結構體了。
3)struct file_operations *fops;
打開文件后,內核會把fops存放在這里,以后的操作就在這里在這里找函數了。
2)struct
inode
這個結構體是用來保存一個文件的基本信息的結構體,即使打開多個相同的文件,也只會有一個對應的inode。
它也有兩個常用的成員:
1)dev_t i_rdev;
這里存放著這個文件的設備號。
2)struct cdev *i_cdev;
這個結構體很熟悉吧,這就是注冊設備時用的cdev就存在這。這個結構體的用處現在我還不好說,待會看程序就知道了。
二、面向對象的思想
接下來就封裝一下之前程序的數據類型吧:
18 struct _test_t{
19 char
kbuf[DEV_SIZE];//這里存放數據
20 unsigned int
major;//這里存放主設備號
21 unsigned int
minor;//這里存放次設備號
22 unsigned int
cur_size;//這里存放當前的kbuf的大小
23 dev_t devno;//這里存放設備號
24 struct cdev
test_cdev;//這里存放cdev結構體
25 };
定義了這樣的一個結構體后,在操作函數中怎么拿到這個結構體的指針呢?
先來個函數:
#define container_of(ptr, type,
member) ({\
const typeof( ((type *)0)->member
) *__mptr = (ptr);\
(type *)( (char *)__mptr -
offsetof(type,member) );})
使用:
已知一個結構體里面一個成員的指針ptr,同時,這個成員也是另外一個結構體類型中的一個成員,這個結構體的類型是type,而這個成員以member這個名字命名。就可以通過這個函數找到指向類型是type的結構體的指針。
返回值:
返回值就是指向type結構體類型的數據的指針。
如:現在定義這樣的兩個結構體:
struct A {
int *xiaobai_a;
};
struct B {
int xiaobai_b;
};
struct A a;
在遙遠的另一處有這樣的定義:struct
B b;
并且,a.xiaobai_a = &b.xiaobai_b;
這樣,在不知道b只知道a的情況下也可以找到b的位置:
struct B *bb =
container_of(a.xiaobai_a, struct B, xiaobai_b);
估計被上面的解釋說暈了吧。我還是舉個例比較方便:
雖然一個函數不值得說這么久,但是我覺得這種思想很不錯,內核中很多時候都用到這個函數,如在內核鏈表中。
來個邪惡的例子名字——老板與小秘:
老板他請了個年輕的小秘,他就跟客戶說:“我電話號碼經常換,你記著我小秘的電話,想找我嘛,找我小秘就可以了!”
于是,客戶想找老板了,就打通小秘的電話,說:“我知道你是秘書小紅,我想找你老板小黑,麻煩給他的電話號碼我。”
這樣,客戶就拿到了老板最新的電話號碼了。
想象老板和客戶是個結構體,秘書和他的電話號碼是個各自成員,電話號碼想象成指針:
老板的電話 =
container_of(秘書的電話, 老板,小秘)
說了半天還沒進入正題,這個函數用在哪里呢?誰當小秘呢?
就是那個說了半天都不知道能做什么還經常出現的struct
cdev!
而我把cdev添加到了我自己建的結構體struct
_test_t中,所喲struct
_test_t就是老板!
而struct
inode就是客戶了,因為它的成員里面有小秘的電話號碼:struct
cdev *i_cdev;
所以,如果想得到_test_t,只要調用這個函數就行了。
下面看一下改良后的open函數
27 int test_open(struct inode *node,
struct file *filp)
28 {
29 struct _test_t *dev;
30 dev =
container_of(node->i_cdev, struct _test_t, test_cdev);
31 filp->private_data
= dev;
32 return 0;
33 }
上面還有一句,將獲得的結構體指針存放到filp的private_data中。
這是因為,struct
file_operations中的每個函數的第一個參數就是struct
file,只要有file,每個函數都可以從private_data中得到數據了。相反,struct
inode這個參數并不是file_operations中所有的函數都有。
下面貼上部分代碼:1st/test.c
18 struct _test_t{
19 char kbuf[DEV_SIZE];
20 unsigned int major;
21 unsigned int minor;
22 unsigned int cur_size;
23 dev_t devno;
24 struct cdev test_cdev;
25 };
26
27 int test_open(struct inode *node,
struct file *filp)
28
{/*open操作需要給把拿到的結構體指針賦值給private_data*/
29 struct _test_t *dev;
30 dev =
container_of(node->i_cdev, struct _test_t, test_cdev);
31 filp->private_data = dev;
32 return 0;
33 }
34
35 int test_close(struct inode
*node, struct file *filp)
36 {
37 return 0;
38 }
39
40 ssize_t test_read(struct file
*filp, char __user *buf, size_t count, loff_t *offset)
41 {
42 int ret;
43 struct _test_t *dev =
filp->private_data;
44
45 if(!dev->cur_size){
46 return 0;
47 }
48
49 if (copy_to_user(buf,
dev->kbuf, count)){
50 ret = - EFAULT;
51
}else{/*read函數成功讀取后要修改cur_size*/
52 ret = count;
53 dev->cur_size -=
count;
54 }
55 P_DEBUG("cur_size:[%d]\n",
dev->cur_size);
56
57 return ret;
58 }
59
60 ssize_t test_write(struct file
*filp, const char __user *buf, size_t count, loff_t *offset)
61 {
62 int ret;
63 struct _test_t *dev =
filp->private_data;
64
65 if(copy_from_user(dev->kbuf,
buf, count)){
66 ret = - EFAULT;
67
}else{/*write函數成功寫入后也要修改cur_size*/
68 ret = count;
69 dev->cur_size +=
count;
70 P_DEBUG("kbuf is
[%s]\n", dev->kbuf);
71
P_DEBUG("cur_size:[%d]\n", dev->cur_size);
72 }
73
74 return ret;
//返回實際寫入的字節數或錯誤號
75 }
上面的程序其實就多了比上一個程序多了三步:
1)封裝了一個結構體。
2)open函數要獲得結構體并存放到private_data中。
3)read和write函數成功后要更新cur_size這個值。
這樣,一個像樣點的程序出來了,寫個應用程序驗證一下:
1 #include
2 #include
3 #include
4 #include
5
6 int main(void)
7 {
8 char buf[20];
9 int fd;
10 fd = open("/dev/test",
O_RDWR);
11 if(fd < 0)
12 {
13 perror("open");
14 return -1;
15 }
16
17 read(fd, buf, 10);
18 printf("buf
is [%s]\n", buf);
19
20 write(fd, "xiao bai",
10);
21
22 read(fd, buf, 10);
23 printf("buf
is [%s]\n", buf);
24
25 close(fd);
26 return 0;
27 }
運行一下:
[root: 1st]# insmod test.ko
major[253] minor[0]
hello kernel
[root: 1st]# mknod /dev/test c 253 0
[root: 1st]# ./app
buf is
[]//第一次讀取時cur_size==0,沒數據就會返回
[test_write]kbuf is
[xiao bai]//成功寫入
[test_write]cur_size:[10]//更新cur_size
[test_read]cur_size:[0]//read讀取成功,跟新cur_size
buf is [xiao
bai]//應用程序返回讀到的內容
[root: 1st]#
三、read、write的改進
上面的函數還是不完善的,想象一下,平時的read、write函數會增加偏移量,但上面的函數是不會的。這是因為還有一個參數我沒用上,就是"loff_t
offset"。
"loff_t
offset"這個參數是內核在調用函數時,從"struct
file"的成員"f_ops"拿到指針并當作參數傳入。這樣的做法讓用戶不用再從"struct
file"提取成員,直接拿參數用就行了!
通過這個參數,我們就可以改進并且實現三個函數:
1test_read:當應用程序調用read時內核會調用test_read。讀取數據的同時,偏移量會增加。
2test_write:當應用程序調用write時內核會調用test_write。寫入數據的同時,偏移量也會增加。
3test_llseek:這是跟應用程序的lseek對應的,用來修改偏移量的位置。
有了上面的三個函數的功能,這樣才算是個像樣的函數!
先改進一下read、write函數
40 ssize_t test_read(struct file
*filp, char __user *buf, size_t count, loff_t *offset)
41 {
42 int ret;
43 struct _test_t *dev =
filp->private_data;
44
45 if(*offset >=
DEV_SIZE){//如果偏移量已經超過了數組的容量
46 return count ? - ENXIO :
0; //count為0則返回0,表示讀取0個數據成功
47 }
//count不為0則分會錯誤號,地址越界
48 if(*offset + count >
DEV_SIZE){ //如果讀取字節數超過了最大偏移量
49 count = DEV_SIZE -
*offset; //則減少讀取字節數。
50 }
51 /*copy_to_user的參數也要改一下*/
52 if (copy_to_user(buf,
dev->kbuf + *offset, count)){
53 ret = - EFAULT;
54 }else{
55 ret = count;
56 dev->cur_size -=
count; //讀取后數組的字節數減少
57 *offset
+= count; //偏移量增加
58 P_DEBUG("read %d
bytes, cur_size:[%d]\n", count,dev->cur_size);
59 }
60
61 return ret;
//返回實際寫入的字節數或錯誤號
62 }
63
64 ssize_t test_write(struct file
*filp, const char __user *buf, size_t count, loff_t *offset)
65 {
66 int ret;
67 struct _test_t *dev =
filp->private_data;
68 /*copy_from_user的參數也要改一下*/
69 if(*offset >=
DEV_SIZE){//如果偏移量已經超過了數組的容量
70 return count ? - ENXIO :
0; //count為0則返回0,表示讀取0個數據成功
71 }
//count不為0則分會錯誤號,地址越界
72 if(*offset + count >
DEV_SIZE){ //如果讀取字節數超過了最大偏移量
73 count = DEV_SIZE -
*offset; //則減少讀取字節數。
74 }
75
76 if(copy_from_user(dev->kbuf,
buf, count)){
77 ret = - EFAULT;
78 }else{
79 ret = count;
80 dev->cur_size +=
count; //寫入后數組的字節數增加
81 *offset
+= count; //偏移量增加
82 P_DEBUG("write %d
bytes, cur_size:[%d]\n", count, dev->cur_size);
83 P_DEBUG("kbuf is
[%s]\n", dev->kbuf);
84 }
85
86 return ret;
//返回實際寫入的字節數或錯誤號
87 }
話說得好,越是需要檢測出錯,代碼就會幾何級增加,如果不想看這么多代碼,把這兩個函數前面的兩個if(45-50、69-74)都刪掉!反正寫應用程序的時候小心翼翼一點就好了。這個程序只是為了驗證"offset"的作用。
再來個小心翼翼的應用程序:
1 #include
2 #include
3 #include
4 #include
5
6 int main(void)
7 {
8 char buf[20];
9 int fd;
10 fd = open("/dev/test",
O_RDWR);
11 if(fd < 0)
12 {
13 perror("open");
14 return -1;
15 }
16
17 write(fd, "xiao bai",
10);
18
19 read(fd, buf, 10);
20 printf("buf
is [%s]\n", buf);
21
22 close(fd);
23 return 0;
24 }
驗證一下:
[root: 2nd]# insmod test.ko
major[253] minor[0]
hello kernel
[root: 2nd]# mknod /dev/test c 253 0
[root: 2nd]# ./app
[test_write]write 10
bytes, cur_size:[10]//寫入
[test_write]kbuf is
[xiao bai]
[test_read]read 10
bytes, cur_size:[0]//但讀不出,因為偏移量增加
buf is []
上面的read函數根本讀不出數據,這是因為偏移量增加了。這個時候需要一個函數來把偏移量移到開頭,lseek函數就用上場了。下面就講一下。
四、lseek函數的實現
應用層的函數lseek函數對應驅動的函數是llseek(為什么多了一個l我也想不懂)。
內核驅動:loff_t (*llseek)
(struct file * filp, loff_t offset, int whence);
對應應用層:off_t lseek(int
fd, off_t offset, int whence);
使用:
一看參數就知道,這兩個函數的第二和第三個參數就是對應的,當應用層調用函數時,對應的參數就會讓內核傳給驅動的函數llseek。
參數:
offset:一看這個參數不是指針,就知道和read、write的參數不一樣。這是應用層傳來的參數,并不是"struct
file"的偏移量"f_ops"。
whence:這個也跟應用層的參數一樣,指定從哪個位置開始偏移。
從開頭位置:#define
SEEK_SET0
從當前位置:#define
SEEK_CUR1
從文件末端:#define
SEEK_END2
返回值:成功返回當前的更新的偏移量,失敗返回錯誤號,而應用層會返回-1。
下面來個程序:/3rd_char/3rd_char_3/3rd/test.c
/*test_llseek*/
89 loff_t test_llseek (struct file
*filp, loff_t offset, int whence)
90 {
91 loff_t new_pos;
//新偏移量
92 loff_t old_pos = filp->f_pos;
//舊偏移量
93
94 switch(whence){
95 case SEEK_SET:
96 new_pos = offset;
97 break;
98 case SEEK_CUR:
99 new_pos = old_pos +
offset;
100 break;
101 case SEEK_END:
102 new_pos = DEV_SIZE +
offset;
103 break;
104 default:
105 P_DEBUG("unknow
whence\n");
106 return - EINVAL;
107 }
108
109 if(new_pos < 0 || new_pos
> DEV_SIZE){ //如果偏移量越界,返回錯誤號
110 P_DEBUG("f_pos
failed\n");
111 return - EINVAL;
112 }
113
114 filp->f_pos = new_pos;
115 return
new_pos;//正確返回新的偏移量
116 }
再來個應用程序:/3rd_char/3rd_char_3/3rd/app.c
1 #include
2 #include
3 #include
4 #include
5
6 int main(void)
7 {
8 char buf[20];
9 int fd;
10 int ret;
11
12 fd = open("/dev/test",
O_RDWR);
13 if(fd < 0)
14 {
15 perror("open");
16 return -1;
17 }
18
19 write(fd, "xiao bai",
10);
20 /*讓偏移量移至開頭,這樣才能讀取數據*/
21 ret = lseek(fd, 0, SEEK_SET);
22
23 read(fd, buf, 10);
24 printf("buf
is [%s]\n", buf);
25
26 close(fd);
27 return 0;
28 }
驗證一下:
[root: 2nd]# ./app
[test_write]write 10
bytes, cur_size:[10]
[test_write]kbuf is
[xiao bai]
[test_read]read 10
bytes, cur_size:[0]//讀到數據了!
buf is [xiao
bai]//讀到數據了!
五、總結
拉風的時序圖我就不畫了。
上面講的東西不多:
1)container_of的使用
2)怎么使用偏移量"filp->f_ops"。
3)llseek的編寫。
=========================================================
總結
以上是生活随笔為你收集整理的linux 设备驱动总结,linux设备驱动归纳总结(三):3面向对象思想和lseek的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: win8计算机管理员权限删除文件,win
- 下一篇: html中隐藏单元格上边框,HTML t