網(wǎng)站用戶體驗(yàn)優(yōu)化方案低價(jià)刷贊網(wǎng)站推廣
本文代碼在哪個(gè)基礎(chǔ)上修改而成?
本文是在博文 https://blog.csdn.net/wenhao_ir/article/details/145228617 的代碼基礎(chǔ)上修改而成。
關(guān)于工作隊(duì)列(Workqueue)的概念
工作隊(duì)列(Workqueue)可以用于實(shí)現(xiàn)Linux的中斷下半部的,之前在博文 https://blog.csdn.net/wenhao_ir/article/details/145309140 中已經(jīng)介紹過(guò)中斷上半部和中斷下半部的概念。
它和軟中斷(SoftIRQ)、任務(wù)隊(duì)列(Tasklet)相比,最大的不同是它是可以進(jìn)入阻塞或休眠狀態(tài),它允許調(diào)用會(huì)導(dǎo)致阻塞或休眠的函數(shù),比如msleep
、mutex_lock
、schedule
等函數(shù)。
當(dāng)然,在三者中,工作隊(duì)列(Workqueue)的優(yōu)先級(jí)相對(duì)來(lái)說(shuō)是最低的。
本文利用工作隊(duì)列(Workqueue)實(shí)現(xiàn)中斷下半部的思路是:在硬中斷中將任務(wù)加入到處理工作隊(duì)列(Workqueue)的內(nèi)核線程中,然后由這個(gè)內(nèi)核線程去調(diào)度這個(gè)任務(wù)的執(zhí)行。
完整源代碼
驅(qū)動(dòng)程序gpio_key_drv.c
中的代碼
#include <linux/module.h>#include <linux/fs.h>
#include <linux/errno.h>
#include <linux/miscdevice.h>
#include <linux/kernel.h>
#include <linux/major.h>
#include <linux/mutex.h>
#include <linux/proc_fs.h>
#include <linux/seq_file.h>
#include <linux/stat.h>
#include <linux/init.h>
#include <linux/device.h>
#include <linux/tty.h>
#include <linux/kmod.h>
#include <linux/gfp.h>
#include <linux/gpio/consumer.h>
#include <linux/platform_device.h>
#include <linux/of_gpio.h>
#include <linux/of_irq.h>
#include <linux/interrupt.h>
#include <linux/irq.h>
#include <linux/slab.h>
#include <linux/workqueue.h>
#include <asm/current.h>
#include <linux/delay.h>struct gpio_key{int gpio;struct gpio_desc *gpiod;int flag;int irq;struct work_struct work;
} ;static struct gpio_key *gpio_keys_100ask;/* 主設(shè)備號(hào) */
static int major = 0;
static struct class *gpio_key_class;static int g_key = 0;static DECLARE_WAIT_QUEUE_HEAD(gpio_key_wait);/* 環(huán)形緩沖區(qū) */
#define BUF_LEN 128
static int g_keys[BUF_LEN];
static int r, w;#define NEXT_POS(x) ((x+1) % BUF_LEN)static int is_key_buf_empty(void)
{return (r == w);
}static int is_key_buf_full(void)
{return (r == NEXT_POS(w));
}static void put_key(int key_value)
{if (!is_key_buf_full()){g_keys[w] = key_value;w = NEXT_POS(w);}
}static int get_key(void)
{int key_value = 0;if (!is_key_buf_empty()){key_value = g_keys[r];r = NEXT_POS(r);}return key_value;
}/* 實(shí)現(xiàn)文件操作結(jié)構(gòu)體中的read函數(shù) */
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_value;wait_event_interruptible(gpio_key_wait, !is_key_buf_empty());//從緩形緩沖區(qū)中取出數(shù)據(jù)key_value = get_key();err = copy_to_user(buf, &key_value, 4);// 返回值為4表明讀到了4字節(jié)的數(shù)據(jù)return 4;
}/* 定義自己的file_operations結(jié)構(gòu)體 */
static struct file_operations gpio_key_drv = {.owner = THIS_MODULE,.read = gpio_key_drv_read,
};static void key_work_func(struct work_struct *work)
{struct gpio_key *gpio_key = container_of(work, struct gpio_key, work);int val;val = gpiod_get_value(gpio_key->gpiod);printk("The function keyw_ork_func is sleeping for 1000 milliseconds...\n");// 內(nèi)核空間函數(shù)msleep可使線程休眠一段時(shí)間,單位為毫秒// 需要包含頭文件 #include <linux/delay.h>// 在中斷下半部中,只有工作隊(duì)列(Workqueue)才能進(jìn)行休眠操作msleep(1000);// g_key的高8位中存儲(chǔ)的是GPIO口的編號(hào),低8位中存儲(chǔ)的是按鍵按下時(shí)的邏輯值g_key = (gpio_key->gpio << 8) | val;//裝按鍵值放入環(huán)形緩沖區(qū)put_key(g_key);wake_up_interruptible(&gpio_key_wait);printk("key_work_func: the process is %s pid %d\n",current->comm, current->pid); // current->comm代表當(dāng)前進(jìn)程(線程)的名字 printk("key_work_func key %d %d\n", gpio_key->gpio, val);
}static irqreturn_t gpio_key_isr(int irq, void *dev_id)
{struct gpio_key *gpio_key = dev_id;// 任務(wù)加入到內(nèi)核kworker線程的工作隊(duì)列上schedule_work(&gpio_key->work);return IRQ_HANDLED; // 表示中斷已處理
}/* 1. 從platform_device獲得GPIO* 2. gpio=>irq* 3. request_irq*/
static int gpio_key_probe(struct platform_device *pdev)
{int err;// 獲取設(shè)備樹(shù)節(jié)點(diǎn)指針struct device_node *node = pdev->dev.of_node;// count用于存儲(chǔ)設(shè)備樹(shù)中描述的GPIO口的數(shù)量int count;int i;enum of_gpio_flags flag;unsigned flags = GPIOF_IN;printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);count = of_gpio_count(node);if (!count){printk("%s %s line %d, there isn't any gpio available\n", __FILE__, __FUNCTION__, __LINE__);return -1;}gpio_keys_100ask = kzalloc(sizeof(struct gpio_key) * count, GFP_KERNEL);if (!gpio_keys_100ask) {printk("Memory allocation failed for gpio_keys_100ask\n");return -ENOMEM;}for (i = 0; i < count; i++){// 獲取GIPO的全局編號(hào)及其標(biāo)志位信息的代碼gpio_keys_100ask[i].gpio = of_get_gpio_flags(node, i, &flag);if (gpio_keys_100ask[i].gpio < 0){printk("%s %s line %d, of_get_gpio_flags fail\n", __FILE__, __FUNCTION__, __LINE__);return -1;}// 獲取GPIO口的GPIO描述符的代碼gpio_keys_100ask[i].gpiod = gpio_to_desc(gpio_keys_100ask[i].gpio);if (!gpio_keys_100ask[i].gpiod) {printk("Failed to get GPIO descriptor for GPIO %d\n", gpio_keys_100ask[i].gpio);return -EINVAL;}// 結(jié)構(gòu)體gpio_key的成員flag用于存儲(chǔ)對(duì)應(yīng)的GPIO口是否是低電平有效,假如是低電平有效,成員flag的值為1,假如不是低電平有效,成員flag的值為0。// 后續(xù)代碼實(shí)際上并沒(méi)有用到成員flag,這里出現(xiàn)這句代碼只是考慮到代碼的可擴(kuò)展性,所以在這里是可以刪除的。gpio_keys_100ask[i].flag = flag & OF_GPIO_ACTIVE_LOW;// 每次循環(huán)都重新初始化flagsflags = GPIOF_IN;// 假如GPIO口是低電平有效,則把flags添加上低電平有效的信息if (flag & OF_GPIO_ACTIVE_LOW)flags |= GPIOF_ACTIVE_LOW;// 請(qǐng)求一個(gè)GPIO硬件資源與設(shè)備結(jié)構(gòu)體`pdev->dev`進(jìn)行綁定// 注意,這個(gè)綁定操作會(huì)在調(diào)用函數(shù)platform_driver_unregister()注銷(xiāo)platform_driver時(shí)自動(dòng)由內(nèi)核解除綁定操作,所以gpio_key_remove函數(shù)中不需要顯示去解除綁定// 由`devm`開(kāi)頭的函數(shù)通常都會(huì)內(nèi)核自動(dòng)管理資源,咱們?cè)谕顺龊瘮?shù)中不用人為的去釋放資源或解除綁定。err = devm_gpio_request_one(&pdev->dev, gpio_keys_100ask[i].gpio, flags, NULL);// 獲取GPIO口的中斷請(qǐng)求號(hào)gpio_keys_100ask[i].irq = gpio_to_irq(gpio_keys_100ask[i].gpio);// 初始化工作隊(duì)列(Workqueue)INIT_WORK(&gpio_keys_100ask[i].work, key_work_func);}for (i = 0; i < count; i++){char irq_name[32]; // 用于存儲(chǔ)動(dòng)態(tài)生成的中斷名稱(chēng)//使用snprintf()函數(shù)將動(dòng)態(tài)生成的中斷名稱(chēng)寫(xiě)入irq_name數(shù)組snprintf(irq_name, sizeof(irq_name), "swh_gpio_irq_%d", i); // 根據(jù)i生成名稱(chēng)//調(diào)用函數(shù)request_irq()來(lái)請(qǐng)求并設(shè)置一個(gè)中斷err = request_irq(gpio_keys_100ask[i].irq, gpio_key_isr, IRQF_TRIGGER_FALLING, irq_name, &gpio_keys_100ask[i]);}/* 注冊(cè)file_operations */major = register_chrdev(0, "swh_read_keys_major", &gpio_key_drv); gpio_key_class = class_create(THIS_MODULE, "swh_read_keys_class");if (IS_ERR(gpio_key_class)) {printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);unregister_chrdev(major, "swh_read_keys_major");return PTR_ERR(gpio_key_class);}// 由于這里是把多個(gè)按鍵看成是一個(gè)設(shè)備,你可以想像一個(gè)鍵盤(pán)上對(duì)應(yīng)多個(gè)按鍵,但鍵盤(pán)本身是一個(gè)設(shè)備,所以只有一個(gè)設(shè)備文件device_create(gpio_key_class, NULL, MKDEV(major, 0), NULL, "read_keys0"); /* /dev/read_keys0 */return 0;}static int gpio_key_remove(struct platform_device *pdev)
{struct device_node *node = pdev->dev.of_node;int count;int i;device_destroy(gpio_key_class, MKDEV(major, 0));class_destroy(gpio_key_class);unregister_chrdev(major, "swh_read_keys_major");count = of_gpio_count(node);for (i = 0; i < count; i++) {// 只有在irq有效時(shí)才釋放中斷資源if (gpio_keys_100ask[i].irq >= 0) {// 釋放GPIO中斷資源,下面這句代碼做了下面兩件事:// 1、解除 `gpio_keys_100ask[i].irq` 中斷號(hào)和 `gpio_key_isr` 中斷處理函數(shù)的綁定。// 2、解除 `gpio_keys_100ask[i].irq` 中斷號(hào)和中斷處理函數(shù)與 `gpio_keys_100ask[i]` 數(shù)據(jù)結(jié)構(gòu)的綁定。free_irq(gpio_keys_100ask[i].irq, &gpio_keys_100ask[i]);}// 釋放GPIO描述符if (gpio_keys_100ask[i].gpiod) {gpiod_put(gpio_keys_100ask[i].gpiod);}}// 釋放內(nèi)存kfree(gpio_keys_100ask);return 0;
}static const struct of_device_id irq_matach_table[] = {{ .compatible = "swh-gpio_irq_key" },{ },
};/* 1. 定義platform_driver */
static struct platform_driver gpio_keys_driver = {.probe = gpio_key_probe,.remove = gpio_key_remove,.driver = {.name = "swh_irq_platform_dirver",.of_match_table = irq_matach_table,},
};/* 2. 在入口函數(shù)注冊(cè)platform_driver */
static int __init gpio_key_init(void)
{int err;printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);err = platform_driver_register(&gpio_keys_driver); return err;
}/* 3. 有入口函數(shù)就應(yīng)該有出口函數(shù):卸載驅(qū)動(dòng)程序時(shí),就會(huì)去調(diào)用這個(gè)出口函數(shù)* 卸載platform_driver*/
static void __exit gpio_key_exit(void)
{printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);platform_driver_unregister(&gpio_keys_driver);
}/* 7. 其他完善:提供設(shè)備信息,自動(dòng)創(chuàng)建設(shè)備節(jié)點(diǎn) */module_init(gpio_key_init);
module_exit(gpio_key_exit);MODULE_LICENSE("GPL");
測(cè)試程序button_test.c
中的代碼
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <string.h>
#include <pthread.h>
#include <stdlib.h>
#include <time.h>/** ./button_test /dev/100ask_button0**/// 打印線程的執(zhí)行函數(shù)
void* print_while_waiting(void* arg)
{while (1){printf("I am another thread, and while the main thread is waiting for a button to be pressed, I can still run normally.\n");sleep(10); // 每隔10秒打印一次}return NULL;
}int main(int argc, char **argv)
{int fd;int val;pthread_t print_thread;int keystroke = 0; //記錄按鍵次數(shù)/* 1. 判斷參數(shù) */if (argc != 2) {printf("Usage: %s <dev>\n", argv[0]);return -1;}/* 2. 打開(kāi)文件 */fd = open(argv[1], O_RDWR);if (fd == -1){printf("Can not open file %s\n", argv[1]);return -1;}// 創(chuàng)建一個(gè)線程,每隔一段時(shí)間打印輸出一條信息表示在等待按鍵期間,另外的線程在繼續(xù)正常執(zhí)行。if (pthread_create(&print_thread, NULL, print_while_waiting, NULL) != 0){printf("Failed to create print thread\n");close(fd);return -1;}while (1){/* 3. 讀文件 */read(fd, &val, 4);/* 提取 GPIO 編號(hào)和邏輯值 */int gpio_number = (val >> 8) & 0xFF; // 高8位為 GPIO 編號(hào)int gpio_value = val & 0xFF; // 低8位為邏輯值keystroke++;/* 打印讀到的信息 */printf("GPIO Number: %d, Logical Value: %d\n", gpio_number, gpio_value);printf("keystrokes is %d\n", keystroke);}//pthread_join的作用是使主線程等待線程print_threa結(jié)束后再繼續(xù)執(zhí)行剩下的代碼。//如果主線程在結(jié)束時(shí)未等待子線程完成,可能會(huì)導(dǎo)致未完成的資源清理或意外的程序終止。//這里由于主線程中有個(gè)條件永遠(yuǎn)為真的while循環(huán),實(shí)際上這句代碼沒(méi)有實(shí)際作用。pthread_join(print_thread, NULL);close(fd);return 0;
}
與工作隊(duì)列(Workqueue)相關(guān)的代碼解讀
由于工作隊(duì)列(Workqueue)還是屬于中斷下半部一種,所以和前面的內(nèi)核定時(shí)器(https://blog.csdn.net/wenhao_ir/article/details/145281064) 和 任務(wù)隊(duì)列(Tasklet)(https://blog.csdn.net/wenhao_ir/article/details/145309140) 的使用基本相同。
首先還是為按鍵結(jié)構(gòu)體 struct gpio_key
添加一個(gè)類(lèi)型為work_struct
的成員。
為什么呢?因?yàn)槊恳粋€(gè)GPIO口我們都要為其分配一個(gè)work_struct
結(jié)構(gòu)體。
然后在platform中的probe操作函數(shù)gpio_key_probe
對(duì)每個(gè)GPIO口初始化時(shí),為每個(gè)GPIO口初始化一個(gè)work
,代碼如下:
INIT_WORK(&gpio_keys_100ask[i].work, key_work_func);
第1個(gè)參數(shù)就是每個(gè)按鍵對(duì)應(yīng)的work_struct
結(jié)構(gòu)體的實(shí)例,第2個(gè)參數(shù)是任務(wù)的回調(diào)函數(shù)。
肯定要問(wèn):怎么不為回調(diào)函數(shù)傳入數(shù)據(jù)?答:因?yàn)閿?shù)據(jù)存儲(chǔ)于gpio_keys_100ask[i]
中,我們可以通過(guò)&gpio_keys_100ask[i].work
反推出gpio_keys_100ask[i]
的指針位置,所以其實(shí)數(shù)據(jù)是已經(jīng)傳入了。這一點(diǎn)和新版的內(nèi)核定時(shí)器是一樣的,詳情見(jiàn) https://blog.csdn.net/wenhao_ir/article/details/145281064 【搜索“注意-Linux_5.x以上對(duì)內(nèi)核定時(shí)器進(jìn)行了修改”】
然后我們?cè)谟仓袛嗵幚砗瘮?shù)中把任務(wù)加入到系統(tǒng)內(nèi)核的工作隊(duì)列(Workqueue)線程中:
static irqreturn_t gpio_key_isr(int irq, void *dev_id)
{struct gpio_key *gpio_key = dev_id;// 任務(wù)加入到內(nèi)核kworker線程的工作隊(duì)列上schedule_work(&gpio_key->work);return IRQ_HANDLED; // 表示中斷已處理
}
代碼很簡(jiǎn)單,詳細(xì)的描述略。
注意: schedule_work
函數(shù)并不一定要運(yùn)行于硬中斷的處理函數(shù)中,具體的情況本文后面有說(shuō)明。
接下來(lái)就去看任務(wù)的回調(diào)函數(shù)key_work_func
了,代碼如下:
static void key_work_func(struct work_struct *work)
{struct gpio_key *gpio_key = container_of(work, struct gpio_key, work);int val;val = gpiod_get_value(gpio_key->gpiod);printk("The function keyw_ork_func is sleeping for 1000 milliseconds...\n");// 內(nèi)核空間函數(shù)msleep可使線程休眠一段時(shí)間,單位為毫秒// 需要包含頭文件 #include <linux/delay.h>// 在中斷下半部中,只有工作隊(duì)列(Workqueue)才能進(jìn)行休眠操作msleep(1000);// g_key的高8位中存儲(chǔ)的是GPIO口的編號(hào),低8位中存儲(chǔ)的是按鍵按下時(shí)的邏輯值g_key = (gpio_key->gpio << 8) | val;//裝按鍵值放入環(huán)形緩沖區(qū)put_key(g_key);wake_up_interruptible(&gpio_key_wait);printk("key_work_func: the process is %s pid %d\n",current->comm, current->pid); // current->comm代表當(dāng)前進(jìn)程(線程)的名字 printk("key_work_func key %d %d\n", gpio_key->gpio, val);
}
同樣屬于中斷下半部,工作隊(duì)列(Workqueue)和軟中斷(SoftIRQ)、任務(wù)隊(duì)列(Tasklet)相比,最大的不同是它是可以進(jìn)入阻塞或休眠狀態(tài),它允許調(diào)用會(huì)導(dǎo)致阻塞或休眠的函數(shù),比如msleep
、mutex_lock
、schedule
等函數(shù),所以我們這里就利用內(nèi)核心空間函數(shù)msleep
使其休眠1000毫秒再運(yùn)行,從而看是不是真的可以進(jìn)行休眠狀態(tài)。
代碼很簡(jiǎn)單,沒(méi)啥好說(shuō)的,只是代碼末尾處還利用printk
打印出了內(nèi)核處理工作隊(duì)列的線程的名字和進(jìn)程號(hào),具體關(guān)于名字的分析在測(cè)試程序之后我有分析。
在本代碼中的工作隊(duì)列(Workqueue)不需要釋放什么資源,因?yàn)槠湔加玫馁Y源由內(nèi)核處理工作隊(duì)列(Workqueue)的線程和相關(guān)機(jī)制自動(dòng)管理, 所以不需要釋放什么資源,所以函數(shù)gpio_key_remove
中不需要增添對(duì)工作隊(duì)列(Workqueue)相關(guān)資源的釋放。
至此,與工作隊(duì)列(Workqueue)相關(guān)的代碼分析完畢。
工作隊(duì)列(Workqueue)機(jī)制的缺點(diǎn)及解決方法
工作隊(duì)列(Workqueue)的缺點(diǎn):
前面的隊(duì)列任務(wù)阻塞時(shí)會(huì)影響后面的隊(duì)列任務(wù)的執(zhí)行,因?yàn)樗鼈兿喈?dāng)于是掛在同一個(gè)內(nèi)核心線程上的任務(wù)。
詳細(xì)解釋如下:
schedule_work()
函數(shù)會(huì)將工作項(xiàng)(work_struct
類(lèi)型)添加到內(nèi)核中的一個(gè) 工作隊(duì)列 上,而這個(gè)工作隊(duì)列會(huì)由內(nèi)核管理的一個(gè)專(zhuān)用線程(或多個(gè)線程)來(lái)執(zhí)行。
詳細(xì)原理
-
工作隊(duì)列的核心
- 工作隊(duì)列(Workqueue)是 Linux 內(nèi)核提供的一種機(jī)制,用于在進(jìn)程上下文中延遲執(zhí)行任務(wù)。
schedule_work()
是將工作項(xiàng)加入到system_wq
(內(nèi)核默認(rèn)的全局工作隊(duì)列)中。
-
內(nèi)核線程執(zhí)行
- 內(nèi)核為工作隊(duì)列創(chuàng)建了一個(gè)專(zhuān)用的內(nèi)核線程(
kworker
),通常命名為kworker/<CPU編號(hào)>
。 - 當(dāng)你調(diào)用
schedule_work()
時(shí),內(nèi)核將你的工作項(xiàng)添加到隊(duì)列中,kworker
線程會(huì)取出并執(zhí)行這些工作項(xiàng)。
- 內(nèi)核為工作隊(duì)列創(chuàng)建了一個(gè)專(zhuān)用的內(nèi)核線程(
-
為什么要這樣設(shè)計(jì)?
- 中斷上下文無(wú)法進(jìn)行阻塞或復(fù)雜操作,但很多任務(wù)需要在進(jìn)程上下文中運(yùn)行。
- 工作隊(duì)列為這種需求提供了解決方案:允許開(kāi)發(fā)者在內(nèi)核線程中運(yùn)行需要延遲執(zhí)行的任務(wù),同時(shí)允許這些任務(wù)休眠、阻塞或執(zhí)行耗時(shí)操作。
schedule_work()
的執(zhí)行過(guò)程
-
調(diào)用
schedule_work()
時(shí):- 檢查
work_struct
是否已在隊(duì)列中(防止重復(fù)排隊(duì))。 - 如果沒(méi)有重復(fù)排隊(duì),將它添加到全局工作隊(duì)列(
system_wq
)。
- 檢查
-
內(nèi)核線程(
kworker
)被喚醒:kworker
線程會(huì)檢查它負(fù)責(zé)的隊(duì)列是否有任務(wù)。- 如果有任務(wù),取出并調(diào)用工作項(xiàng)的處理函數(shù)(由開(kāi)發(fā)者定義)。
-
運(yùn)行你的工作項(xiàng):
- 調(diào)用你在
INIT_WORK()
中指定的處理函數(shù)。 - 工作項(xiàng)處理完成后,從隊(duì)列中移除。
- 調(diào)用你在
在隊(duì)列任務(wù)的執(zhí)行中,如果前面的隊(duì)列任務(wù)進(jìn)入了阻塞狀態(tài),就會(huì)影響后續(xù)隊(duì)列任務(wù)的執(zhí)行。這是因?yàn)?工作隊(duì)列 中的任務(wù)是按照 FIFO(先進(jìn)先出)順序依次執(zhí)行的,而一個(gè)任務(wù)阻塞后,kworker
線程會(huì)一直等待任務(wù)完成,無(wú)法繼續(xù)處理后續(xù)任務(wù)。
工作隊(duì)列的運(yùn)行機(jī)制
-
默認(rèn)情況:一個(gè)
kworker
線程:- 系統(tǒng)默認(rèn)的全局工作隊(duì)列(
system_wq
)使用共享的kworker
線程。 kworker
線程是單線程處理的,一次只能運(yùn)行一個(gè)任務(wù)。如果某個(gè)任務(wù)阻塞,后續(xù)任務(wù)必須等待。
- 系統(tǒng)默認(rèn)的全局工作隊(duì)列(
-
順序處理的特點(diǎn):
- 如果工作隊(duì)列中前面的任務(wù) A 阻塞了,
kworker
會(huì)等待任務(wù) A 完成,再去處理任務(wù) B。 - 任務(wù)之間沒(méi)有搶占關(guān)系,因此阻塞會(huì)直接導(dǎo)致后續(xù)任務(wù)延遲執(zhí)行。
- 如果工作隊(duì)列中前面的任務(wù) A 阻塞了,
如何避免阻塞影響其他任務(wù)?
有幾種方法可以解決這個(gè)問(wèn)題:
-
使用線程化的中斷處理
如果是中斷處理中使用工作隊(duì)列(Workqueue)實(shí)現(xiàn)中斷下半部的處理,那么可以在注冊(cè)中斷的同時(shí)為這個(gè)中斷注冊(cè)一個(gè)屬于這個(gè)中斷的線程。詳情見(jiàn) https://blog.csdn.net/wenhao_ir/article/details/145326705 -
創(chuàng)建獨(dú)立的工作隊(duì)列
- 使用
alloc_workqueue()
創(chuàng)建一個(gè)專(zhuān)用的工作隊(duì)列。每個(gè)工作隊(duì)列會(huì)有獨(dú)立的線程,互不干擾。 - 示例代碼:
#include <linux/workqueue.h>static struct workqueue_struct *my_wq; static struct work_struct my_work;static void my_work_handler(struct work_struct *work) {msleep(5000); // 模擬阻塞操作printk(KERN_INFO "Task finished\n"); }static int __init my_init(void) {// 創(chuàng)建獨(dú)立的工作隊(duì)列my_wq = alloc_workqueue("my_workqueue", WQ_UNBOUND, 0);if (!my_wq)return -ENOMEM;// 初始化并提交工作項(xiàng)到自定義工作隊(duì)列INIT_WORK(&my_work, my_work_handler);queue_work(my_wq, &my_work);printk(KERN_INFO "Work queued\n");return 0; }static void __exit my_exit(void) {// 銷(xiāo)毀工作隊(duì)列if (my_wq)destroy_workqueue(my_wq);printk(KERN_INFO "Module exited\n"); }module_init(my_init); module_exit(my_exit); MODULE_LICENSE("GPL");
- 通過(guò)
alloc_workqueue()
創(chuàng)建的工作隊(duì)列有獨(dú)立的內(nèi)核線程,任務(wù)阻塞不會(huì)影響其他隊(duì)列的任務(wù)。
- 使用
- 使用 WQ_UNBOUND 屬性
- 在創(chuàng)建工作隊(duì)列時(shí)使用
WQ_UNBOUND
標(biāo)志:- 允許工作項(xiàng)不綁定到特定的 CPU,可以并發(fā)運(yùn)行多個(gè)任務(wù)。
- 內(nèi)核會(huì)動(dòng)態(tài)分配線程來(lái)處理這些任務(wù),從而減少任務(wù)阻塞的影響。
- 示例代碼:
alloc_workqueue("my_workqueue", WQ_UNBOUND, 0);
- 在創(chuàng)建工作隊(duì)列時(shí)使用
- 避免任務(wù)長(zhǎng)時(shí)間阻塞
- 如果任務(wù)本身需要長(zhǎng)時(shí)間阻塞,可以考慮拆分任務(wù),將長(zhǎng)時(shí)間的阻塞部分移到用戶態(tài)完成,或者異步處理(如通過(guò)線程或其他機(jī)制)。
總結(jié)
- 默認(rèn)行為:全局工作隊(duì)列(
system_wq
)使用共享的kworker
線程,阻塞任務(wù)會(huì)影響后續(xù)任務(wù)的執(zhí)行。 - 解決辦法:
- 創(chuàng)建專(zhuān)用的工作隊(duì)列(獨(dú)立線程)。
- 使用
WQ_UNBOUND
來(lái)增加并發(fā)能力。 - 避免任務(wù)本身長(zhǎng)時(shí)間阻塞。
通過(guò)這些方法,可以有效避免阻塞任務(wù)影響整個(gè)工作隊(duì)列的運(yùn)行效率。
schedule_work
函數(shù)并不一定要運(yùn)行在硬件中斷的處理函數(shù)中
schedule_work
函數(shù)并不一定要運(yùn)行在硬件中斷的處理函數(shù)中。它可以在任何可以運(yùn)行內(nèi)核代碼的上下文中被調(diào)用,具體包括以下場(chǎng)景:
- 硬件中斷處理函數(shù)中調(diào)用
- 硬件中斷的處理函數(shù)通常要求執(zhí)行迅速,因此適合將復(fù)雜或耗時(shí)的任務(wù)推遲到中斷下半部(如工作隊(duì)列)中執(zhí)行。
- 在中斷處理函數(shù)中調(diào)用
schedule_work
,將工作任務(wù)添加到工作隊(duì)列中,由內(nèi)核的工作線程(kworker
)在合適的時(shí)機(jī)處理。
- 內(nèi)核線程或其他上下文中調(diào)用
schedule_work
可以在任何普通的內(nèi)核上下文中調(diào)用,比如:- 從設(shè)備驅(qū)動(dòng)的
probe
或其他文件操作函數(shù)中。 - 在定時(shí)器回調(diào)函數(shù)中。
- 在內(nèi)核模塊的入口初始化函數(shù)(
module_init
)中。
- 從設(shè)備驅(qū)動(dòng)的
- 無(wú)需限制在中斷上下文中使用。
- 用戶態(tài)系統(tǒng)調(diào)用觸發(fā)的內(nèi)核函數(shù)中
- 用戶態(tài)程序觸發(fā)的系統(tǒng)調(diào)用(例如讀寫(xiě)驅(qū)動(dòng)設(shè)備)中,驅(qū)動(dòng)程序可以調(diào)用
schedule_work
將任務(wù)推遲到工作隊(duì)列中執(zhí)行。 - 這可以避免耗時(shí)任務(wù)阻塞用戶態(tài)進(jìn)程。
為什么不一定要在中斷上下文中調(diào)用?
- 設(shè)計(jì)目的:
schedule_work
的作用是將任務(wù)加入到工作隊(duì)列,它本身的調(diào)用非常輕量,可以在任何允許調(diào)用內(nèi)核函數(shù)的上下文中使用。 - 上下文限制:
- 如果是中斷上下文,不能執(zhí)行可能會(huì)阻塞的操作(如
msleep
或mutex_lock
)。通過(guò)schedule_work
,可以將任務(wù)推遲到工作線程(kworker)中運(yùn)行,從而避開(kāi)這些限制。 - 如果是進(jìn)程上下文,則不存在中斷上下文的限制,可以直接使用。
- 如果是中斷上下文,不能執(zhí)行可能會(huì)阻塞的操作(如
注意事項(xiàng)
- 調(diào)用
schedule_work
后,必須確保對(duì)應(yīng)的work_struct
已正確初始化(通過(guò)INIT_WORK
或INIT_DELAYED_WORK
)。 - 如果模塊卸載時(shí)還有未完成的工作隊(duì)列任務(wù),需要確保處理完畢或取消任務(wù),避免資源泄漏或非法訪問(wèn)。
總結(jié)
schedule_work
并不一定需要在硬件中斷處理函數(shù)中調(diào)用。它可以在任何允許執(zhí)行內(nèi)核代碼的上下文中調(diào)用,目的是將任務(wù)加入工作隊(duì)列,交由 kworker
內(nèi)核線程在合適的時(shí)間處理。這樣做可以延遲執(zhí)行復(fù)雜任務(wù),減少上下文的阻塞時(shí)間,提高內(nèi)核代碼的效率和響應(yīng)性。
設(shè)備樹(shù)文件的修改和更新
和下面兩篇博文一樣:
https://blog.csdn.net/wenhao_ir/article/details/145225508
https://blog.csdn.net/wenhao_ir/article/details/145176361
Makfile文件內(nèi)容
# 使用不同的Linux內(nèi)核時(shí), 一定要修改KERN_DIR,KERN_DIR代表已經(jīng)配置、編譯好的Linux源碼的根目錄KERN_DIR = /home/book/100ask_imx6ull-sdk/Linux-4.9.88all:make -C $(KERN_DIR) M=`pwd` modules# 因?yàn)闇y(cè)試程序中有線程的創(chuàng)建,所以下面的語(yǔ)句需要添加 -lpthread 鏈接選項(xiàng)$(CROSS_COMPILE)gcc -o button_test_02 button_test.c -lpthread clean:make -C $(KERN_DIR) M=`pwd` cleanrm -rf modules.orderrm -f button_test_02obj-m += gpio_key_drv.o
交叉編譯出驅(qū)動(dòng)模塊和測(cè)試程序
源碼復(fù)制到Ubuntu中。
make
將交叉編譯出的gpio_key_drv.ko
和button_test_02
復(fù)制到NFS文件目錄中,備用。
加載模塊
打開(kāi)串口終端→打開(kāi)開(kāi)發(fā)板→掛載網(wǎng)絡(luò)文件系統(tǒng)
mount -t nfs -o nolock,vers=3 192.168.5.11:/home/book/nfs_rootfs /mnt
insmod /mnt/workqueue/gpio_key_drv.ko
檢查設(shè)備文件生成沒(méi)有
ls /dev/
有了:
運(yùn)行測(cè)試程序
先把內(nèi)核printk打印的顯示打開(kāi):
echo "7 4 1 7" > /proc/sys/kernel/printk
然后:
cd /mnt/workqueue
./button_test_02 /dev/read_keys0
從運(yùn)行過(guò)程可以感知到,工作隊(duì)列所在的內(nèi)核線程的確是休眠1000毫秒之后再繼續(xù)運(yùn)行的。在延遲1000毫秒之后后面的代碼把按鍵值放入了環(huán)形緩沖區(qū),進(jìn)面用戶空間中的程序可以讀取相應(yīng)的按鍵值。
測(cè)試成功。
關(guān)于處理工作隊(duì)列的內(nèi)核線程的名字的詳細(xì)解釋
我們的程序在測(cè)試中還打印出了處理工作隊(duì)列的內(nèi)核線程的名字和進(jìn)程號(hào),名字為kworker/0:1
,其所在進(jìn)程號(hào)為1720,我們可以用ps命令看一下:
可見(jiàn),內(nèi)核中確實(shí)存在著一個(gè)進(jìn)程號(hào)為1720的內(nèi)核線程在處理中斷下半部的工作隊(duì)列(Workqueue)。
這里我解釋一下名字的kworker/0:1
含義:
進(jìn)程名字 kworker/0:1
是 Linux 內(nèi)核中 kworker(內(nèi)核工作線程) 的標(biāo)準(zhǔn)命名格式。我們來(lái)逐個(gè)解析其含義:
kworker
- 表示這是一個(gè) 內(nèi)核工作線程(Kernel Worker Thread)。
- 這些線程是由內(nèi)核的 Workqueue(工作隊(duì)列) 機(jī)制管理的,用于處理一些延遲執(zhí)行的任務(wù)或繁重的內(nèi)核工作。
0
- 表示 CPU 的編號(hào)。
- 這里的
0
指的是第 0 號(hào) CPU,也就是說(shuō),這個(gè) kworker 線程被綁定或調(diào)度在 CPU 0 上運(yùn)行。
1
- 表示 線程的 ID,也可以理解為線程在某個(gè) CPU 上的序號(hào)。
- 每個(gè) CPU 可能會(huì)有多個(gè) kworker 線程,它們會(huì)被分配一個(gè)唯一的 ID 來(lái)區(qū)分。在本例中,
1
表示第一個(gè)線程。
完整解釋
kworker/0:1
表示:- 這是一個(gè)內(nèi)核工作線程。
- 它被綁定(或主要調(diào)度)在 CPU 0 上運(yùn)行。
- 它是 CPU 0 上的第 1 號(hào) kworker 線程。
補(bǔ)充說(shuō)明
-
多核系統(tǒng)中的 kworker:
- 在多核系統(tǒng)中,每個(gè) CPU 都可能有多個(gè) kworker 線程。例如:
kworker/1:0
:表示 CPU 1 上的第 0 號(hào) kworker 線程。kworker/2:2
:表示 CPU 2 上的第 2 號(hào) kworker 線程。
- 在多核系統(tǒng)中,每個(gè) CPU 都可能有多個(gè) kworker 線程。例如:
-
動(dòng)態(tài)生成的 kworker 線程:
- kworker 線程的數(shù)量和命名不是固定的,它們根據(jù)內(nèi)核的工作負(fù)載動(dòng)態(tài)生成和銷(xiāo)毀。
- 如果內(nèi)核的某些任務(wù)需要延遲執(zhí)行或者負(fù)載增加時(shí),會(huì)創(chuàng)建更多的 kworker 線程。
-
如何查看這些線程:
- 使用
htop
、top
或直接在/proc
文件系統(tǒng)中可以看到這些 kworker 線程。 - 比如運(yùn)行以下命令可以列出所有
kworker
線程:ps -e | grep kworker
- 使用
-
調(diào)試信息中的 kworker:
dmesg
或內(nèi)核日志中經(jīng)常會(huì)看到kworker
線程參與的內(nèi)核任務(wù),它們通常用來(lái)執(zhí)行 硬件中斷處理的延遲部分、文件系統(tǒng)同步、網(wǎng)絡(luò)包處理 等。
總結(jié)
kworker/0:1
是內(nèi)核的工作線程。0
是 CPU 編號(hào),表示線程綁定在 CPU 0 上。1
是線程序號(hào),表示 CPU 0 上的第 1 號(hào)工作線程。
卸載驅(qū)動(dòng)程序模塊
rmmod gpio_key_drv.ko
運(yùn)行上面命令后,過(guò)了較長(zhǎng)時(shí)間系統(tǒng)仍然能正常運(yùn)行,說(shuō)明卸載沒(méi)有問(wèn)題。證明說(shuō)明工作隊(duì)列(Workqueue)的相關(guān)資源確實(shí)并不需要釋放。原因就是其占用的資源由內(nèi)核處理工作隊(duì)列(Workqueue)的線程和相關(guān)機(jī)制自動(dòng)管理, 所以不需要釋放什么資源。
附完整工程文件
https://pan.baidu.com/s/1b6Nysvb4zU9B1bNQNeh3rw?pwd=cvjq