1. 程式人生 > >linux裝置驅動第三篇:如何實現簡單的字元裝置驅動

linux裝置驅動第三篇:如何實現簡單的字元裝置驅動

linux裝置驅動第一篇:裝置驅動程式簡介中簡單介紹了字元驅動,本篇簡單介紹如何寫一個簡單的字元裝置驅動。本篇借鑑LDD中的原始碼,實現一個與硬體裝置無關的字元裝置驅動,僅僅操作從核心中分配的一些記憶體。

下面就開始學習如何寫一個簡單的字元裝置驅動。首先我們來分解一下字元裝置驅動

都有那些結構或者方法組成,也就是說實現一個可以使用的字元裝置驅動我們必須做些什麼工作。

1、主裝置號和次裝置號

對於字元裝置的訪問是通過檔案系統中的裝置名稱進行的。他們通常位於/dev目錄下。如下:

[email protected]:~$ ls -l /dev/
total 0
brw-rw----  1 root disk        7,   0  3月 25 10:34 loop0
brw-rw----  1 root disk        7,   1  3月 25 10:34 loop1
brw-rw----  1 root disk        7,   2  3月 25 10:34 loop2
crw-rw-rw-  1 root tty         5,   0  3月 25 12:48 tty
crw--w----  1 root tty         4,   0  3月 25 10:34 tty0
crw-rw----  1 root tty         4,   1  3月 25 10:34 tty1
crw--w----  1 root tty         4,  10  3月 25 10:34 tty10

其中b代表塊裝置,c代表字元裝置。對於普通檔案來說,ls -l會列出檔案的長度,而對於裝置檔案來說,上面的7,5,4等代表的是對應裝置的主裝置號,而後面的0,1,2,10等則是對應裝置的次裝置號。那麼主裝置號和次裝置號分別代表什麼意義呢?一般情況下,可以這樣理解,主裝置號標識裝置對應的驅動程式,也就是說1個主裝置號對應一個驅動程式。當然,現在也有多個驅動程式共享主裝置號的情況。而次裝置號有核心使用,用於確定/dev下的裝置檔案對應的具體裝置。舉一個例子,虛擬控制檯和串列埠終端有驅動程式4管理,而不同的終端分別有不同的次裝置號。

1.1、裝置編號的表達

在核心中,dev_t用來儲存裝置編號,包括主裝置號和次裝置號。在2.6的核心版本種,dev_t是一個32位的數,其中12位用來表示主裝置號,其餘20位用來標識次裝置號。 通過dev_t獲取主裝置號和次裝置號使用下面的巨集: MAJOR(dev_t dev); MINOR(dev_t dev); 相反,通過主裝置號和次裝置號轉換為dev_t型別使用: MKDEV(int major, int minor);

1.2、分配和釋放裝置編號

在構建一個字元裝置之前,驅動程式首先要獲得一個或者多個裝置編號,這類似一個營業執照,有了營業執照才在核心中正常工作營業。完成此工作的函式是:
int register_chrdev_region(dev_t first, unsigned int count, const char *name);
first是要分配的裝置編號範圍的起始值。count是連續裝置的編號的個數。name是和該裝置編號範圍關聯的裝置名稱,他將出現在/proc/devices和sysfs中。此函式成功返回0,失敗返回負的錯誤碼。此函式是在已知主裝置號的情況下使用,在未知主裝置號的情況下,我們使用下面的函式:
int alloc_chrdev_region(dev_t *dev, unsigned int firstminor, unsigned int count, const char *name);
dev用於輸出申請到的裝置編號,firstminor要使用的第一個此裝置編號。 在不使用時需要釋放這些裝置編號,已提供其他裝置程式使用:
void unregister_chrdev_region(dev_t dev, unsigned int count);
函式多在模組的清除函式中呼叫。 分配到裝置編號之後,我們只是拿到了營業執照,雖說現在已經準備的差不多了,但是我們只是從核心中申請到了裝置號,應用程式還是不能對此裝置作任何事情,我們需要一個簡單的函式來把裝置編號和此裝置能實現的功能連線起來,這樣我們的模組才能提供具體的功能.這個操作很簡單,稍後就會提到,在此之前先介紹幾個重要的資料結構。

2、重要的資料結構

註冊裝置編號僅僅是完成一個字元裝置驅動的第一步。下面介紹大部分驅動都會包含的三個重要的核心的資料結構。

2.1、檔案操作file_operations

file_operations是第一個重要的結構,定義在 <linux/fs.h>, 是一個函式指標的集合,裝置所能提供的功能大部分都由此結構提供。這些操作也是裝置相關的系統呼叫的具體實現。此結構的具體實現如下所示:
struct file_operations {
        //它是一個指向擁有這個結構的模組的指標. 這個成員用來在它的操作還在被使用時阻止模組被解除安裝. 幾乎所有時間中, 它被簡單初始化為 THIS_MODULE
        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);
        ssize_t (*read_iter) (struct kiocb *, struct iov_iter *);
        ssize_t (*write_iter) (struct kiocb *, struct iov_iter *);
        int (*iterate) (struct file *, struct dir_context *);
        unsigned int (*poll) (struct file *, struct poll_table_struct *);
        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 *, loff_t, loff_t, int datasync);
        int (*aio_fsync) (struct kiocb *, int datasync);
        int (*fasync) (int, struct file *, int);
        int (*lock) (struct file *, int, struct file_lock *);
        ssize_t (*sendpage) (struct file *, struct page *, int, size_t, loff_t *, int);
        unsigned long (*get_unmapped_area)(struct file *, unsigned long, unsigned long, unsigned long, unsigned long);
        int (*check_flags)(int);
        int (*flock) (struct file *, int, struct file_lock *);
        ssize_t (*splice_write)(struct pipe_inode_info *, struct file *, loff_t *, size_t, unsigned int);
        ssize_t (*splice_read)(struct file *, loff_t *, struct pipe_inode_info *, size_t, unsigned int);
        int (*setlease)(struct file *, long, struct file_lock **);
        long (*fallocate)(struct file *file, int mode, loff_t offset,
                          loff_t len);
        int (*show_fdinfo)(struct seq_file *m, struct file *f);
};

需要說明的是這裡面的函式在驅動中不用全部實現,不支援的操作留置為NULL。

2.2、檔案結構struct file

struct file, 定義於 <linux/fs.h>, 是裝置驅動中第二個最重要的資料結構。檔案結構代表一個開啟的檔案. (它不特定給裝置驅動; 系統中每個開啟的檔案有一個關聯的 struct file 在核心空間). 它由核心在 open 時建立, 並傳遞給在檔案上操作的任何函式, 直到最後的關閉. 在檔案的所有例項都關閉後, 核心釋放這個資料結構。file結構的詳細可參考fs.h,這裡列出來幾個重要的成員。
  • struct file_operations *f_op:就是上面剛剛介紹的檔案操作的集合結構。
  • mode_t f_mode:檔案模式確定檔案是可讀的或者是可寫的(或者都是), 通過位 FMODE_READ 和 FMODE_WRITE. 你可能想在你的 open 或者 ioctl 函式中檢查這個成員的讀寫許可, 但是你不需要檢查讀寫許可, 因為核心在呼叫你的方法之前檢查. 當檔案還沒有為那種存取而開啟時讀或寫的企圖被拒絕, 驅動甚至不知道這個情況
  • loff_t f_pos:當前讀寫位置. loff_t 在所有平臺都是 64 位。驅動可以讀這個值, 如果它需要知道檔案中的當前位置, 但是正常地不應該改變它。
  • unsigned int f_flags:這些是檔案標誌, 例如 O_RDONLY, O_NONBLOCK, 和 O_SYNC. 驅動應當檢查 O_NONBLOCK 標誌來看是否是請求非阻塞操作。
  • void *private_data:open 系統呼叫設定這個指標為 NULL, 在為驅動呼叫 open 方法之前. 你可自由使用這個成員或者忽略它; 你可以使用這個成員來指向分配的資料, 但是接著你必須記住在核心銷燬檔案結構之前, 在 release 方法中釋放那個記憶體. private_data 是一個有用的資源, 在系統呼叫間保留狀態資訊, 我們大部分例子模組都使用它

2.3、inode 結構

inode 結構由核心在內部用來表示檔案. 因此, 它和代表開啟檔案描述符的檔案結構是不同的. 可能有代表單個檔案的多個開啟描述符的許多檔案結構, 但是它們都指向一個單個 inode 結構。

inode 結構包含大量關於檔案的資訊。但對於驅動程式編寫來說一般不用關心,暫且不說。

3、字元裝置的註冊

核心在內部使用型別 struct cdev 的結構來代表字元裝置. 在核心呼叫你的裝置操作前, 你編寫分配並註冊一個或幾個這些結構。

有 2 種方法來分配和初始化一個這些結構. 如果你想在執行時獲得一個獨立的 cdev 結構, 你可以為此使用這樣的程式碼:

struct cdev *my_cdev = cdev_alloc();
my_cdev->ops = &my_fops;
更多的情況是把cdv結構嵌入到你自己封裝的裝置結構中,這時需要使用下面的方法來分配和初始化:

void cdev_init(struct cdev *cdev, struct file_operations *fops);

後面的例子程式就是這麼做的。一旦 cdev 結構建立, 最後的步驟是把它告訴核心:

int cdev_add(struct cdev *dev, dev_t num, unsigned int count)

<span style="font-family: Simsun;">這裡, dev 是 cdev 結構, num 是這個裝置響應的第一個裝置號, count 是應當關聯到裝置的裝置號的數目. 常常 count 是 1。</span>
<span style="font-family: Simsun;"></span><p style="font-family: Simsun;">從系統去除一個字元裝置, 呼叫:</p>
void cdev_del(struct cdev *dev);
<h3 style="margin: 0px; padding: 0px;">4、一個簡單的字元裝置</h3><div>上面大致介紹了實現一個字元裝置所要做的工作,下面就來一個真實的例子來總結上面介紹的內容。原始碼中的關鍵地方已經作了註釋。</div>
#include <linux/module.h>
#include <linux/types.h>
#include <linux/fs.h>
#include <linux/errno.h>
#include <linux/mm.h>
#include <linux/sched.h>
#include <linux/init.h>
#include <linux/cdev.h>
#include <asm/io.h>
#include <asm/uaccess.h>
#include <linux/timer.h>
#include <asm/atomic.h>
#include <linux/slab.h>
#include <linux/device.h>

#define CDEVDEMO_MAJOR 255  /*預設cdevdemo的主裝置號*/

static int cdevdemo_major = CDEVDEMO_MAJOR;

/*裝置結構體,此結構體可以封裝裝置相關的一些資訊等
  訊號量等也可以封裝在此結構中,後續的裝置模組一般都
  應該封裝一個這樣的結構體,但此結構體中必須包含某些
  成員,對於字元裝置來說,我們必須包含struct cdev cdev*/
struct cdevdemo_dev	
{
	struct cdev cdev;
};

struct cdevdemo_dev *cdevdemo_devp;	/*裝置結構體指標*/

/*檔案開啟函式,上層對此裝置呼叫open時會執行*/
int cdevdemo_open(struct inode *inode, struct file *filp)	
{
	printk(KERN_NOTICE "======== cdevdemo_open ");
	return 0;
}

/*檔案釋放,上層對此裝置呼叫close時會執行*/
int cdevdemo_release(struct inode *inode, struct file *filp)	
{
	printk(KERN_NOTICE "======== cdevdemo_release ");	
	return 0;
}

/*檔案的讀操作,上層對此裝置呼叫read時會執行*/
static ssize_t cdevdemo_read(struct file *filp, char __user *buf, size_t count, loff_t *ppos)
{
	printk(KERN_NOTICE "======== cdevdemo_read ");	
}

/* 檔案操作結構體,文中已經講過這個結構*/
static const struct file_operations cdevdemo_fops =
{
	.owner = THIS_MODULE,
	.open = cdevdemo_open,
	.release = cdevdemo_release,
	.read = cdevdemo_read,
};

/*初始化並註冊cdev*/
static void cdevdemo_setup_cdev(struct cdevdemo_dev *dev, int index)
{
	printk(KERN_NOTICE "======== cdevdemo_setup_cdev 1");	
	int err, devno = MKDEV(cdevdemo_major, index);
	printk(KERN_NOTICE "======== cdevdemo_setup_cdev 2");

	/*初始化一個字元裝置,裝置所支援的操作在cdevdemo_fops中*/	
	cdev_init(&dev->cdev, &cdevdemo_fops);
	printk(KERN_NOTICE "======== cdevdemo_setup_cdev 3");	
	dev->cdev.owner = THIS_MODULE;
	dev->cdev.ops = &cdevdemo_fops;
	printk(KERN_NOTICE "======== cdevdemo_setup_cdev 4");	
	err = cdev_add(&dev->cdev, devno, 1);
	printk(KERN_NOTICE "======== cdevdemo_setup_cdev 5");
	if(err)
	{
		printk(KERN_NOTICE "Error %d add cdevdemo %d", err, index);	
	}
}

int cdevdemo_init(void)
{
	printk(KERN_NOTICE "======== cdevdemo_init ");	
	int ret;
	dev_t devno = MKDEV(cdevdemo_major, 0);

	struct class *cdevdemo_class;
	/*申請裝置號,如果申請失敗採用動態申請方式*/
	if(cdevdemo_major)
	{
		printk(KERN_NOTICE "======== cdevdemo_init 1");
		ret = register_chrdev_region(devno, 1, "cdevdemo");
	}else
	{
		printk(KERN_NOTICE "======== cdevdemo_init 2");
		ret = alloc_chrdev_region(&devno,0,1,"cdevdemo");
		cdevdemo_major = MAJOR(devno);
	}
	if(ret < 0)
	{
		printk(KERN_NOTICE "======== cdevdemo_init 3");
		return ret;
	}
	/*動態申請裝置結構體記憶體*/
	cdevdemo_devp = kmalloc(sizeof(struct cdevdemo_dev), GFP_KERNEL);
	if(!cdevdemo_devp)	/*申請失敗*/
	{
		ret = -ENOMEM;
		printk(KERN_NOTICE "Error add cdevdemo");	
		goto fail_malloc;
	}

	memset(cdevdemo_devp,0,sizeof(struct cdevdemo_dev));
	printk(KERN_NOTICE "======== cdevdemo_init 3");
	cdevdemo_setup_cdev(cdevdemo_devp, 0);

	/*下面兩行是建立了一個匯流排型別,會在/sys/class下生成cdevdemo目錄
	  這裡的還有一個主要作用是執行device_create後會在/dev/下自動生成
	  cdevdemo裝置節點。而如果不呼叫此函式,如果想通過裝置節點訪問裝置
	  需要手動mknod來建立裝置節點後再訪問。*/
	cdevdemo_class = class_create(THIS_MODULE, "cdevdemo");
	device_create(cdevdemo_class, NULL, MKDEV(cdevdemo_major, 0), NULL, "cdevdemo");

	printk(KERN_NOTICE "======== cdevdemo_init 4");
	return 0;

	fail_malloc:
		unregister_chrdev_region(devno,1);
}

void cdevdemo_exit(void)	/*模組解除安裝*/
{
	printk(KERN_NOTICE "End cdevdemo");	
	cdev_del(&cdevdemo_devp->cdev);	/*登出cdev*/
	kfree(cdevdemo_devp);		/*釋放裝置結構體記憶體*/
	unregister_chrdev_region(MKDEV(cdevdemo_major,0),1);	//釋放裝置號
}

MODULE_LICENSE("Dual BSD/GPL");
module_param(cdevdemo_major, int, S_IRUGO);
module_init(cdevdemo_init);
module_exit(cdevdemo_exit);

Makefile檔案如下:
ifneq ($(KERNELRELEASE),)
obj-m := cdevdemo.o
else
KERNELDIR ?= /lib/modules/$(shell uname -r)/build
PWD := $(shell pwd)
default:
	$(MAKE) -C $(KERNELDIR) M=$(PWD) modules
endif

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

5、<strong>總結</strong>
本篇主要介紹了簡單字元裝置的編寫與實現以及其中的關鍵點。下一篇會主要講解下驅動的一些常用的除錯技巧。

第一時間獲得部落格更新提醒,以及更多技術資訊分享,歡迎關注個人微信公眾平臺:程式設計師互動聯盟(coder_online)

1.直接幫你解答linux裝置驅動疑問點

2.第一時間獲得業內十多個領域技術文章

3.針對文章內疑點提出問題,第一時間回覆你,幫你耐心解答

4.讓你和原創作者成為很好的朋友,拓展自己的人脈資源

掃一掃下方二維碼或搜尋微訊號coder_online即可關注,我們可以線上交流。


相關推薦

linux裝置驅動如何實現簡單字元裝置驅動

在linux裝置驅動第一篇:裝置驅動程式簡介中簡單介紹了字元驅動,本篇簡單介紹如何寫一個簡單的字元裝置驅動。本篇借鑑LDD中的原始碼,實現一個與硬體裝置無關的字元裝置驅動,僅僅操作從核心中分配的一些記憶體。 下面就開始學習如何寫一個簡單的字元裝置驅動。首先我們來分解一下

linux裝置驅動寫一個簡單字元裝置驅動

在linux裝置驅動第一篇:裝置驅動程式簡介中簡單介紹了字元驅動,本篇簡單介紹如何寫一個簡單的字元裝置驅動。本篇借鑑LDD中的原始碼,實現一個與硬體裝置無關的字元裝置驅動,僅僅操作從核心中分配的一些記憶體。 下面就開始學習如何寫一個簡單的字元裝置驅動。首先我們來分解一下字元

linux設備驅動寫一個簡單的字符設備驅動

提示 copy flags 驅動程序 相關 clas open ugo param 在linux設備驅動第一篇:設備驅動程序簡介中簡單介紹了字符驅動,本篇簡單介紹如何寫一個簡單的字符設備驅動。本篇借鑒LDD中的源碼,實現一個與硬件設備無關的字符設備驅動,僅僅操

Android應用程式訪問linux驅動實現並向系統註冊Service

前面兩篇部落格記錄了實現Linux驅動和使用HAL層訪問Linux驅動的程式碼,我們分別對這兩部分做了測試,他們都正常工作。有了前面的基礎,我們就可以實現service層了,我們想系統註冊我們自己的service,在service中訪問HAL層,在HAL層中訪問

Linux裝置驅動高階字元驅動操作之阻塞IO

我們之前介紹過簡單的read,write操作,那麼會有一個問題:當驅動無法立即響應請求該怎麼辦?比如一個程序呼叫read讀取資料,當沒有資料可讀時該怎麼辦,是立即返回還是等到有資料的時候;另一種情況是程序呼叫write向裝置寫資料,如果緩衝區滿了或者裝置正忙的時候怎麼辦,

linux裝置驅動從如何定位oops的程式碼行談驅動除錯方法

上一篇我們大概聊瞭如何寫一個簡單的字元裝置驅動,我們不是神,寫程式碼肯定會出現問題,我們需要在編寫程式碼的過程中不斷除錯。在普通的c應用程式中,我們經常使用printf來輸出資訊,或者使用gdb來除錯程式,那麼驅動程式如何除錯呢?我們知道在除錯程式時經常遇到的問題就是野指標

linux裝置驅動驅動中的併發與竟態

綜述 在上一篇介紹了linux驅動的除錯方法,這一篇介紹一下在驅動程式設計中會遇到的併發和竟態以及如何處理併發和競爭。 首先什麼是併發與竟態呢?併發(concurrency)指的是多個執行單元同時、並行被執行。而併發的執行單元對共享資源(硬體資源和軟體上的全域性、靜態變數)

Linux實戰RHEL7.3 yum更換實戰

yum個人筆記分享(在線閱讀):http://note.youdao.com/noteshare?id=cdae09cf51bf77a4e94a2e2865562dbbPDF版本下載http://down.51cto.com/data/2323064本文出自 “人才雞雞” 博客,請務必保留此出處http://

Certificate Vending Machine – IoT 裝置接入 AWS IoT 平臺解決方案

AWS IoT 物聯網系列部落格 當前物聯網環境中,裝置型別多種多樣,連線方式不一而足。為了幫助讀者更好的理解並運用 AWS IoT 相關服務,我們提供了一個完整的 IoT 起步指南,包含裝置的註冊及上線、裝置管理、使用者身份及許可權管理以及成本控制,通過這一系列的起步指南,

簡單粗暴JavaWeb-通過controller實現頁面跳轉

現在實現一個最簡單的登入頁面:輸入使用者名稱和密碼後,跳轉到登入結果頁面,提示登入結果。 1、編輯使用者名稱及密碼登入介面 首先重新編輯index.jsp首頁,使其擁有一個form表單,包含使用者名

OpenCV學習圖片的掩膜操作(實現影象的對比度調整)

掩膜操作實現影象的對比度調整 矩陣的掩膜操作十分簡單,根據掩膜來重新計算每個畫素的畫素值,掩膜(mask也被稱為kernel) I(i,j) = 5* I(i,j)-[I(i-1,j)+I(i+1,

Docker實戰 | Docker安裝Nginx,實現基於vue-element-admin框架構建的專案線上部署

## 一. 前言 在上一文中 [點選跳轉](https://www.cnblogs.com/haoxianrui/p/14088400.html) 通過IDEA整合Docker外掛實現微服務的一鍵部署,但 [youlai-mall](https://github.com/hxrui/youlai-mall

Python基礎函數

turn 說明 代碼 名稱 維護 span 大小寫 div 邏輯 一、Python函數介紹 1.函數的作用 規範代碼使代碼變得邏輯性更強 提高可讀性,方便管理,降低維護成本,以及降低代碼冗余 函數是組織好的,可重復使用的,用來實現單一,或相關聯功能的代碼段。 2.函

爬蟲框架 - Scrapy

工程 講解 爬取 turn 本體 爬蟲框架 sel 傳遞 使用 前言 Python提供了一個比較實用的爬蟲框架 - Scrapy。在這個框架下只要定制好指定的幾個模塊,就能實現一個爬蟲。 本文將講解Scrapy框架的基本體系結構,以及使用這

數據可視化 - ggplot2

strong 保存 轉換成 特征 散點圖 說明 pdf格式 ota 目的 前言 R語言的強大之處在於統計和作圖。其中統計部分的內容很多很強大,因此會在以後的實例中逐步介紹;而作圖部分的套路相對來說是比較固定的,現在可以先對它做一個總體的認識。

開發中的問題及解決方式

.text cat 彈窗 ret 如何 配置 中項 新的 顯示 1.texarea 如何保存空格、換行? 答:var content1= $("#content").val(); var content =content1.replace(/\n|\r\n/g,"&

Shell基本語法

允許 主體 賦值 算數 export $* lar script userdel 目錄 一、什麽是shell script 二、變量 三、運算符 四、流程控制 五、函數   一、什麽是shell script   將OS命令堆積到可執行文件裏,由上至下的順序執行文本裏的

python函數

名稱 bsp 一行 turn 內置 提高 none def 簡單 1、python函數 函數是組織好的,可重復使用的,用來實現單一,或相關聯功能的代碼段。 函數能提高應用的模塊性,和代碼的重復利用率。你已經知道Python提供了許多內建函數,比如print()。但你也可

Docker容器架構

通信 進程 核心 aca 虛擬化 比較 部署 fff 圖片 Docker 使用客戶端-服務器 (C/S) 架構模式,使用遠程API來管理和創建Docker容器。 Docker 容器通過 Docker 鏡像來創建。 Docker Daemon是docker最核心的守護進程,

R實戰 數據處理

ase 語言 dex test 矩陣 表達 set mat cond 在實際分析數據之前,必須對數據進行清理和轉化,使數據符合相應的格式,提高數據的質量。數據處理通常包括增加新的變量、處理缺失值、類型轉換、數據排序、數據集的合並和獲取子集等。 一,增加新的變量 通常需要