文章目錄
- 一、驅動程序基石
- 1-1 休眠與喚醒
- 1-2 POLL機制
- 1-3 異步通知
- (1) 異步通知程序解析
- (2) 異步通知機制內核代碼詳解
- 1-4 阻塞與非阻塞
- 1-5 定時器
- (1) 內核函數
- (2) 定時器時間單位
- 1-6 中斷下半部 tasklet
本人學習完韋老師的視頻,因此來復習鞏固,寫以筆記記之。
韋老師的課比較難,第一遍不知道在說什么,但是堅持看完一遍,再來復習,基本上就水到渠成了。
看完視頻復習的同學觀看最佳!
基于 IMX6ULL-PRO
參考視頻 Linux快速入門到精通視頻
參考資料:01_嵌入式Linux應用開發完全手冊V5.1_IMX6ULL_Pro開發板.pdf
一、驅動程序基石
1-1 休眠與喚醒
當應用程序必須等待某個事件發生,比如必須等待按鍵被按下時, 可以使用休眠-喚醒機制
① APP調用read等函數試圖讀取數據,比如讀取按鍵;
② APP進入內核態,也就是調用驅動中的對應函數,發現有數據則復制到用戶空間并馬上返回;
③ 如果APP在內核態,也就是在驅動程序中發現沒有數據,則APP休眠;
④ 當有數據時,比如當按下按鍵時,驅動程序的中斷服務程序被調用,它會記錄數據、喚醒APP
⑤ APP繼續運行它的內核態代碼,也就是驅動程序中的函數,復制數據到用戶空間并馬上返回。
驅動框架
休眠,直到condition 為真;休眠期間是可被打斷的,可以被信號打斷
wait_event_interruptible(wq, condition)
喚醒wq隊列中狀態為“ TASK_INTERRUPTIBLE ”的線程,只喚醒其中的一個線程
wake_up_interruptible(wq)
要休眠的線程,放在wq 隊列里,中斷處理函數從wq隊列里把它取出來喚
醒。
① 初始化wq隊列
② 在驅動的read函數中,調用 wait_event_interruptible。它本身會判斷
event是否為 FALSE ,如果為FASLE表示無數據,則休眠。
當從wait_event_interruptible 返回后,把數據復制回用戶空間。
③ 在中斷服務程序里:設置event 為TRUE,并調用wake_up_interruptible 喚醒線程。
1-2 POLL機制
使用休眠喚醒的方式等待某個事件發生時,有一個缺點:等待的時間可能
很久。我們可以加上一個超時時間,這時就可以使用poll機制。
① APP不知道驅動程序中是否有數據,可以先調用 poll函數查詢一下, poll函數可以傳入超時時間
② APP進入內核態,調用驅動程序的poll函數,有數據的話立刻返回;
③ 如果發現沒有數據時就休眠一段時間
④ 當有數據時,比如當按下按鍵時,驅動程序的中斷服務程序被調用,它會記錄數據、喚醒APP
⑤ 當超時時間到了之后,內核也會喚醒APP
⑥ APP根據poll函數的返回值就可以知道是否有數據,如果有數據就調用
read得到數據。
drv_poll函數需要做的事
① 把當前線程掛入隊列wq:poll_wait
②返回設備狀態:drv_poll 要返回自己的當前狀態:P OLLIN | POLLRDNORM) 或 POLLOUT | POLLWRNORM) 。
static unsigned int gpio_key_drv_poll(struct file *fp, poll_table * wait)
{printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);poll_wait(fp, &gpio_key_wait, wait);return is_key_buf_empty() ? 0 : POLLIN | POLLRDNORM;}
button_test.c
int main(int argc, char **argv)
{int fd;int val;struct pollfd fds[1];int timeout_ms = 5000;int ret;/* 1. 判斷參數 */if (argc != 2) {printf("Usage: %s <dev>\n", argv[0]);return -1;}/* 2. 打開文件 */fd = open(argv[1], O_RDWR);if (fd == -1){printf("can not open file %s\n", argv[1]);return -1;}fds[0].fd = fd;fds[0].events = POLLIN;while (1){/* 3. 讀文件 */ret = poll(fds, 1, timeout_ms);if((ret == 1) && (fds[0].revents & POLLIN)){read(fd, &val, 4);printf("get button : 0x%x\n", val);}else{printf("timeout\n");}}close(fd);return 0;
}
1-3 異步通知
(1) 異步通知程序解析
異步通知流程如下:
① APP給SIGIO這個信號注冊信號處理函數func,以后APP收到SIGIO信號時,這個函數會被自動調用。
② 把APP的PID(進程 ID)告訴驅動程序,這個調用不涉及驅動程序,在內核的文件系統層次記錄PID。
filp->f_owner.pid = get_pid(pid);
③ 讀取驅動程序文件Flag
④ 設置Flag里面的FASYNC位為1;當 FASYNC位發生變化時,會導致驅動程序的fasync被調用
⑤ 調用faync_helper ,它會根據 FAYSNC的值決定是否設置
button_async -->fa_file=filp(內含PID);open文件時,會在內核文件系統中有一個struct file *filp結構體,filp->f_owner.pid里面含有之前設置的PID 。
⑥ APP做其他事;當按下按鍵,發生中斷,驅動程序的中斷服務程序被調用,里面調用kill_fasync 發信號
⑦ APP收到信號后,它的信號處理函數被自動調用,可以在里面調用
read函數讀取按鍵。
驅動程序中提供對應的drv_fasync
函數,并在FAYNC變化時,調用fasync_helper
函數,使得button_fasync->fa_file = filp或者NULL
struct fasync_struct *button_fasync;
static int gpio_key_drv_fasync(int fd, struct file *file, int on)
{if (fasync_helper(fd, file, on, &button_fasync) >= 0)return 0;elsereturn -EIO;
}
在GPIO中斷服務程序中,若button_fasync->fa_file非空,則獲得PID,并發信號給上層應用
kill_fasync(&button_fasync, SIGIO, POLL_IN);
上層應用程序
(2) 異步通知機制內核代碼詳解
上層應用執行fcntl函數,內核會調用fs/fcntl.c 的如下函數。
進入do_fcntl函數,flag標志對應函數的cmd
以下分別對三種flag進行代碼演示
① 當flag是F_SETOWN
時,內核do_fcntl中調用f_setown函數,最終將pid給filp
fcntl(fd, F_SETOWN, getpid());
② 當flag是F_GETFL
時,獲取文件的狀態標志
flags = fcntl(fd, F_GETFL);
③ 當flag是F_SETFL時,設置文件支持異步通知功能
fcntl(fd, F_SETFL, flags | FASYNC);
啟動了FASYNC 功能的話,驅動程序的 button_fasync 就被設置了,它指向的 fasync_struct 結構體里含有 filp里含有PID
static int gpio_key_drv_fasync(int fd, struct file *file, int on)
{if (fasync_helper(fd, file, on, &button_fasync) >= 0)return 0;elsereturn -EIO;
}
從button_fasync 指針中,取出 fasync_struct 結構體,從這個結構體的 fa_file 中得到接收方的PID ,然后使用 send_sigio函數發送信號。根據 PID找到進程在內核的 task_struct結構體, 修改里面的某些成員表示收到了信號。
kill_fasync(&button_fasync, SIGIO, POLL_IN);
1-4 阻塞與非阻塞
所謂阻塞,就是等待某件事情發生。比如調用read讀取按鍵時,如果沒有按鍵數據則read函數不會返回,它會讓線程休眠等待。
使用poll時,傳入超時時間不為0(阻塞);設置超時時間為0,沒有數據立即返回(非阻塞)
如何設置阻塞與非阻塞呢?
① open時
int fd = open(“/dev/xxx”, O_RDWR | O_NONBLOCK); /* 非阻塞方式*/
int fd = open(“/dev/xxx”, O_RDWR ); /* 阻塞方式*/
② open后
int flags = fcntl(fd, F_GETFL);
fcntl(fd, F_SETFL, flags | O_NONBLOCK); /* 非阻塞方式*/
fcntl(fd, F_SETFL, flags & ~O_NONBLOCK); /* 阻塞方式*/
驅動程序中,當APP打開某個驅動時,在內核中會有一個struct file 結構體的f _flags對應打開文件時的標記位;可以設置f _flasgs 的O_NONBLOCK 位,表示非阻塞;也可以清除這個位表示阻塞。
/* 實現對應的open/read/write等函數,填入file_operations結構體*/
static ssize_t gpio_key_drv_read (struct file *file, char __user *buf, size_t size, loff_t *offset)
{//printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);int err;int key;if(is_key_buf_empty() && (file->f_flags & O_NONBLOCK))return -EAGAIN;else{wait_event_interruptible(gpio_key_wait, !is_key_buf_empty());key = get_key();err = copy_to_user(buf, &key, 4);return 4;}
}
1-5 定時器
(1) 內核函數
所謂定時器,就是鬧鐘,時間到后你就要做某些事。有2個要素:時間、做事;換成程序員的話就是:超時時間、函數。
內核源碼:include\linux\timer.h
1、設置定時器,主要是初始化timer_list結構體,設置其中的函數、參數。
setup_timer(timer, fn, data);
2、向內核添加定時器。 timer–>expires 表示超時時間。
當超時時間到達,內核就會調用這個函數:timer->function(timer -->data) 。
void add_timer(struct timer_list *timer)
3、修改定時器的超時時間
int mod_timer(struct timer_list *timer, unsigned long expires):
4、刪除定時器
int del_timer(struct timer_list *timer)
(2) 定時器時間單位
這表示內核每秒中會發生100次系統滴答中斷 (tick),這是Linux系統的心跳。每發生一次tick中斷,全局變量jiffies累加1。即:每個滴答是10ms。
CONFIG_HZ=100
按鍵觸發中斷,進入中斷處理函數,若不斷發生機械振動,會不斷進入中斷處理函數更新定時器超時時間,時間到后進入定時器處理函數,打印GPIO端口信息
probe函數設置定時器
static int gpio_key_probe(struct platform_device *pdev)
{/* 設置定時器*/setup_timer(&gpio_keys_100ask[i].key_timer, key_timer_expire, &gpio_keys_100ask[i]);/*設置超時時間*/gpio_keys_100ask[i].key_timer.expires = ~0;add_timer(&gpio_keys_100ask[i].key_timer);
}
中斷處理函數修改定時器超時時間
static irqreturn_t gpio_key_isr(int irq, void *dev_id)
{struct gpio_key *gpio_key = dev_id;printk("gpio_key_isr %d irq happened\n", gpio_key->gpio);mod_timer(&gpio_key->key_timer, jiffies + HZ/50); //20ms HZ = 1sreturn IRQ_HANDLED;
}
定時器處理函數中打印GPIO信息和喚醒線程
struct timer_list key_timer;
/*定時器處理函數*/
static void key_timer_expire(unsigned long data)
{/*data ==> gpio*/struct gpio_key *gpio_key = data;int val;int key;val = gpiod_get_value(gpio_key->gpiod);printk("key_timer_expire %d %d\n", gpio_key->gpio, val);key = (gpio_key->gpio << 8) | val;put_key(key);wake_up_interruptible(&gpio_key_wait); /*喚醒線程*/kill_fasync(&button_fasync, SIGIO, POLL_IN); /*發信號*/}
1-6 中斷下半部 tasklet
在上半部處理緊急的事情時,在處理過程中,中斷是被禁止的;在下半部處理耗時的事情時,在處理過程中,中斷是使能的。
內核源碼:include\linux\interrupt.h
struct tasklet_struct
{struct tasklet_struct *next;unsigned long state;atomic_t count;void (*func)(unsigned long);unsigned long data;
};
state用于表示 tasklet 的狀態,一共有2位。
bit0 表示 TASKLET_STATE_SCHED
等于1時,表示已經執行了 tasklet_schedule把tasklet放入隊列;
bit1 表示 TASKLET_STATE_RUN
等于1時,表示正在運行 tasklet 中的func函數;函數執行完后內核會把該位清0。