1. 程式人生 > >Linux 字符設備驅動及一些簡單的Linux知識

Linux 字符設備驅動及一些簡單的Linux知識

tro 直接 數據 null 高版本 exit const tdi 知識

一、linux系統將設備分為3類:字符設備、塊設備、網絡設備

1、字符設備:是指只能一個字節一個字節讀寫的設備,不能隨機讀取設備內存中的某一數據,讀取數據需要按照先後數據。字符設備是面向流的設備,常見的字符設備有鼠標、鍵盤、串口、控制臺和LED設備等。
2、塊設備:是指可以從設備的任意位置讀取一定長度數據的設備。塊設備包括硬盤、磁盤、U盤和SD卡等。

每一個字符設備或塊設備都在/dev目錄下對應一個設備文件。linux用戶程序通過設備文件(或稱設備節點)來使用驅動程序操作字符設備和塊設備。

本文主要是介紹字符設備驅動程序,從驅動程序開始,涉及文件操作,一共四個函數:包括文件的打開,讀,寫,刪除。還有文件的註冊和註銷。

廢話不多說,上源代碼,以代碼為例開始解釋:

1.mydriver.c:

#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/init.h>
#include <linux/uaccess.h>

#if CONFIG_MODVERSIONS == 1
#define MODVERSIONS
#include <linux/version.h>
#endif

#define DEVICE_NUM 0 //隨機產生一個設備號


static int device_num = 0; //用來保存創建成功後的設備號
static char buffer[1024] = "mydriver"; //數據緩沖區
static int open_nr = 0; //打開設備的進程數,用於內核的互斥

//函數聲明
// inode;linux下文件的管理號。
//file:linux一切皆文件。文件結構體代表一個打開的文件,系統中的每個打開的文件在內核空間都有一個關聯的 struct file。它由內核在打開文件時創建,並傳遞給在文件上進行操作的任何函數。在文件的所有實例都關閉後,內核釋放這個數據結構
static int mydriver_open(struct inode *inode, struct file *filp);
static int mydriver_release(struct inode *inode, struct file* filp);
static ssize_t mydriver_read(struct file *file, char __user *buf, size_t count, loff_t *f_pos); //loff-t:long long 型
static ssize_t mydriver_write(struct file *file, const char __user *buf, size_t count, loff_t *f_pos); //__user表明參數是一個用戶空間的指針,不能在kernel代碼中直接訪問。
//size_t:一個基本的無符號整數的C / C + +類型
//填充file_operations結構相關入口
static struct file_operations mydriver_fops = {
.read = mydriver_read,
.write = mydriver_write,
.open = mydriver_open,
.release = mydriver_release,
};

//打開函數
static int mydriver_open(struct inode *inode, struct file *filp)
{
printk("\nMain device is %d, and the slave device is %d\n", MAJOR(inode->i_rdev), MINOR(inode->i_rdev)); //把主從設備號傳入
if (open_nr == 0) {
open_nr++;
try_module_get(THIS_MODULE); //嘗試打開模塊
return 0;
}
else {
printk(KERN_ALERT "Another process open the char device.\n");//進程掛起
return -1;
}
}

//讀函數
static ssize_t mydriver_read(struct file *file, char __user *buf, size_t count, loff_t *f_pos)
{
//if (buf == NULL) return 0;
if (copy_to_user(buf, buffer, sizeof(buffer))) //讀緩沖 ,第一個參數是to:用戶空間的地址,第二個參數是from,內核空間的地址,第三個參數是要從內核空間拷貝的字節數
{
return -1;
}
return sizeof(buffer);
}

//寫函數,將用戶的輸入字符串寫入
static ssize_t mydriver_write(struct file *file, const char __user *buf, size_t count, loff_t *f_pos)
{
//if (buf == NULL) return 0;
if (copy_from_user(buffer, buf, sizeof(buffer))) //寫緩沖
{
return -1;
}
return sizeof(buffer);
}

//釋放設備函數
static int mydriver_release(struct inode *inode, struct file* filp)
{
open_nr--; //進程數減1
printk("The device is released!\n");
module_put(THIS_MODULE); //釋放模塊
return 0;
}

//註冊設備函數
static int __init mydriver_init(void)
{
int result;

printk(KERN_ALERT "Begin to init Char Device!"); //註冊設備
//向系統的字符登記表登記一個字符設備
result = register_chrdev(DEVICE_NUM, "mydriver", &mydriver_fops); //第一個參數等於0,則表示采用系統動態分配的主設備號;不為0,則表示靜態註冊。 第二個參數命名, 第三個參數為其地址

if (result < 0) {
printk(KERN_WARNING "mydriver: register failure\n");
return -1;
}
else {
printk("mydriver: register success!\n");
device_num = result;
return 0;
}
}

//註銷設備函數
static void __exit mydriver_exit(void)
{
printk(KERN_ALERT "Unloading...\n");
unregister_chrdev(device_num, "mydriver"); //註銷設備
printk("unregister success!\n");
}

//模塊宏定義
module_init(mydriver_init); //模塊加載函數
module_exit(mydriver_exit); //設備卸載函數

MODULE_LICENSE("GPL"); // "GPL" 是指明了 這是GNU General Public License的任意版本

因為註釋內容有限,在後面貼上一些對源代碼的註釋:

inode;linux下文件的管理號。(靜態的)
file:linux一切皆文件。文件結構體代表一個打開的文件,系統中的每個打開的文件在內核空間都有一個關聯的 struct file。它由內核在打開文件時創建,並傳遞給在文件上進行操作的任何函數。在文件的所有實例都關閉後,內核釋放這個數據結構。(動態的)
__user:The use of char __user *buf is typically found in the linux kernel...denoting that this address is in the user space. For example when writing to disk the kernel copies the contents of *buf into a kernel space buffer before writing it out to disk. Similarly when a process requests a read operation...the device driver at the behest of the kernel reads the desired disk blocks into a kernel space buffer...and then copies them into the user space buffer pointed to by *buf.
__user表明參數是一個用戶空間的指針,不能在kernel代碼中直接訪問。也方便其它工具對代碼進行檢查。
file-operations:
用來存儲驅動內核模塊提供的 設備進行各種操作的函數的指針。該結構體的每個域都對應著驅動內核模塊用來處理某個被請求的事務的函數的地址。
MAJOR(inode->i_rdev), MINOR(inode->i_rdev):
如果inode代表一個設備,則i_rdev的值為設備號。為了代碼更好地可移植性,獲取inode的major和minor號應該使用imajor和iminor函數
#include <linux/module.h> :
寫內核驅動的時候 必須加載這個頭文件,作用是動態的將模塊加載到內核中去
#include <linux/version.h> :
當設備驅動需要同時支持不同版本內核時,在編譯階段,內核模塊需要知道當前使用的內核源碼的版本,從而使用相應的內核 API
try_module_get(&module), module_put(&module):
靈活的模塊計數管理接口
#include <linux/kernel.h> :
kernel.h中包含了內核打印函數 printk函數 等
#include <linux/uaccess.h>:
包含了copy_to_user、copy_from_user等內核訪問用戶進程內存地址的函數定義。(copy_to_user()完成用戶空間到內核空間的復制,函數copy_from_user()完成內核空間到用戶空間的復制)
copy_to_user(buf, buffer, sizeof(buffer)//
#include <linux/fs.h> :
包含了文件操作相關struct的定義,例如大名鼎鼎的struct file_operations

#include <linux/init.h> :
內核模塊的初始化和註銷函數就在這個文件中

2. makefile:

# if KERNELRELEASE is defined, we‘ve been invoked from the

# kernel build system and can use its language.

ifeq ($(KERNELRELEASE),)

# Assume the source tree is where the running kernel was built

# You should set KERNELDIR in the environment if it‘s elsewhere

KERNELDIR ?= /lib/modules/$(shell uname -r)/build

# The current directory is passed to sub-makes as argument

PWD := $(shell pwd)

modules:

$(MAKE) -C $(KERNELDIR) M=$(PWD) modules

modules_install:

$(MAKE) -C $(KERNELDIR) M=$(PWD) modules_install

clean:

rm -rf *.o *~ core .depend .*.cmd *.ko *.mod.c .tmp_versions

.PHONY: modules modules_install clean

else

# called from kernel build system: just declare what our modules are

obj-m := mydriver.o

endif

3.

  1. test.c

#include <sys/types.h>

#include <sys/stat.h>

#include <stdlib.h>

#include <string.h>

#include <stdio.h>

#include <fcntl.h>

#include <unistd.h>

#define MAX_SIZE 1024

int main(void)

{

int fd;

char buf[MAX_SIZE];

char get[MAX_SIZE];

char devName[20], dir[49] = "/dev/";

system("ls /dev/");

printf("Please input the device‘s name you wanna to use :");

gets(devName);

strcat(dir, devName);

fd = open(dir, O_RDWR | O_NONBLOCK);

if (fd != -1)

{

read(fd, buf, sizeof(buf));

printf("The device was inited with a string : %s\n", buf);

// 測試寫

printf("Please input a string :\n");

gets(get);

write(fd, get, sizeof(get));

//測試讀

read(fd, buf, sizeof(buf));

system("dmesg");

printf("\nThe string in the device now is : %s\n", buf);

close(fd);

return 0;

}

else

{

printf("Device open failed\n");

return -1;

}

}

以上源代碼均可直接使用,以下是本人對基礎知識的一些拙見,如果您有什麽高見請務必指出,多謝各位大佬!

一.什麽是Makefile
(1)KERNELRELEASE在linux內核源代碼中的頂層makefile中有定義
(2)shell pwd會取得當前工作路徑
(3)shell uname -r會取得當前內核的版本號
(4)由於make 後面沒有目標,所以make會在Makefile中的第一個不是以.開頭的目標作為默認的目標執行。
於是modules成為make的目標
(5)首先-C表示到存放內核的目錄(KERNELDIR)執行其makefile,其中保存有內核的頂層makefile,M=選項的作用是,
當用戶需要以某個內核為基礎編譯一個外部模塊的話,需要在make modules 命令中加入M=$(PWD),
程序會自動到你所指定的dir目錄中查找模塊源碼,將其編譯,生成KO文件。
(6)obj-m是表示該文件要作為模塊編譯,只是編譯得到globalmem.o而不鏈接進內核
(7)每個Makefile中都應該寫一個清空目標文件(.o和執行文件)的規則,這不僅便於重編譯
,也很利於保持文件的清潔——clean
(8)make modules_install是把編譯好的模塊拷貝到系統目錄下(一般是/lib/modules/),
拷貝到系統目錄下的目的是方便使用,加載驅動就使用modprobe globalmem命令,該命令從系統目錄下查找名為globalmem的模塊

二.什麽是模塊化編程?為什麽要模塊化編程?
(1)內核模塊是一些可以讓操作系統內核在需要時載入和執行的代碼,同時在不需要的時候可以卸載。
這是一個好的功能,擴展了操作系統的內核功能,卻不需要重新啟動系統,是一種動態加載的技術。
(2)內核模塊代碼運行在內核空間,而應用程序在用戶空間。應用程序的運行會形成新的進程,而內核模塊一般不會
。每當應用程序執行系統調用時,Linux執行模式從用戶空間切換到內核空間。
答:Linux 內核的整體結構非常龐大,其包含的組件也非常多。我們怎樣把需要的部分都包含在內核中呢?
一種方法是把所有需要的功能都編譯到 Linux 內核。
這會導致兩個問題,一是生成的內核會很大,二是如果我們要在現有的內核中新增或刪除功能,將不得不重新編譯內核。
有沒有一種機制使得編譯出的內核本身並不需要包含所有功能,而在這些功能需要被使用的時候,
其對應的代碼可被動態地加載到內核中呢?
Linux 提供了這樣的一種機制,這種機制被稱為模塊(Module),可以實現以上效果。模塊具有以下特點。
1.模塊本身不被編譯入內核映像,從而控制了內核的大小。
2.模塊一旦被加載,它就和內核中的其他部分完全一樣。

三.如何理解設備號,什麽是主設備號,什麽是次設備號?

一個字符設備或塊設備都有一個主設備號和一個次設備號。主設備號用來標識與設備文件相連的驅動程序,用來反映設備類型。次設備號被驅動程序用來辨別操作的是哪個設備,用來區分同類型的設備。
  linux內核中,設備號用dev_t來描述,2.6.28中定義如下:
  typedef u_long dev_t;
  在32位機中是4個字節,高12位表示主設備號,低12位表示次設備號。

四.整個驅動從編譯到用測試程序進行測試的全過程,需要掌握相關的各種操作命令
1.在globalmen這個目錄裏面,先make一下,把globalmem.c編譯成模塊globalmem.ko
2.lsmod查看這個Linux裏面有沒有globalmem模塊
3.insmod globalmem.ko 加載globalmem模塊
4.lsmod查看是否加載成功
5.cat /proc/devices查看加載驅動程序時生成的設備 (proc目錄是一個虛擬文件系統,
可以為linux用戶空間和內核空間提供交互,它只存在於內存中,而不占實際的flash或硬盤空間)
6.mknod /dev/globalmem c 200 0 給globalmem設備創建設備節點,主設備號為200,次設備號為0
7.cd /dev 進入dev目錄
8.ls 查看dev目錄下是否有globalmem設備
9.echo ‘老師你真的很帥!‘>/dev/globalmem 向globalmem設備寫入一句話
10.cat /dev/globalmem 查看globalmem設備裏面的內容
11.gcc -o test.c test 用gcc來編譯測試程序
12../test 運行測試程序

五.三個重要的數據結構(結構體):file_operations、innode、file,尤其是file_operations
file文件結構:
struct file,定義於<linux/fs.h>.文件結構代表一個打開的文件.(它不特定給設備驅動;
系統中每個打開的文件有一個關聯的 struct file 在內核空間).
它由內核在 open 時創建,並傳遞給在文件上操作的任何函數, 直到最後的關閉.
在文件的所有實例都關閉後, 內核釋放這個數據結構
.struct file的指針常常稱為filp("file pointer"),源代碼裏面沒有這個結構體,
但是在函數裏面會經常用到它,例如
int globalmem_open(struct inode *inode, struct file *filp);
int globalmem_release(struct inode *inode, struct file *filp);
static ssize_t globalmem_read(struct file *filp, char __user *buf, size_t size, loff_t *ppos);
static ssize_t globalmem_write(struct file *filp, const char __user *buf, size_t size, loff_t *ppos);
static loff_t globalmem_llseek(struct file *filp, loff_t offset, int orig);
static long globalmem_ioctl(struct file *filp, unsigned int cmd, unsigned long arg);

inode結構:
inode 結構由內核在內部用來表示文件. 因此, 它和代表打開文件描述符的文件結構是不同的.
可能有代表單個文件的多個打開描述符的許多文件結構, 但是它們都指向一個單個 inode 結構.
inode 結構包含大量關於文件的信息. 作為一個通用的規則, 這個結構只有 2 個成員對於編寫驅動代碼有用:
dev_ti_rdev;
對於代表設備文件的節點,這個成員包含實際的設備編號.
structcdev *i_cdev;
structcdev 是內核的內部結構, 代表字符設備; 這個成員包含一個指針, 指向這個結構, 當節點指的是一個字符設備文件時.

file_operations結構:
結構體file_operations在頭文件 linux/fs.h中定義,用來存儲驅動內核模塊提供的對設備進行各種操作的函數的指針。
該結構體的每個域都對應著驅動內核模塊用來處理某個被請求的 事務的函數的地址。
如下為C99語法的使用該結構體的方法,並且沒有顯示聲明的結構體成員都被gcc初始化為NULL
static const struct file_operations globalmem_fops = {
.owner = THIS_MODULE,
.llseek = globalmem_llseek,
.read = globalmem_read,
.write = globalmem_write,
.unlocked_ioctl = globalmem_ioctl,/* 在2.6.x的內核版本中,文件操作結構體中,
才會有ioctl的字段,高版本中使用unlocked_ioctl */
.open = globalmem_open,
.release = globalmem_release,
};

Linux 字符設備驅動及一些簡單的Linux知識