文章目錄
- Linux驅動
- 概念
- 應用程序調用驅動程序流程
- 驅動模塊的加載
- linux設備號
- 加載和卸載
- 注冊
- 新字符設備注冊
- 設備節點
- 自動創建設備節點
- 編譯
- 編譯驅動程序
- 編譯應用程序
- 地址映射
- ioctrl
- 命令碼的解析
- 并發與競爭
- 原子操作
- 自旋鎖
- 信號量
- 互斥體
- linux中斷
- DMA映射
- 其它
- printk
- memcpy
- volatile關鍵字
- 用戶訪問內核
Linux驅動
概念
驅動充當著硬件與應用軟件之間的橋梁(上面是系統調用,下面是硬件)。
Linux 驅動屬于內核的一部分,因此驅動運行于內核空間。
驅動的具體任務:
- 讀寫設備寄存器(實現控制的方式);
- 完成設備的輪詢、中斷處理、DMA通信(CPU與外設通信的方式);
- 進行物理內存向虛擬內存的映射(在開啟硬件MMU的情況下);
驅動的兩個方向:
- 操作硬件(向下);
- 將驅動程序通入內核,實現面向操作系統內核的接口內容,接口由操作系統實現(向上)

用戶空間不能直接對內核進行操作,必須使用**“系統調用”**的方法來實現從用戶空間’‘陷入’'到內核空間,這樣才能實現對底層驅動的操作。
應用程序調用驅動程序流程
驅動加載成功以后會在“/dev”目錄下生成一個相應的文件, 應用程序通過對這個名為“/dev/xxx”(xxx 是具體的驅動文件名字)的文件進行相應的操作即可實現對硬件的操作。
open(close)函數:打開(關閉)/dev/xxxx
write(read)函數:向驅動寫入數據(從驅動讀取數據)
mmap函數:用于將設備的內存映射到進程空間中
應用程序使用到的函數在具體驅動程序中都有與之對應的函數,每一個系統調用,在驅動中都有與之對應的一個驅動函數。
驅動模塊的加載
在 Linux 內核文件 include/linux/fs.h 中有個叫做file_operations的結構體,此結構體就是 Linux 內核驅動具體操作函數的集合。可以通過重新定義這些函數,來實現自己想要的功能。
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 (*read_iter) (struct kiocb *, struct iov_iter *); ssize_t (*write_iter) (struct kiocb *, struct iov_iter *); int (*iterate) (struct file *, struct dir_context *); int (*iterate_shared) (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 (*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, gned 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 **, void **); long (*fallocate)(struct file *file, int mode, loff_t offset, loff_t len); void (*show_fdinfo)(struct seq_file *m, struct file *f); #ifndef CONFIG_MMU unsigned (*mmap_capabilities)(struct file *); #endif ssize_t (*copy_file_range)(struct file *, loff_t, struct file *, loff_t, size_t, unsigned int); int (*clone_file_range)(struct file *, loff_t, struct file *, loff_t, u64); ssize_t (*dedupe_file_range)(struct file *, u64, u64, struct file *, u64); } __randomize_layout;
linux設備號
Linux 中每個設備都有一個設備號(32位的dev_t類型),由主設備號(高12位)和次設備號(低20位)兩部分組成,主設備號范圍為0~4096,不可超出范圍。主設備號表示某一個具體的驅動,次設備號表示使用這個驅動的各個設備。
一些設備號的操作函數:
從dev_t中獲取主設備號:
MAJOR(dev)
從dev_t中獲取從設備號:
MANOR(dev)
將給定的主設備號和次設備號的值組合成 dev_t 類型的設備號:
MKDEV(ma,mi)
加載和卸載
將驅動編譯成模塊(.ko),linux啟動以后,使用相應命令加載驅動:
加載驅動模塊:
insmod xxx.ko
卸載模塊:
rmmod xxx.ko
查看系統中加載的所有模塊及模塊間的依賴關系:
ismod
注冊
編寫驅動程序時,需要向內核注冊模塊加載函數(加載驅動時,module_init會被調用(入口);卸載驅動時,module_exit會被調用(出口)):
static struct file_operations chrdevbase_fops = {
/*傳輸的函數名稱*/
};
/* 驅動入口函數 */
static int __init xxx_init(void)
{
/* 入口函數具體內容 */
return 0;
} /* 驅動出口函數 */
static void __exit xxx_exit(void)
{
/* 出口函數具體內容 */
} /* 將上面兩個函數指定為驅動的入口和出口函數 */
module_init(xxx_init);
module_exit(xxx_exit);
/*添加模塊license信息*/
MODULE_LICENSE("GPL");
新字符設備注冊
cdev結構體:
struct cdev { struct kobject kobj; struct module *owner; const struct file_operations *ops; struct list_head list; dev_t dev; unsigned int count; };
-
對cdev變量初始化:
cdev_init(&cdev, &fops);
,cdev就是要初始化的cdev結構體變量,fops是字符設備文件操作函數集合。 -
向linux系統添加字符設備:
cdev_add(&cdev, devid, 1);
,cdev是要添加的字符設備,devid是該設備的設備號,count是要添加的設備數量。 -
卸載驅動時要刪除字符設備:
cdev_del(&cdev);
內核動態分配設備號:alloc_chrdev_region(dev, basemibor, count, name)
,dev用來獲取設備號,baseminor是次設備號起始值,count是次設備號個數,name是設備名稱,返回值為0代表錯誤。
釋放設備號:unregister_chrdev_region(from, count)
,from是要釋放的設備號,count是從from開始要釋放的設備號數量。
設備節點
驅動加載成功后需要在/dev目錄下創建一個與之對應的設備節點文件,應用程序就是通過操作這個設備節點文件來完成對具體設備的操作:mknod /dev/chrdevbase c 200 0
“mknod”是創建節點命令,“/dev/chrdevbase”是要創建的節點文件,“c”表示這 是個字符設備,“200”是設備的主設備號,“0”是設備的次設備號。創建完成以后就會存在 /dev/chrdevbase 這個文件
自動創建設備節點
在驅動入口函數創建類和設備。
-
創建類:
class_create(owner, name)
,owner一般為固定的THIS_MODULE,name是類名字。刪除類:
class_destroy(cls);
,cls是要刪除的類。 -
在類下創建設備:
device_create(class,parent,devt,drvdata,fmt)
,其中class就是設備要創建于哪個類下面,parent和drvdata一般為0,devt是設備號,fmt是設備名字(如果設置fmt=xxx,就會生成/dev/xxx這個設備文件)。刪除設備:
device_destroy(class,devt)
,class是要刪除的類,devt是要刪除的設備號。 -
將設備的屬性信息寫成結構體:
struct test_dev{ dev_t devid; /* 設備號 */ struct cdev cdev; /* cdev */ struct class *class; /* 類 */ struct device *device; /* 設備 */ int major; /* 主設備號 */ int minor; /* 次設備號 */ };
編譯
編譯驅動程序
obj-m表示將.c文件編譯為模塊,-C表示將當前目錄切換到指定目錄,加入M=dir以后程序會自動到指定的 dir 目錄中讀取模塊的源碼并將其編譯為.ko 文件,modules表示編譯模塊。
KERN_DIR := $linux_pathobj-m := xxx.oall:make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- -C $(KERN_DIR) M=`pwd` modulesclean:make -C $(KERN_DIR) M=`pwd` clean
編譯應用程序
測試 APP 是要在 ARM 開發板上運行的,所以需要使用 arm-linux-gnueabihf-gcc 來編譯:arm-linux-gnueabihf-gcc xxx.c -o xxx
地址映射
MMU(內存管理單元)功能:
- 完成虛擬空間到物理空間的映射;
- 內存保護,設置存儲器的訪問權限,設置虛擬存儲空間的緩沖特性;
32位處理器虛擬地址范圍是2^32=4GB
CPU只能訪問虛擬地址,不能直接向寄存器地址寫入數據,必須通過寄存器物理地址在Linux系統中對應的虛擬地址。
地址映射函數:ioremap(phys_addr,size)
,phys_addr是要映射給的物理起始地址,size是要映射的內存空間大小。
#define APER_CLK_CTRL 0xF800012C
static void __iomem *aper_clk_ctrl_addr;
aper_clk_ctrl_addr = ioremap(APER_CLK_CTRL, 4);
釋放映射函數:iounmap(addr)
,其中addr是要取消映射的虛擬地址空間首地址。
iounmap(aper_clk_ctrl_addr);
IO內存寫入函數:iowrite32(v,p)
,p為寫入的虛擬地址,v為寫入數據的地址。
ioctrl
ioctrl是設備接口控制函數,一些無法歸類于file_operations所列功能的函數可以統一放在ioctrl這個函數操作中,對應于file_operations結構體的unlocked_ioctl成員。ioctrl函數里實現了多個對硬件的操作,應用層通過傳入命令來調用相應的操作。
原型:int (*ioctl) (struct inode * node, struct file *filp, unsigned int cmd, unsigned long arg);
使用:
ioctrl(fd,cmd,arg);
fd:文件標識符,一般對應設備文件/dev/xxx
cmd:命令碼
arg:用戶傳遞的數據的地址
ioctrl本質上就是用戶空間向內核空間提交一段具有特定含義的命令碼,內核空間根據內核規定好的方式,對命令碼進行解析,執行對應的底層操作。即:命令碼和底層操作應該是一一對應的,一個具體的命令碼就代表了一次底層操作的全部信息。
命令碼的解析
每個命令碼由32bit組成:
bit 位數 | 31 : 30 | 29 : 16 | 15 : 8 | 7 : 0 |
---|---|---|---|---|
代表含義 | 數據傳輸方向 | 數據傳遞大小 | 設備類型碼(魔數) | 功能碼 |
占用bit數 | 2 | 14 | 8 | 8 |
可以使用內核定義好的宏定義簡化命令碼的封裝:如_IOR(設備碼,功能碼,變量類型)
。
數據傳輸方向:
[00] 表示不傳遞數據,內核宏定義為: _IO
[10] 表示只讀,內核定義為: _IOR
[01] 表示只寫,內核宏定義為: _IOW
[11] 表示可讀可寫,內核宏定義為: _IOWR
數據傳遞大小:使用宏定義時,需填寫數據類型,如無符號32位就要填int。
設備類型碼:每個驅動通過一個唯一的字符來代表,只是為了區分設備,可以為任意char型字符,如‘a’,‘b’。
功能碼:區分不同的功能,可以為任意無符號整型數據,范圍為0~255,如果定義了多個 ioctl 命令,通常從 0 開始編號遞增。
舉例:
#include <sys/ioctl.h>/* 使用宏定義封裝命令碼 表示只寫 設備類型碼為 'L' 點燈功能碼為 10 or 11 傳輸數據大小為 int 的大小*/
#define LED_ON_FUNC _IOW('L', 10, int)
#define LED_OFF_FUNC _IOW('L', 11, int)
.........
int main(int argc, const char *argv[])
{int fd; // 文件描述符int ledSwitch = 0; // 用來表示操作哪盞 LEDif((fd = open("/dev/ledDev", O_RDWR)) == -1){perror("Open dev failed");return -1;}ledSwitch = 2; // 表示要操作 LED2/* 使用 ioctl 函數 注意傳入的是 ledSwitch 的地址*/if((ioctl(fd, LED_ON_FUNC, &ledSwitch)) == -1){perror("ioctl failed");return -1;}
}
并發與競爭
在驅動開發中要注意處理對共享資源的并發訪問,保護多個線程都會訪問的共享數據。
臨界區就是共享數據段,對于臨界區必須保證一次只有一個線程訪問。
原子操作
為避免競爭,將一些指令作為一個整體運行,即作為一個原子存在,只能對整形變量或者位進行保護。
定義原子變量:atomic_t a;
示例:
atomic_t v = ATOMIC_INIT(0); /* 定義并初始化原子變零 v=0 */ atomic_set(10); /* 設置 v=10 */
atomic_read(&v); /* 讀取 v 的值,肯定是 10 */
atomic_inc(&v); /* v 的值加 1,v=11 */
自旋鎖
當一個線程要訪問某個共享資源的時候首先要先獲取相應的鎖,鎖只能被一個線程持有, 只要此線程不釋放持有的鎖,那么其他的線程就不能獲取此鎖。對于自旋鎖而言,如果自旋鎖 正在被線程 A 持有,線程 B 想要獲取自旋鎖,那么線程 B 就會處于忙循環-旋轉-等待狀態。
一 般 在 線 程 中 使 用 spin_lock_irqsave/ spin_unlock_irqrestore , 在 中 斷 服 務 函 數 中 使 用 spin_lock/spin_unlock。
注意:
①因為在等待自旋鎖的時候處于“自旋”狀態,因此鎖的持有時間不能太長,一定要短, 否則的話會降低系統性能。如果臨界區比較大,運行時間比較長的話要選擇其他的并發處理方 式,比如信號量和互斥體。
②自旋鎖保護的臨界區內不能調用任何可能導致線程休眠的 API 函數,否則的話可能導致死鎖。
③不能遞歸申請自旋鎖,因為一旦通過遞歸的方式申請一個你正在持有的鎖,那么你就 必須“自旋”,等待鎖被釋放,然而你正處于“自旋”狀態,根本沒法釋放鎖。結果就是自己 把自己鎖死了!
示例:
/* 定義并初始化一個自旋鎖 */ static spinlock_t lock;spin_lock_init(&lock);/* 線程 A */ void functionA (){ unsigned long flags; /* 中斷狀態 */ spin_lock_irqsave(&lock, flags); /* 獲取鎖 */ /* 臨界區 */ spin_unlock_irqrestore(&lock, flags); /* 釋放鎖 */ } /* 中斷服務函數 */ void irq() { spin_lock(&lock); /* 獲取鎖 */ /* 臨界區 */ spin_unlock(&lock); /* 釋放鎖 */ }
信號量
特點:
①因為信號量可以使等待資源線程進入休眠狀態,因此適用于那些占用資源比較久的場 合。
②因此信號量不能用于中斷,因為信號量會引起休眠,中斷不能休眠。
③如果共享資源的持有時間比較短,那就不適合使用信號量了,因為頻繁的休眠、切換 線程引起的開銷要遠大于信號量帶來的那點優勢。
示例:
struct semaphore sem; /* 定義信號量 */ sema_init(&sem, 1); /* 初始化信號量 */ down(&sem); /* 申請信號量 */
/* 臨界區 */
up(&sem); /* 釋放信號量 */
sem_t
是 信號量(semaphore)的類型定義,通常用于多線程或多進程之間的同步和互斥。信號量是一個非負整數,用于控制對共享資源的訪問。
信號量通常通過以下幾個函數來操作:
sem_init()
:初始化一個信號量。sem_post()
:增加信號量的值(釋放一個資源)。sem_wait()
:減少信號量的值(請求一個資源)。如果信號量的值為零,則調用線程將被阻塞,直到信號量的值大于零。sem_trywait()
:嘗試減少信號量的值。如果信號量的值大于零,則減少它并立即返回;如果信號量的值為零,則立即返回錯誤。sem_destroy()
:銷毀一個信號量。
互斥體
互斥訪問表示一次只有一個線程可以訪 問共享資源,不能遞歸申請互斥體。
特點:
①mutex 可以導致休眠,因此不能在中斷中使用 mutex,中斷中只能使用自旋鎖(因為中 斷不參與進程調度,如果一旦在中斷服務函數執行過程中休眠了,休眠了則意味著交出了 CPU 的使用權,CPU 使用權則跑到了其它線程了,那么就不能再回到中斷斷點處了)。
②和信號量一樣,mutex 保護的臨界區可以調用引起阻塞的 API 函數。
③因為一次只有一個線程可以持有 mutex,因此,必須由 mutex 的持有者釋放 mutex。 并且 mutex 不能遞歸上鎖和解鎖。
示例:
struct mutex lock; /* 定義一個互斥體 */ mutex_init(&lock); /* 初始化互斥體 */ mutex_lock(&lock); /* 上鎖 */ /* 臨界區 */ mutex_unlock(&lock); /* 解鎖 */
linux中斷
申請中斷:request_irq(irq,handler,flags,name,dev)
,irq是要申請的中斷號;handler是對應的中斷處理函數;flags是中斷標志;name是中斷名字(設置后可以在/proc/interrupts文件中看到);dev用于區分,可設為NULL;返回0中斷申請成功,其它負值則中斷申請失敗。
中斷標志:
IRQF_TRIGGER_RISI NG 上升沿觸發
IRQF_TRIGGER_FALL ING下降沿觸發
IRQF_TRIGGER_HIGH高電平觸發
IRQF_TRIGGER_LOW低電平觸發
IRQF_ONESHOT單次中斷,中斷執行一次就結束
IRQF_TRIGGER_NONE無觸發
釋放中斷:free_irq(irq,dev)
,irq是要釋放的中斷,dev可設為NULL。
中斷處理函數定義:irqreturn_t xxx (*irq_handler_t,int, void *)
,handler_t是要處理的中斷號;void要與request_irq 函數的 dev 參數保持一致;返回值使用如下形式:return IRQ_RETVAL(IRQ_HANDLED)
。
獲取設備節點:of_find_node_by_path(“node”)
,node是定義在設備樹中的節點名,返回值是設備節點的結構體。
從interupts屬性中提取設備號:irq_of_parse_and_map(struct device_node *dev, int index)
,dev是設備節點,index是索引號,返回中斷號。
DMA映射
DMA傳輸需要連續的物理地址,而內核中使用的都是虛擬地址,需要建立物理地址和虛擬地址的映射。
一致性DMA映射:A=dma_alloc_coherent(dev,size,handle,flag)
dev:struct device *類型,可設為NULL
size:要分配的內存大小
handle:返回的內存物理(起始)地址,DMA可用
flag:用于指定內存分配的類型和行為的標志位,可設為GFP_KERNEL
返回值A:內存的虛擬起始地址
調用此函數將會分配一段內存,handle將返回這段內存的實際物理地址供DMA使用,A是handle對應的虛擬地址供內核使用,對A和handle任意一個操作,都會改變這段內存緩沖區的內容。
dma_addr_t是一個數據類型,通常用于表示DMA傳輸中數據所在的物理地址。
其它
printk
printk運行在內核態,根據級別對消息分類。
#define KERN_SOH "\001" /* ASCII Start Of Header */
#define KERN_SOH_ASCII '\001' #define KERN_EMERG KERN_SOH "0" /* system is unusable */
#define KERN_ALERT KERN_SOH "1" /* action must be taken immediately */
#define KERN_CRIT KERN_SOH "2" /* critical conditions */
#define KERN_ERR KERN_SOH "3" /* error conditions */
#define KERN_WARNING KERN_SOH "4" /* warning conditions */
#define KERN_NOTICE KERN_SOH "5" /* normal but significant condition */
#define KERN_INFO KERN_SOH "6" /* informational */
#define KERN_DEBUG KERN_SOH "7" /* debug-level messages */
0優先級最高,默認消息級別為4,只有比7級別高的消息才能顯示在控制臺上。
memcpy
memcpy函數的功能是從源頭指向的內存塊拷貝固定字節數的數據到目標指向的內存塊。
memcpy用法:memcpy(destination,source,num)
,destination是要拷貝的目的地址內存起始地址,source是源頭內存塊起始地址,num是要拷貝的字節數。
volatile關鍵字
編譯器優化常用的方法有:將內存變量緩存到寄存器;調整指令順序充分利用CPU指令流水線。
volatile表示該變量隨時可能發生變化,使用volatile聲明變量時,總是從它所在的內存讀取數據,遇到這個關鍵字聲明的變量,編譯器對訪問該變量的代碼不再進行優化,從而可以提供對特殊地址的穩定訪問。
用戶訪問內核
內核空間到用戶空間的復制:copy_from_user(to,from,n)
,to為目標用戶空間的地址,from是要拷貝的內容的源內核空間地址,n是要拷貝的字節數,成功返回0。
用戶空間到內核空間的復制:copy_to_usr(to,from,n)