Linux驅(qū)動(dòng)實(shí)踐:中斷處理函數(shù)如何【發(fā)送信號(hào)】給應(yīng)用層?
別人的經(jīng)驗(yàn),我們的階梯!
大家好,我是道哥,今天我為大伙兒解說的技術(shù)知識(shí)點(diǎn)是:【中斷程序如何發(fā)送信號(hào)給應(yīng)用層】。
最近分享的幾篇文章都比較基礎(chǔ),關(guān)于字符類設(shè)備的驅(qū)動(dòng)程序,以及中斷處理程序。
也許在現(xiàn)代的項(xiàng)目是用不到這樣的技術(shù),但是萬丈高樓平地起。
只有明白了這些最基礎(chǔ)的知識(shí)點(diǎn)之后,再去看那些進(jìn)化出來的高級(jí)玩意,才會(huì)有一步一個(gè)腳印的獲得感。
如果缺少了這些基礎(chǔ)的環(huán)節(jié),很多深層次的東西,學(xué)起來就有點(diǎn)空中樓閣的感覺。
就好比研究Linux內(nèi)核,如果一上來就從Linux 4.x/5.x內(nèi)核版本開始研究,可以看到很多“歷史遺留”代碼。
這些代碼就見證著Linux一步一步的發(fā)展歷史,甚至有些人還會(huì)專門去研究 Linux 0.11 版本的內(nèi)核源碼,因?yàn)楹芏嗷舅枷攵际且粯拥摹?/p>
今天這篇文章,主要還是以代碼實(shí)例為主,把之前的兩個(gè)知識(shí)點(diǎn)結(jié)合起來:
在中斷處理函數(shù)中,發(fā)送信號(hào)給應(yīng)用層,以此來通知應(yīng)用層處理響應(yīng)的中斷業(yè)務(wù)。
驅(qū)動(dòng)程序
示例代碼全貌
所有的操作都是在 ~/tmp/linux-4.15/drivers 目錄下完成的。
首先創(chuàng)建驅(qū)動(dòng)模塊目錄:
- $ cd ~/tmp/linux-4.15/drivers
- $ mkdir my_driver_interrupt_signal
- $ touch my_driver_interrupt_signal.c
文件內(nèi)容如下:
- #include <linux/module.h>
- #include <linux/kernel.h>
- #include <linux/ctype.h>
- #include <linux/device.h>
- #include <linux/cdev.h>
- #include <asm/siginfo.h>
- #include <linux/pid.h>
- #include <linux/uaccess.h>
- #include <linux/sched/signal.h>
- #include <linux/pid_namespace.h>
- #include <linux/interrupt.h>
- // 中斷號(hào)
- #define IRQ_NUM 1
- // 定義驅(qū)動(dòng)程序的 ID,在中斷處理函數(shù)中用來判斷是否需要處理
- #define IRQ_DRIVER_ID 1234
- // 設(shè)備名稱
- #define MYDEV_NAME "mydev"
- // 驅(qū)動(dòng)程序數(shù)據(jù)結(jié)構(gòu)
- struct myirq
- {
- int devid;
- };
- struct myirq mydev ={ IRQ_DRIVER_ID };
- #define KBD_DATA_REG 0x60
- #define KBD_STATUS_REG 0x64
- #define KBD_SCANCODE_MASK 0x7f
- #define KBD_STATUS_MASK 0x80
- // 設(shè)備類
- static struct class *my_class;
- // 用來保存設(shè)備
- struct cdev my_cdev;
- // 用來保存設(shè)備號(hào)
- int mydev_major = 0;
- int mydev_minor = 0;
- // 用來保存向誰發(fā)送信號(hào),應(yīng)用程序通過 ioctl 把自己的進(jìn)程 ID 設(shè)置進(jìn)來。
- static int g_pid = 0;
- // 用來發(fā)送信號(hào)給應(yīng)用程序
- static void send_signal(int sig_no)
- {
- int ret;
- struct siginfo info;
- struct task_struct *my_task = NULL;
- if (0 == g_pid)
- {
- // 說明應(yīng)用程序沒有設(shè)置自己的 PID
- printk("pid[%d] is not valid! \n", g_pid);
- return;
- }
- printk("send signal %d to pid %d \n", sig_no, g_pid);
- // 構(gòu)造信號(hào)結(jié)構(gòu)體
- memset(&info, 0, sizeof(struct siginfo));
- info.si_signo = sig_no;
- info.si_errno = 100;
- info.si_code = 200;
- // 獲取自己的任務(wù)信息,使用的是 RCU 鎖
- rcu_read_lock();
- my_task = pid_task(find_vpid(g_pid), PIDTYPE_PID);
- rcu_read_unlock();
- if (my_task == NULL)
- {
- printk("get pid_task failed! \n");
- return;
- }
- // 發(fā)送信號(hào)
- ret = send_sig_info(sig_no, &info, my_task);
- if (ret < 0)
- {
- printk("send signal failed! \n");
- }
- }
- //中斷處理函數(shù)
- static irqreturn_t myirq_handler(int irq, void * dev)
- {
- struct myirq mydev;
- unsigned char key_code;
- mydev = *(struct myirq*)dev;
- // 檢查設(shè)備 id,只有當(dāng)相等的時(shí)候才需要處理
- if (IRQ_DRIVER_ID == mydev.devid)
- {
- // 讀取鍵盤掃描碼
- key_code = inb(KBD_DATA_REG);
- if (key_code == 0x01)
- {
- printk("EXC key is pressed! \n");
- send_signal(SIGUSR1);
- }
- }
- return IRQ_HANDLED;
- }
- // 驅(qū)動(dòng)模塊初始化函數(shù)
- static void myirq_init(void)
- {
- printk("myirq_init is called. \n");
- // 注冊(cè)中斷處理函數(shù)
- if(request_irq(IRQ_NUM, myirq_handler, IRQF_SHARED, MYDEV_NAME, &mydev)!=0)
- {
- printk("register irq[%d] handler failed. \n", IRQ_NUM);
- return -1;
- }
- printk("register irq[%d] handler success. \n", IRQ_NUM);
- }
- // 當(dāng)應(yīng)用程序打開設(shè)備的時(shí)候被調(diào)用
- static int mydev_open(struct inode *inode, struct file *file)
- {
- printk("mydev_open is called. \n");
- return 0;
- }
- static long mydev_ioctl(struct file* file, unsigned int cmd, unsigned long arg)
- {
- void __user *pArg;
- printk("mydev_ioctl is called. cmd = %d \n", cmd);
- if (100 == cmd)
- {
- // 說明應(yīng)用程序設(shè)置進(jìn)程的 PID
- pArg = (void *)arg;
- if (!access_ok(VERIFY_READ, pArg, sizeof(int)))
- {
- printk("access failed! \n");
- return -EACCES;
- }
- // 把用戶空間的數(shù)據(jù)復(fù)制到內(nèi)核空間
- if (copy_from_user(&g_pid, pArg, sizeof(int)))
- {
- printk("copy_from_user failed! \n");
- return -EFAULT;
- }
- }
- return 0;
- }
- static const struct file_operations mydev_ops={
- .owner = THIS_MODULE,
- .open = mydev_open,
- .unlocked_ioctl = mydev_ioctl
- };
- static int __init mydev_driver_init(void)
- {
- int devno;
- dev_t num_dev;
- printk("mydev_driver_init is called. \n");
- // 注冊(cè)中斷處理函數(shù)
- if(request_irq(IRQ_NUM, myirq_handler, IRQF_SHARED, MYDEV_NAME, &mydev)!=0)
- {
- printk("register irq[%d] handler failed. \n", IRQ_NUM);
- return -1;
- }
- // 動(dòng)態(tài)申請(qǐng)?jiān)O(shè)備號(hào)(嚴(yán)謹(jǐn)點(diǎn)的話,應(yīng)該檢查函數(shù)返回值)
- alloc_chrdev_region(&num_dev, mydev_minor, 1, MYDEV_NAME);
- // 獲取主設(shè)備號(hào)
- mydev_major = MAJOR(num_dev);
- printk("mydev_major = %d. \n", mydev_major);
- // 創(chuàng)建設(shè)備類
- my_class = class_create(THIS_MODULE, MYDEV_NAME);
- // 創(chuàng)建設(shè)備節(jié)點(diǎn)
- devno = MKDEV(mydev_major, mydev_minor);
- // 初始化cdev結(jié)構(gòu)
- cdev_init(&my_cdev, &mydev_ops);
- // 注冊(cè)字符設(shè)備
- cdev_add(&my_cdev, devno, 1);
- // 創(chuàng)建設(shè)備節(jié)點(diǎn)
- device_create(my_class, NULL, devno, NULL, MYDEV_NAME);
- return 0;
- }
- static void __exit mydev_driver_exit(void)
- {
- printk("mydev_driver_exit is called. \n");
- // 刪除設(shè)備節(jié)點(diǎn)
- cdev_del(&my_cdev);
- device_destroy(my_class, MKDEV(mydev_major, mydev_minor));
- // 釋放設(shè)備類
- class_destroy(my_class);
- // 注銷設(shè)備號(hào)
- unregister_chrdev_region(MKDEV(mydev_major, mydev_minor), 1);
- // 注銷中斷處理函數(shù)
- free_irq(IRQ_NUM, &mydev);
- }
- MODULE_LICENSE("GPL");
- module_init(mydev_driver_init);
- module_exit(mydev_driver_exit);
以上代碼主要做了兩件事情:
- 注冊(cè)中斷號(hào) 1 的處理函數(shù):myirq_handler();
- 創(chuàng)建設(shè)備節(jié)點(diǎn) /dev/mydev;
這里的中斷號(hào)1,是鍵盤中斷。
因?yàn)樗枪蚕淼闹袛?,因此?dāng)鍵盤被按下的時(shí)候,操作系統(tǒng)就會(huì)依次調(diào)用所有的中斷處理函數(shù),當(dāng)然就包括我們的驅(qū)動(dòng)程序所注冊(cè)的這個(gè)函數(shù)。
中斷處理部分相關(guān)的幾處關(guān)鍵代碼如下:
- //中斷處理函數(shù)
- static irqreturn_t myirq_handler(int irq, void * dev)
- {
- ...
- }
- // 驅(qū)動(dòng)模塊初始化函數(shù)
- static void myirq_init(void)
- {
- ...
- request_irq(IRQ_NUM, myirq_handler, IRQF_SHARED, MYDEV_NAME, &mydev);
- ...
- }
在中斷處理函數(shù)中,目標(biāo)是發(fā)送信號(hào) SIGUSR1 到應(yīng)用層,因此驅(qū)動(dòng)程序需要知道應(yīng)用程序的進(jìn)程號(hào)(PID)。
根據(jù)之前的文章Linux驅(qū)動(dòng)實(shí)踐:驅(qū)動(dòng)程序如何發(fā)送【信號(hào)】給應(yīng)用程序?,應(yīng)用程序必須主動(dòng)把自己的 PID 告訴驅(qū)動(dòng)模塊才可以。這可以通過 write 或者ioctl函數(shù)來實(shí)現(xiàn),
驅(qū)動(dòng)程序用來接收 PID 的相關(guān)代碼是:
- static long mydev_ioctl(struct file* file, unsigned int cmd, unsigned long arg)
- {
- ...
- if (100 == cmd)
- {
- pArg = (void *)arg;
- ...
- copy_from_user(&g_pid, pArg, sizeof(int));
- }
- }
知道了應(yīng)用程序的 PID,驅(qū)動(dòng)程序就可以在中斷發(fā)生的時(shí)候(按下鍵盤ESC鍵),發(fā)送信號(hào)出去了:
- static void send_signal(int sig_no)
- {
- struct siginfo info;
- ...
- send_sig_info(...);
- }
- static irqreturn_t myirq_handler(int irq, void * dev)
- {
- ...
- send_signal(SIGUSR1);
- }
Makefile 文件
- ifneq ($(KERNELRELEASE),)
- obj-m := my_driver_interrupt_signal.o
- else
- KERNELDIR ?= /lib/modules/$(shell uname -r)/build
- PWD := $(shell pwd)
- default:
- $(MAKE) -C $(KERNELDIR) M=$(PWD) modules
- clean:
- rm -rf *.o *.ko *.mod.* modules.* Module.*
- $(MAKE) -C $(KERNEL_PATH) M=$(PWD) clean
- endif
編譯、測(cè)試
首先查看一下加載驅(qū)動(dòng)模塊之前,1號(hào)中斷的所有驅(qū)動(dòng)程序:
再看一下設(shè)備號(hào):
- $ cat /proc/devices
因?yàn)轵?qū)動(dòng)注冊(cè)在創(chuàng)建設(shè)備節(jié)點(diǎn)的時(shí)候,是動(dòng)態(tài)請(qǐng)求系統(tǒng)分配的。
根據(jù)之前的幾篇文章可以知道,系統(tǒng)一般會(huì)分配244這個(gè)主設(shè)備號(hào)給我們,此刻還不存在這個(gè)設(shè)備號(hào)。
編譯、加載驅(qū)動(dòng)模塊:
- $ make
- $ sudo insmod my_driver_interrupt_signal.ko
首先看一下 dmesg 的輸出信息:
然后看一下中斷驅(qū)動(dòng)程序:
可以看到我們的驅(qū)動(dòng)程序( mydev )已經(jīng)登記在1號(hào)中斷的最右面。
最后看一下設(shè)備節(jié)點(diǎn)情況:
驅(qū)動(dòng)模塊已經(jīng)準(zhǔn)備妥當(dāng),下面就是應(yīng)用程序了。
應(yīng)用程序
應(yīng)用程序的主要功能就是兩部分:
通過 ioctl 函數(shù)把自己的 PID 告訴驅(qū)動(dòng)程序;
注冊(cè)信號(hào) SIGUSR1 的處理函數(shù);
示例代碼全貌
- #include <stdio.h>
- #include <stdlib.h>
- #include <unistd.h>
- #include <assert.h>
- #include <fcntl.h>
- #include <sys/ioctl.h>
- #include <signal.h>
- char *dev_name = "/dev/mydev";
- // 信號(hào)處理函數(shù)
- static void signal_handler(int signum, siginfo_t *info, void *context)
- {
- // 打印接收到的信號(hào)值
- printf("signal_handler: signum = %d \n", signum);
- printf("signo = %d, code = %d, errno = %d \n",
- info->si_signo,
- info->si_code,
- info->si_errno);
- }
- int main(int argc, char *argv[])
- {
- int fd, count = 0;
- int pid = getpid();
- // 打開GPIO
- if((fd = open(dev_name, O_RDWR | O_NDELAY)) < 0){
- printf("open dev failed! \n");
- return -1;
- }
- printf("open dev success! \n");
- // 注冊(cè)信號(hào)處理函數(shù)
- struct sigaction sa;
- sigemptyset(&sa.sa_mask);
- sa.sa_sigaction = &signal_handler;
- sa.sa_flags = SA_SIGINFO;
- sigaction(SIGUSR1, &sa, NULL);
- // set PID
- printf("call ioctl. pid = %d \n", pid);
- ioctl(fd, 100, &pid);
- // 死循環(huán),等待接收信號(hào)
- while (1)
- sleep(1);
- // 關(guān)閉設(shè)備
- close(fd);
- }
在應(yīng)用程序的最后,是一個(gè) while(1) 死循環(huán)。因?yàn)橹挥性诎聪骆I盤上的ESC按鍵時(shí),驅(qū)動(dòng)程序才會(huì)發(fā)送信號(hào)上來,因此應(yīng)用程序需要一直存活著。
編譯、測(cè)試
新開一個(gè)中斷窗口,編譯、執(zhí)行應(yīng)用程序:
- $ gcc my_interrupt_singal.c -o my_interrupt_singal
- $ sudo ./my_interrupt_singal
- open dev success!
- call ioctl. pid = 12907
- // 這里進(jìn)入 while 循環(huán)
由于應(yīng)用程序調(diào)用了 open 和 ioctl 這兩個(gè)函數(shù),因此,驅(qū)動(dòng)程序中兩個(gè)對(duì)應(yīng)的函數(shù)就會(huì)被執(zhí)行。
這可以通過 dmesg 命令的輸出信息看出來:
這個(gè)時(shí)候,按下鍵盤上的 ESC 鍵,此時(shí)驅(qū)動(dòng)程序中打印如下信息:
說明:驅(qū)動(dòng)程序捕獲到了鍵盤上的 ESC 鍵,并且發(fā)送信號(hào)給應(yīng)用程序了。
在執(zhí)行應(yīng)用程序的終端窗口中,可以看到如下輸出信息:
說明:應(yīng)用程序接收到了驅(qū)動(dòng)程序發(fā)來的信號(hào)!