1. 內核工具和輔助函數
1.1宏container_of
container_of函數可以通過結構體的成員變量檢索出整個結構體
函數原型:
/*
pointer 指向結構體字段的指針
container_type 結構體類型
container_field 結構體字段名稱
返回值是一個指針
*/
container_of(pointer, container_type,container_field);
struct mcp23016 {
struct i2c_client *client;
struct gpio_chip chip;
};
static inline struct mcp23016* to_mcp23016(struct gpio_chip *gc)
{
return container_of(gc,struct mcp23016,chip);
}
1.2 鏈表
內核開發(fā)者只實現了循環(huán)雙鏈表,因為這個結構能夠實現FIFO和LIFO,并且內核開發(fā)者要保持最少代碼。 為了支持鏈表,代碼中要添加的頭文件是<linux/list.h>。內核中鏈表實現核心部分的數據結構
是struct list_head,其定義如下 :
struct list_head {
struct list_head *next, *prev;
};
實例代碼:
#include <linux/list.h>
struct car {
int door_number;
char *color;
char *model;
struct list_head list; /*內核的表結構 */
};
struct car *redcar = kmalloc(sizeof(*car),GFP_KERNEL);
struct car *bluecar = kmalloc(sizeof(*car),GFP_KERNEL);
/* 初始化每個節(jié)點的列表條目*/
INIT_LIST_HEAD(&bluecar->list);
INIT_LIST_HEAD(&redcar->list);
/* 為顏色和模型字段分配內存,并填充每個字段 */
list_add(&redcar->list, &carlist) ;
list_add(&bluecar->list, &carlist) ;
鏈表初始化:
struct list_head mylist;
INIT_LIST_HEAD(&mylist);
static inline void INIT_LIST_HEAD(struct list_head *list)
{
list->next = list;
list->prev = list;
}
添加鏈表節(jié)點:
static inline void list_add(struct list_head *new, struct list_head *head)
{
__list_add(new, head, head->next);
}
/*新節(jié)點都是添加在head的后面而不是最后*/
static inline void __list_add(struct list_head *new,struct list_head *prev, struct list_head *next)
{
next->prev = new;
new->next = next;
new->prev = prev;
prev->next = new;
}
刪除鏈表節(jié)點:
void list_del(struct list_head *entry);
list_del(&redcar->list);
鏈表遍歷:
list_for_each_entry(pos, head, member)
1.3等待隊列
等待隊列=鏈表+鎖
想要其入睡的每個進程都在該鏈表中排隊(因此被稱作等待隊列)并進入睡眠狀態(tài),直到條件變?yōu)檎?。等待隊列可以被看作簡單的進程鏈表和鎖。
wait_event_interruptible不會持續(xù)輪詢,而只是在被調用時評估條件。如果條件為假,則進程將進入TASK_INTERRUPTIBLE狀態(tài)并從運行隊列中刪除。之后,當每次在等待隊列中調用wake_up_interruptible時,都會重新檢查條件。如果wake_up_interruptible運行時發(fā)現條件為真,則等待隊列中的進程將被喚醒,并將其狀態(tài)設置為TASK_RUNNING。進程按照它們進入睡眠的順序喚醒。要喚醒在隊列中等待的所有進程,應該使用wake_up_interruptible_all。
如果調用了wake_up或wake_up_interruptible,并且條件仍然是FALSE,則什么都不會發(fā)生。如果沒有調用wake_up(或wake_up_interuptible),進程將永遠不會被喚醒。下面是一個等待隊列的例子:
#include <linux/module.h>
#include <linux/init.h>
#include <linux/sched.h>
#include <linux/time.h>
#include <linux/delay.h>
#include<linux/workqueue.h>
static DECLARE_WAIT_QUEUE_HEAD(my_wq);
static int condition = 0;
/* 聲明一個工作隊列*/
static struct work_struct wrk;
static void work_handler(struct work_struct *work)
{
printk("Waitqueue module handler %s\n",__FUNCTION__);
msleep(5000);
printk("Wake up the sleeping module\n");
condition = 1;
wake_up_interruptible(&my_wq);
}
static int __init my_init(void)
{
printk("Wait queue example\n");
INIT_WORK(&wrk, work_handler);
schedule_work(&wrk);//將work_handler加入工作隊列,等待調用(系統(tǒng)自動調)
printk("Going to sleep %s\n", __FUNCTION__);
wait_event_interruptible(my_wq, condition !=0);//卡在這里,直到work_handler被調用才繼續(xù)執(zhí)行
pr_info("woken up by the work job\n");
return 0;
}
void my_exit(void)
{
printk("waitqueue example cleanup\n");
}
module_init(my_init);
module_exit(my_exit);
MODULE_AUTHOR("John Madieu<john.madieu@foobar.com>");
MODULE_LICENSE("GPL");
等待隊列定義:
動態(tài)定義:
wait_queue_head_t my_wait_queue;
init_waitqueue_head(&my_wait_queue);
靜態(tài)定義:
DECLARE_WAIT_QUEUE_HEAD(name)
阻塞:
/*
* 如果條件為false,則阻塞等待隊列中的當前任務(進程)
*/
int wait_event_interruptible(wait_queue_head_t q, CONDITION);
解除阻塞:
/*
* 如果上述條件為true,則喚醒在等待隊列中休眠的進程
*/
void wake_up_interruptible(wait_queue_head_t *q);
工作隊列定義:
INIT_WORK(&wrk, work_handler);
工作隊列調度:
schedule_work(&wrk);//等待系統(tǒng)調用
工作隊列和等待隊列的區(qū)別:工作隊列會綁定函數執(zhí)行,等待隊列不會綁定函數,單純的當進程同步使用。
1.4內核延遲與定時器
Jiffy是在<linux/jiffies.h>中聲明的內核時間單位。為了理解Jiffy,需要引入一個新的常量HZ,它是jiffies在1s內遞增的次數。每個增量被稱為一個Tick。換句話說,HZ代表Jiffy的大小。HZ取決于硬件和內核版本,也決定了時鐘中斷觸發(fā)的頻率。
1.4.1標準定時器
定時器API
定時器在內核中表示為timer_list的一個實例
#include <linux/timer.h>
struct timer_list {
struct list_head entry; //雙向鏈表
unsigned long expires; //以jiffies為單位絕對值
struct tvec_t_base_s *base;
void (*function)(unsigned long);
unsigned long data;
);
1.設置定時器
void setup_timer( struct timer_list *timer, void (*function)(unsigned long),
unsigned long data);
也可以使用這個函數
void init_timer(struct timer_list *timer);
2.設置過期時間。當定時器初始化時,需要在啟動回調之前設置它的過期時間:
int mod_timer( struct timer_list *timer,unsigned long expires);
3.釋放定時器。定時器用過之后需要釋放
void del_timer(struct timer_list *timer);
int del_timer_sync(struct timer_list *timer);
樣例:
#include <linux/init.h>
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/timer.h>
static struct timer_list my_timer;
void my_timer_callback(unsigned long data)
{
printk("%s called (%ld).\n", __FUNCTION__,jiffies);
}
static int __init my_init(void)
{
int retval;
printk("Timer module loaded\n");
setup_timer(&my_timer, my_timer_callback,0);
printk("Setup timer to fire in 300ms(%ld)\n", jiffies);
retval = mod_timer( &my_timer, jiffies + msecs_to_jiffies(300) );
if (retval)
printk("Timer firing failed\n");
return 0;
}
static void my_exit(void)
{
int retval;
retval = del_timer(&my_timer);
/* 定時器仍然是活動的(1)或沒有(0)*/
if (retval)
printk("The timer is still inuse...\n");
pr_info("Timer module unloaded\n");
}
module_init(my_init);
module_exit(my_exit);
1.4.2高精度定時器
高精度定時器由內核配置中的CONFIG_HIGH_RES_TIMERS選項啟用,其精度達到微秒(取決于平臺,最高可達納秒),而標準定時器的精度則為毫秒。標準定時器取決于HZ(因為它們依賴于jiffies),而HRT實現是基于ktime。
#include <linux/hrtimer.h>
struct hrtimer {
struct timerqueue_node node;
ktime_t _softexpires;
enum hrtimer_restart (*function)(structhrtimer *);
struct hrtimer_clock_base *base;
u8 state;
u8 is_rel;
};
1.初始化hrtimer。hrtimer初始化之前,需要設置ktime,它代表持續(xù)時間。
void hrtimer_init(struct hrtimer *time,clockid_t which_clock,enum hrtimer_mode mode);
2.啟動hrtimer
int hrtimer_start( struct hrtimer *timer,ktime_t time,const enum hrtimer_mode mode);
/*mode代表到期模式。對于絕對時間值,它應該是HRTIMER_MODE_ABS,對于相對于現在的時間值,應該是HRTIMER_MODE_REL。*/
3.取消hrtimer??梢匀∠〞r器或者查看是否可能取消它
int hrtimer_cancel( struct hrtimer *timer);
int hrtimer_try_to_cancel(struct hrtimer *timer);
/*這兩個函數當定時器沒被激活時都返回0,激活時返回1。這兩個函數之間的區(qū)別是,如果定時器處于激活狀態(tài)或其回調函數正在運行,則hrtimer_try_to_cancel會失敗,返回-1,而hrtimer_cancel將等待回調完成。*/
用下面的函數可以獨立檢查hrtimer的回調函數是否仍在運行:
int hrtimer_callback_running(struct hrtimer *timer);
1.5內核鎖機制
1.5.1互斥鎖
struct mutex {
/*
1: 解鎖, 0: 鎖定, negative: 鎖定, 可能的等待
其結構中有一個鏈表類型字段:wait_list,睡眠的原理是一樣的
*/
atomic_t count;
spinlock_t wait_lock;
struct list_head wait_list;
[...]
};
1.聲明
靜態(tài)聲明:
DEFINE_MUTEX(my_mutex);
動態(tài)聲明:
struct mutex my_mutex;
mutex_init(&my_mutex);
2.上鎖
void mutex_lock(struct mutex *lock);
int mutex_lock_interruptible(struct mutex *lock); //驅動程序可以被所有信號中斷,推薦
int mutex_lock_killable(struct mutex *lock); //只有殺死進程的信號才能中斷驅動程序
3.解鎖
void mutex_unlock(struct mutex *lock);
有時,可能需要檢查互斥鎖是否鎖定。為此使用int mutex_is_locked(struct mutex *lock)函數
這個函數只是檢查互斥鎖的所有者是否為空(NULL)。還有一個函數int mutex_trylock(struct mutex *lock),如果還沒有鎖定,則它獲取互斥鎖,并返回1;否則返回0。
實例:
struct mutex my_mutex;
mutex_init(&my_mutex);
/* 在工作或線程內部*/
mutex_lock(&my_mutex);
access_shared_memory();
mutex_unlock(&my_mutex)
互斥鎖使用須知:
- 一次只能有一個任務持有互斥鎖;這其實不是規(guī)則,而是事實。
- 多次解鎖是不允許的。
- 它們必須通過API初始化。
- 持有互斥鎖的任務不可能退出,因為互斥鎖將保持鎖定,可能的競爭者會永遠等待(將睡眠)。
- 不能釋放鎖定的內存區(qū)域。
- 持有的互斥鎖不得重新初始化。
- 由于它們涉及重新調度,因此互斥鎖不能用在原子上下文中,如Tasklet和定時器。
與wait_queue一樣,互斥鎖也沒有輪詢機制。每次在互斥鎖上調用mutex_unlock時,內核都會檢查wait_list中的等待者。如果有等待者,則其中的一個(且只有一個)將被喚醒和調度;它們喚醒的順序與它們入睡的順序相同。
1.5.2自旋鎖
spinlock_t my_spinlock;
spin_lock_init(my_spinlock);
static irqreturn_t my_irq_handler(int irq, void *data)
{
unsigned long status, flags;
/*
spin_lock_irqsave()函數會在獲取自旋鎖之前,禁止當前處理器(調用該函數的處理器)上中斷。
spin_lock_irqsave在內部調用local_irq_save (flags)和preempt_disable(),前者是一個依賴于體系結構的函數,用于保存IRQ狀態(tài),后者禁止在相關CPU上發(fā)生搶占。
*/
spin_lock_irqsave(&my_spinlock, flags);
status = access_shared_resources();
spin_unlock_irqrestore(&gpio->slock, flags); //釋放鎖
return IRQ_HANDLED;
}
自旋鎖與互斥鎖的區(qū)別:
-
互斥鎖保護進程的關鍵資源,而自旋鎖保護IRQ處理程序的關鍵部分。
-
互斥鎖讓競爭者在獲得鎖之前睡眠,而自旋鎖在獲得鎖之前一直自旋循環(huán)(消耗CPU)。
-
鑒于上一點,自旋鎖不能長時間持有,因為等待者在等待取鎖期間會浪費CPU時間;而互斥鎖則可以長
時間持有,只要保護資源需要,因為競爭者被放入等待隊列中進入睡眠狀態(tài)。
1.6工作延時機制
延遲是將所要做的工作安排在將來執(zhí)行的一種方法,這種方法推后發(fā)布操作。顯然,內核提供了一些功能來實現這種機制;它允許延遲調用和執(zhí)行任何類型函數。下面是內核中的3項功能。
SoftIRQ(執(zhí)行在原子上下文) ,Tasklet (執(zhí)行在原子上下文),工作隊列 (執(zhí)行在進程上下文)
1.6.1 Tasklet
Tasklet構建在Softirq之上的下半部(稍后將會看到這意味著什么)機制。它們在內核中表示為struct
tasklet_struct的實例:
struct tasklet_struct
{
struct tasklet_struct *next;
unsigned long state;
atomic_t count;
void (*func)(unsigned long);
unsigned long data;
};
1、聲明:
動態(tài)聲明:
void tasklet_init(struct tasklet_struct *t,void (*func)(unsigned long), unsigned long data);
靜態(tài)聲明:
DECLARE_TASKLET( tasklet_example,tasklet_function, tasklet_data );
DECLARE_TASKLET_DISABLED(name, func, data);
這兩個函數有一個區(qū)別,前者創(chuàng)建的Tasklet已經啟用,并準備好在沒有任何其他函數調用的情況下被調度,這通過將count字段設置為0來實現;而后者創(chuàng)建的Tasklet被禁用(通過將count設置為1來實現),必須在其上調用tasklet_enable ()之后,才可以調度這一Tasklet。
2、啟動和禁用TaskLet
void tasklet_enable(struct tasklet_struct *);
void tasklet_disable(struct tasklet_struct *); //本次tasklet執(zhí)行后返回
void tasklet_disable_nosync(struct tasklet_struct *); //直接終止執(zhí)行立刻返回
3、TaskLet調度
void tasklet_schedule(struct tasklet_struct *t);
void tasklet_hi_schedule(struct tasklet_struct *t);
內核把普通優(yōu)先級和高優(yōu)先級的Tasklet維護在兩個不同的鏈表中。tasklet_schedule將Tasklet添加到普通優(yōu)先級鏈表中,用TASKLET_SOFTIRQ標志調度相關的Softirq。tasklet_hi_schedule將Tasklet添加到高優(yōu)先級鏈表中,并用HI_SOFTIRQ標志調度相關的Softirq。高優(yōu)先級Tasklet旨在用于具有低延遲要求的軟中斷處理程序。
4、終止TaskLet
void tasklet_kill(struct tasklet_struct *t);
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/interrupt.h>
char tasklet_data[]="We use a string; but it could be pointer to a structure";
/* Tasklet處理程序,只打印數據 */
void tasklet_work(unsigned long data)
{
printk("%s\n", (char *)data);
}
DECLARE_TASKLET(my_tasklet, tasklet_work,(unsigned long)tasklet_data);
static int __init my_init(void)
{
/*
* 安排處理程序
* 從中斷處理程序調度Tasklet arealso
*/
tasklet_schedule(&my_tasklet);
return 0;
}
void my_exit(void)
{
tasklet_kill(&my_tasklet);
}
module_init(my_init);
module_exit(my_exit);
MODULE_AUTHOR("John Madieu<john.madieu@gmail.com>");
MODULE_LICENSE("GPL");
1.6.2工作隊列
作為延遲機制,工作隊列采用的方法與我們之前介紹的方法相反,它只能運行在搶占上下文中。如果需要在中斷下半部睡眠,工作隊列則是唯一的選擇 。
#include <linux/module.h>
#include <linux/init.h>
#include <linux/sched.h> /* 睡眠 */
#include <linux/wait.h> /* 等待列隊 */
#include <linux/time.h>
#include <linux/delay.h>
#include <linux/slab.h> /* kmalloc() */
#include <linux/workqueue.h>
//static DECLARE_WAIT_QUEUE_HEAD(my_wq);
static int sleep = 0;
struct work_data {
struct work_struct my_work;
wait_queue_head_t my_wq;
int the_data;
};
static void work_handler(struct work_struct *work)
{
struct work_data *my_data =container_of(work, struct work_data, my_work);
printk("Work queue module handler: %s, data is %d\n", __FUNCTION__,my_data->the_data);
msleep(2000);
wake_up_interruptible(&my_data->my_wq);
kfree(my_data);
}
static int __init my_init(void)
{
struct work_data * my_data;
my_data = kmalloc(sizeof(struct work_data),GFP_KERNEL);
my_data->the_data = 34;
INIT_WORK(&my_data->my_work, work_handler);
init_waitqueue_head(&my_data->my_wq);
schedule_work(&my_data->my_work);
printk("I'm goint to sleep ...\n");
wait_event_interruptible(my_data->my_wq,sleep != 0);
printk("I am Waked up...\n");
return 0;
}
static void __exit my_exit(void)
{
printk("Work queue module exit: %s %d\n",
__FUNCTION__, __LINE__);
}
module_init(my_init);
module_exit(my_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("John Madieu<john.madieu@gmail.com> ");
MODULE_DESCRIPTION("Shared workqueue");
1.7內核中斷
注冊中斷函數
int request_irq(unsigned int irq, irq_handler_t handler,unsigned long flags, const char *name, void *dev);
flag表示掩碼:
IRQF_TIMER:通知內核這個處理程序是由系統(tǒng)定時器中斷觸發(fā)的。
IRQF_SHARED:用于兩個或多個設備共享的中斷線。共享這個中斷線的所有設備都必須設置該標志。如果被忽略,將只能為該中斷線注冊一個處理程序。
IRQ_ONESHOT:主要在線程中斷中使用,它要求內核在硬中斷處理程序沒有完成之前,不要重新啟用該中斷。在線程處理程序運行之前,中斷會一直保持禁用狀態(tài)。
name:內核用來標識/proc/interrupts和/proc/irq中的驅動程序
dev:其主要用途是作為參數傳遞給中斷處理程序,這對每個中斷處理程序都是唯一的,因為它用來標識這個設備。對于非共享中斷,它可以是NULL,但共享中斷不能為NULL。使用它的常見方法是提供設備結構,因為它既獨特,又可能對處理程序有用。也就是說,指向有一個指向設備數據結構的指針就足夠了。
struct my_data {
struct input_dev *idev;
struct i2c_client *client;
char name[64];
char phys[32];
};
static irqreturn_t my_irq_handler(int irq, void*dev_id)
{
struct my_data *md = dev_id;
unsigned char nextstate = read_state(lp);
/* Check whether my device raised the irq or no */
[...]
return IRQ_HANDLED;
}
/* 在probe函數的某些位置 */
int ret;
struct my_data *md = kzalloc(sizeof(*md), GFP_KERNEL);
ret = request_irq(client->irq, my_irq_handler,IRQF_TRIGGER_LOW |IRQF_ONESHOT,DRV_NAME, md);
/* 在釋放函數中*/
free_irq(client->irq, md);
中斷處理函數:
static irqreturn_t my_irq_handler(int irq, void *dev);
中斷返回值:
IRQ_NONE:設備不是中斷的發(fā)起者(在共享中斷線上尤其會出現這種情況)。
IRQ_HANDLED:設備引發(fā)中斷
中斷上半部處理緊急事件,中斷下半部用工作隊列等延遲機制處理不緊急事件,并且上半部處理的時候需要將所有中斷全部緊張,避免中斷嵌套,等上半部處理完再開啟中斷。
2.字符設備驅動程序
2.1設備文件操作
內核把文件描述為inode結構(不是文件結構)的實例,inode結構在include/linux/fs.h中定義:
struct inode {
[...]
struct pipe_inode_info *i_pipe; /* 如果這是Linux內核管道,則設置并使用 */
struct block_device *i_bdev; /* 如果這是塊設備,則設置并使用 */
struct cdev *i_cdev; /* 如果這是字符設備,則設置并使用 */
[...]
}
struct inode是文件系統(tǒng)的數據結構,它只與操作系統(tǒng)相關,用于保存文件(無論它的類型是字符、塊、管道等)或目錄(從內核的角度來看,目錄也是文件,是其他文件的入口點)信息。
struct file結構(也在include/linux/fs.h中定義)是更高級的文件描述,它代表內核中打開的文件,依賴于低層的struct inode數據結構:
struct file {
[...]
struct path f_path; /* 文件路徑 */
struct inode *f_inode; /* 與此文件相關的inode */
const struct file_operations *f_op; /* 可以在此文件上執(zhí)行的操作 */
loff_t f_pos; /* 此文件中光標的位置 */
/* 需要tty驅動程序等 */
void *private_data; /* 驅動程序可以設置的私有數據,以便在文件操作之間共享,這可以指向任何結構*/
[...]
}
struct inode和struct file的區(qū)別在于,inode不跟蹤文件的當前位置和當前模式,它只是幫助操作系統(tǒng)找到底層文件結構的內容(管道、目錄、常規(guī)磁盤文件、塊/字符設備文件等)。而struct file則是一個基本結構(它實際上持有一個指向struct inode的指針),它代表打開的文件,并且提供一組函數,它們與底層文件結構上執(zhí)行的方法相關,這些方法包括open、write、seek、read、select等。所有這一切都強化了UNIX系統(tǒng)的哲學:一切皆是文件。
2.2分配和注冊設備
字符設備在內核中表示為struct cdev的實例。在編寫字符設備驅動程序時,目標是最終創(chuàng)建并注冊與struct file_operations關聯(lián)的結構實例,為用戶空間提供一組可以在該設備上執(zhí)行的操作(函數)。為了實現這個目標,必須執(zhí)行以下幾個步驟。
(1)使用alloc_chrdev_region()保留一個主設備號和一定范圍的次設備號。
(2)使用class_create()創(chuàng)建自己的設備類,該函數在/sys/class中定義。
(3)創(chuàng)建一個struct file_operation(傳遞給cdev_init),每一個設備都需要創(chuàng)建,并調用call_init和cdev_add()注冊這個設備。
(4)調用device_create()創(chuàng)建每個設備,并給它們一個合適的名字。這樣,就可在/dev目錄下創(chuàng)建出設備。
2.3 ioctrl
ioctrl原型:
_IO(MAGIC, SEQ_NO)
_IOW(MAGIC, SEQ_NO, TYPE)
_IOR(MAGIC, SEQ_NO, TYPE)
_IORW(MAGIC, SEQ_NO, TYPE)
eep_ioctl.h:
#ifndef PACKT_IOCTL_H
#define PACKT_IOCTL_H/*
* 需要為驅動選擇一個數字,以及每個命令的序列號
*/
#define EEP_MAGIC 'E'
#define ERASE_SEQ_NO 0x01
#define RENAME_SEQ_NO 0x02
#define ClEAR_BYTE_SEQ_NO 0x03
#define GET_SIZE 0x04
/*
* 分區(qū)名必須是最大32字節(jié)
*/
#define MAX_PART_NAME 32
/*
* 定義ioctl編號
*/
#define EEP_ERASE _IO(EEP_MAGIC, ERASE_SEQ_NO)
#define EEP_RENAME_PART _IOW(EEP_MAGIC,RENAME_SEQ_NO, unsigned long)
#define EEP_GET_SIZE _IOR(EEP_MAGIC, GET_SIZE,int *)
#endif
long ioctl(struct file *f, unsigned int cmd,unsigned long arg);
ioctrl步驟:使用switch-case來調用自定義函數
/*
* 用戶空間代碼還需要包括定義ioctls的頭文件,這里是eep_iocl.h
*/
#include "eep_ioctl.h"
static long eep_ioctl(struct file *f, unsigned int cmd, unsigned long arg)
{
int part;
char *buf = NULL;
int size = 1300;
switch(cmd){
case EEP_ERASE:
erase_eepreom();
break;
case EEP_RENAME_PART:
buf = kmalloc(MAX_PART_NAME,GFP_KERNEL);
copy_from_user(buf, (char *)arg,MAX_PART_NAME);
rename_part(buf);
break;
case EEP_GET_SIZE:
copy_to_user((int*)arg, &size,sizeof(int));
break;
default:
return -ENOTTY;
return 0;
}
用戶程序調ioctrl:
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#include "eep_ioctl.h" /* our ioctl header file*/
int main()
{
int size = 0;
int fd;
char *new_name = "lorem_ipsum"; /* 不超過MAX_PART_NAME */
fd = open("/dev/eep-mem1", O_RDWR);
if (fd == -1){
printf("Error while opening the eeprom\n");
return -1;
}
ioctl(fd, EEP_ERASE); /* 調用ioctl來擦除分區(qū)*/
ioctl(fd, EEP_GET_SIZE, &size); /* 調用ioctl獲取分區(qū)大小 */
ioctl(fd, EEP_RENAME_PART, new_name); /*調用ioctl來重命名分區(qū) */close(fd);
return 0;
}
用戶空間ioctl傳遞fd和cmd給內核空間,內核空間根據cmd用switch-case調用對應的函數處理。cmd在頭文件通過_IO(MAGIC, SEQ_NO)等io宏定義即可。
3.平臺設備驅動程序
3.1平臺驅動程序
I2C設備或SPI設備是平臺設備,但分別依賴于I2C或SPI總線,而不是平臺總線。
對于平臺驅動程序一切都需要手動完成。平臺驅動程序必須實現probe函數,在插入模塊或設備聲明時,內核調用它。在開發(fā)平臺驅動程序時,必須填寫主結構struct platform_driver,并用專用函數把驅動程序注冊到平臺總線核,如下所示:
static struct platform_driver mypdrv = {
.probe = my_pdrv_probe, /*設備匹配后聲明驅動程序時所調用的函數。*/
.remove = my_pdrv_remove,
.driver = {
.name = "my_platform_driver",
.owner = THIS_MODULE,
},
}
在內核中注冊平臺驅動程序很簡單,只需在init函數中調用platform_driver_register()或platform_driver_probe()(模塊加載時)。這兩個函數之間的區(qū)別如下。
·platform_driver_register():注冊驅動程序并將其放入由內核維護的驅動程序列表中,以便每當發(fā)現新的匹配時就可以按需調用其probe()函數。為防止驅動程序在該列表中插入和注冊,請使用下一個函數。
·platform_driver_probe():調用該函數后,內核立即運行匹配循環(huán),檢查是否有平臺設備名稱匹配,如果匹配則調用驅動程序的probe(),這意味著設備存在;否則,驅動程序將被忽略。此方法可防止延遲探測,因為它不會在系統(tǒng)上注冊驅動程序。在這里,probe函數被放置在__init部分,當內核啟動完成時這個部分被釋放,從而防止了延遲探測并減少驅動程序的內存占用。如果100%確定設備存在于系統(tǒng)中,請使用此方法:
ret = platform_driver_probe(&mypdrv,my_pdrv_probe);
平臺驅動程序簡單樣例:
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/platform_device.h>
static int my_pdrv_probe (struct platform_device *pdev){
pr_info("Hello! device probed!\n");
return 0;
}
static void my_pdrv_remove(struct platform_device *pdev){
pr_info("good bye reader!\n");
}
static struct platform_driver mypdrv = {
.probe = my_pdrv_probe,
.remove = my_pdrv_remove,
.driver = {
.name = KBUILD_MODNAME,
.owner = THIS_MODULE,
},
};
static int __init my_drv_init(void)
{
pr_info("Hello Guy\n");/* 向內核注冊*/
platform_driver_register(&mypdrv);
return 0;
}
static void __exit my_pdrv_remove (void)
{
pr_info("Good bye Guy\n");
/* 從內核注銷 */
platform_driver_unregister(&my_driver);
}
module_init(my_drv_init);
module_exit(my_pdrv_remove);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("John Madieu");
MODULE_DESCRIPTION("My platform Hello World module");
每個總線都有特定的宏來注冊驅動程序,以下列表是其中的一部分。
·module_platform_driver(struct platform_driver):用于平臺驅動程序,專用于傳統(tǒng)物理總線以外的設備
·module_spi_driver (struct spi_driver):用于SPI驅動程序。
·module_i2c_driver (struct i2c_driver):用于I2C驅動程序。
·module_pci_driver(struct pci_driver):用于PCI驅動程序。
·module_usb_driver(struct usb_driver):用于USB驅動程序。
·module_mdio_driver(struct mdio_driver):用于MDIO。
3.2平臺設備
完成驅動程序后,必須向內核提供需要該驅動程序的設備。平臺設備在內核中表示為struct platform_device的實例,如下所示:
struct platform_device {
const char *name;
u32 id;
struct device dev;
u32 num_resources;
struct resource *resource;
};
3.1設備驅動總線匹配
在匹配發(fā)生之前,Linux會調用platform_match(struct device * dev,structdevice_driver * drv)。平臺設備通過字符串與驅動程序匹配。根據Linux設備模型,**總線元素是最重要的部分。每個總線都維護一個注冊的驅動程序和設備列表??偩€驅動程序負責設備和驅動程序的匹配。**每當連接新設備或者向總線添加新的驅動程序時,總線都會啟動匹配循環(huán)。
內核通過以下方式觸發(fā)I2C總線匹配循環(huán):調用由I2C總線驅動程序注冊的I2C核心匹配函數,以檢查是否有已注冊的驅動程序與該設備匹配。如果沒有匹配,則什么都不會發(fā)生;如果發(fā)現匹配,則內核將通知(通過netlink套接字通信機制)設備管理器(udev/mdev),由它加載(如果尚未加載)與設備匹配的驅動程序。一旦驅動程序加載完成,其probe()函數就立即執(zhí)行。
(1)內核設備與驅動程序匹配函數
內核中負責平臺設備和驅動程序匹配功能的函數在/drivers/base/platform.c中,定義如下:
static int platform_match(struct device *dev, struct device_driver *drv)
{
struct platform_device *pdev = to_platform_device(dev);
struct platform_driver *pdrv = to_platform_driver(drv);
/* 在設置driver_override時,只綁定到匹配的驅動程序*/
if (pdev->driver_override)
return !strcmp(pdev->driver_override,drv->name);
/* 嘗試一個樣式匹配*/
if (of_driver_match_device(dev, drv))
return 1;
/* 嘗試ACPI樣式匹配 */
if (acpi_driver_match_device(dev, drv))
return 1;
/* 嘗試匹配ID表 */
if (pdrv->id_table)
return platform_match_id(pdrv->id_table, pdev) != NULL;
/* 回退到驅動程序名稱匹配 */
return (strcmp(pdev->name, drv->name) == 0);
}
static const struct platform_device_id *platform_match_id(const struct platform_device_id *id, struct platform_device *pdev)
{
while (id->name[0])
{
if (strcmp(pdev->name, id->name)== 0) {
pdev->id_entry = id;
return id;
}
id++;
}
return NULL;
}
struct device_driver是每個設備驅動程序的基礎。無論是I2C、SPI、TTY,還是其他設備驅動程序,它們都嵌入
struct device_driver元素。
struct device_driver {
const char *name;
[...]
const struct of_device_id *of_match_table;
const struct acpi_device_id *acpi_match_table;
};
4.設備樹
4.1設備樹機制
將選項CONFIG_OF設置為Y即可在內核中啟用DT。要在驅動程序中調用DT API,必須添加以下頭文件:
#include <linux/of.h>
#include <linux/of_device.h>
4.1.1命名約定
每個節(jié)點都必須有 [@
]形式的名稱,其中是一個字符串,其長度最多為31個字符,[@ ]是可選的,具體取決于節(jié)點代表是否為可尋址的設備。i2c@021a0000 {
compatible = "fsl,imx6q-i2c", "fsl,imx21-i2c";
reg = <0x021a0000 0x4000>;
[...]
};
4.1.2處理中斷
中斷接口實際上分為兩部分,消費者端和控制器端。DT中用4個屬性描述中斷連接??刂破魇菫橄M者提供中斷線的設備。在控制器端有以下屬性?!nterrupt-controller:為了將設備標記為中斷控制器而應該定義的空(布爾)屬性?!?interrupt-cells:這是中斷控制器的屬性。它指出為該中斷控制器指定一個中斷要使用多少個單元。消費者是生成中斷的設備。消費者綁定需要以下屬性?!nterrupt-parent:對于產生中斷的設備節(jié)點,這個屬性包含指向設備所連接的中斷控制器節(jié)點的指針phandle。如果省略,則設備從其父節(jié)點繼承該屬性。
interrupts = <0 66 IRQ_TYPE_LEVEL_HIGH>;
·0:共享外設中斷(SPI),用于核間共享的中斷信號,可由GIC路由至任意核。
·1:專用外設中斷(PPI),專用于單核的中斷信號?!さ诙€單元格保存中斷號。該中斷號取決于中斷線是PPI還是SPI。
·第三個單元,這里的IRQ_TYPE_LEVEL_HIGH代表感知級別。所有可用的感知級別在include/linux/irq.h中定義。
5.I2C客戶端驅動程序
I2C驅動程序在內核中表示為struct i2c_driver的實例。I2C客戶端(代表設備本身)由struct i2c_client結構表示。
5.1i2c_driver結構
struct i2c_driver {
/* 標準驅動模型接口 */
int (*probe)(struct i2c_client *, const struct i2c_device_id *);
int (*remove)(struct i2c_client *);
/* 與枚舉無關的驅動類型接口 */
void (*shutdown)(struct i2c_client *);
struct device_driver driver;
const struct i2c_device_id *id_table;
};
struct i2c_driver
結構包含并描述通用訪問例程,這些例程是處理聲明驅動程序的設備所必需的,而struct i2c_client
則包含設備特有的信息,如其地址。struct i2c_client
結構表示和描述I2C設備
struct i2c_client {
unsigned short flags; /* div., 見下文 */
unsigned short addr; /* chip address - NOTE:7bit */
/* 地址被存儲在 _LOWER_ 7 bits */
char name[I2C_NAME_SIZE];
struct i2c_adapter *adapter; /* 適配器 */
struct device dev; /* 設備結構 */
int irq; /* 由設備發(fā)出的IR */
struct list_head detected;
#if IS_ENABLED(CONFIG_I2C_SLAVE)i2c_slave_cb_t slave_cb; /* 回調從設備 */
#endif
};
5.2普通I2C通信
int i2c_master_send(struct i2c_client *client,const char *buf, int count);
int i2c_master_recv(struct i2c_client *client,char *buf, int count);
幾乎所有I2C通信函數都以struct i2c_client作為第一個參數。第二個參數包含要讀取或寫入的字節(jié),第三個參數表示要讀取或寫入的字節(jié)數。像任何讀/寫函數一樣,返回值是讀/寫的字節(jié)數。也可以使用以下方式處理消息傳輸:
int i2c_transfer(struct i2c_adapter *adap,struct i2c_msg *msg, int num);
i2c_transfer發(fā)送一組消息,其中每個消息可以是讀取操作或寫入操作,也可以是它們的任意混合。請記住,每兩個事務之間沒有停止位。 i2c_msg結構描述和表示I2C消息。它必須包含每條消息的客戶端地址、消息的字節(jié)數和消息有效載荷。
struct i2c_msg {
__u16 addr; /* 從設備地址 */
__u16 flags; /* 信息標志 */
__u16 len; /* msg長度 */
__u8 *buf; /* 指向msg數據的指針 */
};
樣例:
ssize_t eep_read(struct file *filp, char __user *buf, size_t count, loff_t *f_pos)
{
[...]
int _reg_addr = dev->current_pointer;
u8 reg_addr[2];reg_addr[0] = (u8)(_reg_addr>> 8);
reg_addr[1] = (u8)(_reg_addr& 0xFF);
struct i2c_msg msg[2];
msg[0].addr = dev->client->addr;
msg[0].flags = 0; /* 寫入*/
msg[0].len = 2; /* 地址是2字節(jié)編碼 */
msg[0].buf = reg_addr;
msg[1].addr = dev->client->addr;
msg[1].flags = I2C_M_RD; /* 讀取*/
msg[1].len = count;
msg[1].buf = dev->data;
if (i2c_transfer(dev->client->adapter, msg,2) < 0)
pr_err("ee24lc512: i2c_transferfailed\n");
if (copy_to_user(buf, dev->data, count) !=0) {
retval = -EIO;
goto end_read;
}
[...]
}
6.Regmap API—寄存器映射抽象
內核版本3.1中引入了Regmap API,用于分解和統(tǒng)一內核開發(fā)人員訪問SPI/I2C設備的方式。接下來的問題是,無論它是SPI設備,還是I2C設備,只需要初始化、配置Regmap,并流暢地處理所有讀/寫/修改操作
6.1.1使用Regmap API編程
Regmap API非常簡單,只需了解幾個結構即可。這個API中的兩個重要結構是struct regmap_config(代表Regmap配置)和struct regmap(Regmap實例本身)。
struct regmap_config在驅動程序的生命周期中存儲Regmap配置,這里的設置會影響讀/寫操作,它是Regmap API中最重要的結構。
struct regmap_config {
const char *name;
int reg_bits;//寄存器地址中的位數
int reg_stride;
int pad_bits;
int val_bits;
bool (*writeable_reg)(struct device *dev,unsigned int reg);
/*回調函數。如果提供,則在需要寫入寄存器時供Regmap子系統(tǒng)使用。*/
bool (*readable_reg)(struct device *dev,unsigned int reg);
bool (*volatile_reg)(struct device *dev,unsigned int reg);
/*每當需要通過Regmap緩存讀取或寫入寄存器時調用它。*/
bool (*precious_reg)(struct device *dev,unsigned int reg);
regmap_lock lock;
regmap_unlock unlock;
void *lock_arg;
int (*reg_read)(void *context, unsigned intreg,unsigned int *val);
int (*reg_write)(void *context, unsigned intreg,unsigned int val);
bool fast_io;
unsigned int max_register;
const struct regmap_access_table *wr_table;
const struct regmap_access_table *rd_table;
const struct regmap_access_table *volatile_table;
const struct regmap_access_table *precious_table;
const struct reg_default *reg_defaults;
unsigned int num_reg_defaults;
enum regcache_type cache_type;
const void *reg_defaults_raw;
unsigned int num_reg_defaults_raw;
u8 read_flag_mask;
u8 write_flag_mask;
bool use_single_rw;
bool can_multi_write;
enum regmap_endian reg_format_endian;
enum regmap_endian val_format_endian;
const struct regmap_range_cfg *ranges;
unsigned int num_ranges;
}
7.內核內存管理
7.1Sla分配器
Slab分配器是kmalloc()所依賴的分配器。其主要目的是消除小內存分配情況下由伙伴系統(tǒng)引起的內存分配/釋放造成的碎片,加快常用對象的內存分配。
7.1.1伙伴系統(tǒng)
分配內存時,所請求的是大小被四舍五入為2的冪,伙伴分配器搜索相應的列表。如果請求列表中無項存在,則把下一個上部列表(其塊大小為前一列表的兩倍)的項拆分成兩部分(稱為伙伴)。分配器使用前半部分,而另一部分則向下添加到下一個列表中。這是一種遞歸方法,當伙伴分配器成功找到可以拆分的塊或達到最大塊大小并且沒有可用的空閑塊時,該遞歸方法停止。文章來源:http://www.zghlxwxcb.cn/news/detail-816190.html
7.1.2slab分配器概述
在介紹Slab分配器之前,先定義它使用的一些術語。
·Slab:這是由數個頁面幀組成的一塊連續(xù)的物理內存。每個Slab分成大小相同的塊,用于存儲特定類型的內核對象,例如inode、互斥鎖等。每個Slab是對象數組。文章來源地址http://www.zghlxwxcb.cn/news/detail-816190.html
到了這里,關于Linux設備驅動開發(fā)學習筆記(等待隊列,鎖,字符驅動程序,設備樹,i2C...)的文章就介紹完了。如果您還想了解更多內容,請在右上角搜索TOY模板網以前的文章或繼續(xù)瀏覽下面的相關文章,希望大家以后多多支持TOY模板網!