05網(wǎng)答案深圳關(guān)鍵詞優(yōu)化平臺
嵌入式Linux應用開發(fā)-基礎(chǔ)知識-第十九章驅(qū)動程序基石④
- 第十九章 驅(qū)動程序基石④
- 19.7 工作隊列
- 19.7.1 內(nèi)核函數(shù)
- 19.7.1.1 定義 work
- 19.7.1.2 使用 work:schedule_work
- 19.7.1.3 其他函數(shù)
- 19.7.2 編程、上機
- 19.7.3 內(nèi)部機制
- 19.7.3.1 Linux 2.x的工作隊列創(chuàng)建過程
- 19.7.3.2
- 19.8 中斷的線程化處理
- 19.8.1 內(nèi)核機制
- 19.8.1.1 調(diào)用 request_threaded_irq后內(nèi)核的數(shù)據(jù)結(jié)構(gòu)
- 19.8.1.2
- 19.8.1.3 中斷的執(zhí)行過程
- 19.8.2 編程、上機
第十九章 驅(qū)動程序基石④
19.7 工作隊列
使用 GIT命令載后,本節(jié)源碼位于這個目錄下:
01_all_series_quickstart\
05_嵌入式 Linux驅(qū)動開發(fā)基礎(chǔ)知識\source\
06_gpio_irq\ 09_read_key_irq_poll_fasync_block_timer_tasklet_workqueue
前面講的定時器、下半部 tasklet,它們都是在中斷上下文中執(zhí)行,它們無法休眠。當要處理更復雜的事情時,往往更耗時。這些更耗時的工作放在定時器或是下半部中,會使得系統(tǒng)很卡;并且循環(huán)等待某件事情完成也太浪費 CPU資源了。
如果使用線程來處理這些耗時的工作,那就可以解決系統(tǒng)卡頓的問題:因為線程可以休眠。
在內(nèi)核中,我們并不需要自己去創(chuàng)建線程,可以使用“工作隊列”(workqueue)。內(nèi)核初始化工作隊列是,就為它創(chuàng)建了內(nèi)核線程。以后我們要使用“工作隊列”,只需要把“工作”放入“工作隊列中”,對應的內(nèi)核線程就會取出“工作”,執(zhí)行里面的函數(shù)。
在 2.xx的內(nèi)核中,工作隊列的內(nèi)部機制比較簡單;在現(xiàn)在 4.x的內(nèi)核中,工作隊列的內(nèi)部機制做得復雜無比,但是用法是一樣的。
工作隊列的應用場合:要做的事情比較耗時,甚至可能需要休眠,那么可以使用工作隊列。
缺點:多個工作(函數(shù))是在某個內(nèi)核線程中依序執(zhí)行的,前面函數(shù)執(zhí)行很慢,就會影響到后面的函數(shù)。 在多 CPU的系統(tǒng)下,一個工作隊列可以有多個內(nèi)核線程,可以在一定程度上緩解這個問題。
我們先使用看看怎么使用工作隊列。
19.7.1 內(nèi)核函數(shù)
內(nèi)核線程、工作隊列(workqueue)都由內(nèi)核創(chuàng)建了,我們只是使用。使用的核心是一個 work_struct結(jié)構(gòu)體,定義如下:
使用工作隊列時,步驟如下:
① 構(gòu)造一個 work_struct結(jié)構(gòu)體,里面有函數(shù);
② 把這個 work_struct結(jié)構(gòu)體放入工作隊列,內(nèi)核線程就會運行 work中的函數(shù)。
19.7.1.1 定義 work
參考內(nèi)核頭文件:include\linux\workqueue.h
#define DECLARE_WORK(n, f) \ struct work_struct n = __WORK_INITIALIZER(n, f)
#define DECLARE_DELAYED_WORK(n, f) \ struct delayed_work n = __DELAYED_WORK_INITIALIZER(n, f, 0)
第 1個宏是用來定義一個 work_struct結(jié)構(gòu)體,要指定它的函數(shù)。
第 2個宏用來定義一個 delayed_work結(jié)構(gòu)體,也要指定它的函數(shù)。所以“delayed”,意思就是說要讓它運行時,可以指定:某段時間之后你再執(zhí)行。
如果要在代碼中初始化 work_struct結(jié)構(gòu)體,可以使用下面的宏:
#define INIT_WORK(_work, _func)
19.7.1.2 使用 work:schedule_work
調(diào)用 schedule_work時,就會把 work_struct結(jié)構(gòu)體放入隊列中,并喚醒對應的內(nèi)核線程。內(nèi)核線程就會從隊列里把 work_struct結(jié)構(gòu)體取出來,執(zhí)行里面的函數(shù)。
19.7.1.3 其他函數(shù)
19.7.2 編程、上機
19.7.3 內(nèi)部機制
初學者知道 work_struct中的函數(shù)是運行于內(nèi)核線程的上下文,這就足夠了。
在 2.xx版本的 Linux內(nèi)核中,創(chuàng)建 workqueue時就會同時創(chuàng)建內(nèi)核線程;
在 4.xx版本的 Linux內(nèi)核中,內(nèi)核線程和 workqueue是分開創(chuàng)建的,比較復雜。
19.7.3.1 Linux 2.x的工作隊列創(chuàng)建過程
代碼在 kernel\workqueue.c中:
init_workqueues
keventd_wq = create_workqueue("events"); __create_workqueue((name), 0, 0) for_each_possible_cpu(cpu) { err = create_workqueue_thread(cwq, cpu); p = kthread_create(worker_thread, cwq, fmt, wq->name, cpu);
對于每一個 CPU,都創(chuàng)建一個名為“events/X”的內(nèi)核線程,X從 0開始。
在創(chuàng)建 workqueue的同時創(chuàng)建內(nèi)核線程。
19.7.3.2
Linux 4.x的工作隊列創(chuàng)建過程
Linux4.x中,內(nèi)核線程和工作隊列是分開創(chuàng)建的。
先創(chuàng)建內(nèi)核線程,代碼在 kernel\workqueue.c中: init_workqueues
/* initialize CPU pools */
for_each_possible_cpu(cpu) { for_each_cpu_worker_pool(pool, cpu) { /* 對每一個 CPU都創(chuàng)建 2個 worker_pool結(jié)構(gòu)體,它是含有 ID的 */ /* 一個 worker_pool對應普通優(yōu)先級的 work,第 2個對應高優(yōu)先級的 work */ }
/* create the initial worker */
for_each_online_cpu(cpu) { for_each_cpu_worker_pool(pool, cpu) { /* 對每一個 CPU的每一個 worker_pool,創(chuàng)建一個 worker */
/* 每一個 worker對應一個內(nèi)核線程 */ BUG_ON(!create_worker(pool)); }
}
create_worker函數(shù)代碼如下:
創(chuàng)建好內(nèi)核線程后,再創(chuàng)建 workqueue,代碼在 kernel\workqueue.c中:
init_workqueues
system_wq = alloc_workqueue("events", 0, 0); __alloc_workqueue_key wq = kzalloc(sizeof(*wq) + tbl_size, GFP_KERNEL); // 分配 workqueue_struct alloc_and_link_pwqs(wq) // 跟 worker_poll建立聯(lián)系
一開始時,每一個 worker_poll下只有一個線程,但是系統(tǒng)會根據(jù)任務繁重程度動態(tài)創(chuàng)建、銷毀內(nèi)核線程。所以你可以在 work中打印線程 ID,發(fā)現(xiàn)它可能是變化的。
19.8 中斷的線程化處理
使用 GIT命令載后,本節(jié)源碼位于這個目錄下:
01_all_series_quickstart\
05_嵌入式 Linux驅(qū)動開發(fā)基礎(chǔ)知識\source\
06_gpio_irq\ 10_read_key_irq_poll_fasync_block_timer_tasklet_workqueue_threadedirq
請先回顧《18.2.7 新技術(shù):threaded irq》。
復雜、耗時的事情,盡量使用內(nèi)核線程來處理。上節(jié)視頻介紹的工作隊列用起來挺簡單,但是它有一個缺點:工作隊列中有多個 work,前一個 work沒處理完會影響后面的 work。解決方法有很多種,比如干脆自己創(chuàng)建一個內(nèi)核線程,不跟別的 work湊在一塊了。在 Linux系統(tǒng)中,對于存儲設(shè)備比如 SD/TF卡,它的驅(qū)動程序就是這樣做的,它有自己的內(nèi)核線程。
對于中斷處理,還有另一種方法:threaded irq,線程化的中斷處理。中斷的處理仍然可以認為分為上半部、下半部。上半部用來處理緊急的事情,下半部用一個內(nèi)核線程來處理,這個內(nèi)核線程專用于這個中斷。 內(nèi)核提供了這個函數(shù):
你可以只提供 thread_fn,系統(tǒng)會為這個函數(shù)創(chuàng)建一個內(nèi)核線程。發(fā)生中斷時,系統(tǒng)會立刻調(diào)用 handler函數(shù),然后喚醒某個內(nèi)核線程,內(nèi)核線程再來執(zhí)行 thread_fn函數(shù)。
19.8.1 內(nèi)核機制
19.8.1.1 調(diào)用 request_threaded_irq后內(nèi)核的數(shù)據(jù)結(jié)構(gòu)
19.8.1.2
request_threaded_irq
request_threaded_irq函數(shù),肯定會創(chuàng)建一個內(nèi)核線程。
源碼在內(nèi)核文件 kernel\irq\manage.c中,
int request_threaded_irq(unsigned int irq, irq_handler_t handler, irq_handler_t thread_fn, unsigned long irqflags, const char *devname, void *dev_id)
{ // 分配、設(shè)置一個 irqaction結(jié)構(gòu)體 action = kzalloc(sizeof(struct irqaction), GFP_KERNEL); if (!action) return -ENOMEM;
action->handler = handler;
action->thread_fn = thread_fn; action->flags = irqflags;
action->name = devname;
action->dev_id = dev_id; retval = __setup_irq(irq, desc, action); // 進一步處理 } __setup_irq函數(shù)代碼如下(只摘取重要部分):
if (new->thread_fn && !nested) { ret = setup_irq_thread(new, irq, false);
setup_irq_thread函數(shù)代碼如下(只摘取重要部分):
if (!secondary) { t = kthread_create(irq_thread, new, "irq/%d-%s", irq, new->name);
} else { t = kthread_create(irq_thread, new, "irq/%d-s-%s", irq, new->name); param.sched_priority -= 1;
}
new->thread = t;
19.8.1.3 中斷的執(zhí)行過程
對于 GPIO中斷,我使用 QEMU的調(diào)試功能找出了所涉及的函數(shù)調(diào)用,其他板子可能稍有不同。 調(diào)用關(guān)系如下,反過來看:
Breakpoint 1, gpio_keys_gpio_isr (irq=200, dev_id=0x863e6930) at drivers/input/keyboard/gpio_keys.c:393
393 {
(gdb) bt
#0 gpio_keys_gpio_isr (irq=200, dev_id=0x863e6930) at drivers/input/keyboard/gpio_keys.c:393 #1 0x80270528 in __handle_irq_event_percpu (desc=0x8616e300, flags=0x86517edc) at kernel/irq/handle.c:145
#2 0x802705cc in handle_irq_event_percpu (desc=0x8616e300) at kernel/irq/handle.c:185
#3 0x80270640 in handle_irq_event (desc=0x8616e300) at kernel/irq/handle.c:202
#4 0x802738e8 in handle_level_irq (desc=0x8616e300) at kernel/irq/chip.c:518
#5 0x8026f7f8 in generic_handle_irq_desc (desc=<optimized out>) at ./include/linux/irqdesc.h:150
#6 generic_handle_irq (irq=<optimized out>) at kernel/irq/irqdesc.c:590
#7 0x805005e0 in mxc_gpio_irq_handler (port=0xc8, irq_stat=2252237104) at drivers/gpio/gpio-mxc.c:274
#8 0x805006fc in mx3_gpio_irq_handler (desc=<optimized out>) at drivers/gpio/gpio-mxc.c:291 #9 0x8026f7f8 in generic_handle_irq_desc (desc=<optimized out>) at ./include/linux/irqdesc.h:150
#10 generic_handle_irq (irq=<optimized out>) at kernel/irq/irqdesc.c:590
#11 0x8026fd0c in __handle_domain_irq (domain=0x86006000, hwirq=32, lookup=true, regs=0x86517fb0) at kernel/irq/irqdesc.c:627
#12 0x80201484 in handle_domain_irq (regs=<optimized out>, hwirq=<optimized out>, domain=<optimized out>) at ./include/linux/irqdesc.h:168
#13 gic_handle_irq (regs=0xc8) at drivers/irqchip/irq-gic.c:364
#14 0x8020b704 in __irq_usr () at arch/arm/kernel/entry-armv.S:464
我們只需要分析__handle_irq_event_percpu函數(shù),它在 kernel\irq\handle.c中:
線程的處
理函數(shù)為 irq_thread,代碼在 kernel\irq\handle.c中:
19.8.2 編程、上機
調(diào)用request_threaded_irq函數(shù)注冊中斷,調(diào)用free_irq卸載中斷。
從前面可知,我們可以提供上半部函數(shù),也可以不提供:
① 如果不提供
內(nèi)核會提供默認的上半部處理函數(shù):irq_default_primary_handler,它是直接返回 IRQ_WAKE_THREAD。 ② 如果提供的話
返回值必須是:IRQ_WAKE_THREAD。
在 thread_fn中,如果中斷被正確處理了,應該返回 IRQ_HANDLED。