Linux驅動實踐:中斷處理函數如何【發送訊號】給應用層?
作 者:道哥,10+年嵌入式開發老兵,專註於:C/C++、嵌入式、Linux。
關注下方公眾號,回復【書籍】,獲取 Linux、嵌入式領域經典書籍;回復【PDF】,獲取所有原創文章( PDF 格式)。
目錄
別人的經驗,我們的階梯!
大家好,我是道哥,今天我為大伙兒解說的技術知識點是:【中斷程式如何發送訊號給應用層】。
最近分享的幾篇文章都比較基礎,關於字元類設備的驅動程式,以及中斷處理程式。
也許在現代的項目是用不到這樣的技術,但是萬丈高樓平地起。
只有明白了這些最基礎的知識點之後,再去看那些進化出來的高級玩意,才會有一步一個腳印的獲得感。
如果缺少了這些基礎的環節,很多深層次的東西,學起來就有點空中樓閣的感覺。
就好比研究Linux
內核,如果一上來就從Linux 4.x/5.x
內核版本開始研究,可以看到很多「歷史遺留」程式碼。
這些程式碼就見證著Linux
一步一步的發展歷史,甚至有些人還會專門去研究 Linux 0.11 版本的內核源碼,因為很多基本思想都是一樣的。
今天這篇文章,主要還是以程式碼實例為主,把之前的兩個知識點結合起來:
在中斷處理函數中,發送訊號給應用層,以此來通知應用層處理響應的中斷業務。
驅動程式
示例程式碼全貌
所有的操作都是在 ~/tmp/linux-4.15/drivers 目錄下完成的。
首先創建驅動模組目錄:
$ cd ~/tmp/linux-4.15/drivers
$ mkdir my_driver_interrupt_signal
$ touch my_driver_interrupt_signal.c
文件內容如下:
#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>
// 中斷號
#define IRQ_NUM 1
// 定義驅動程式的 ID,在中斷處理函數中用來判斷是否需要處理
#define IRQ_DRIVER_ID 1234
// 設備名稱
#define MYDEV_NAME "mydev"
// 驅動程式數據結構
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
// 設備類
static struct class *my_class;
// 用來保存設備
struct cdev my_cdev;
// 用來保存設備號
int mydev_major = 0;
int mydev_minor = 0;
// 用來保存向誰發送訊號,應用程式通過 ioctl 把自己的進程 ID 設置進來。
static int g_pid = 0;
// 用來發送訊號給應用程式
static void send_signal(int sig_no)
{
int ret;
struct siginfo info;
struct task_struct *my_task = NULL;
if (0 == g_pid)
{
// 說明應用程式沒有設置自己的 PID
printk("pid[%d] is not valid! \n", g_pid);
return;
}
printk("send signal %d to pid %d \n", sig_no, g_pid);
// 構造訊號結構體
memset(&info, 0, sizeof(struct siginfo));
info.si_signo = sig_no;
info.si_errno = 100;
info.si_code = 200;
// 獲取自己的任務資訊,使用的是 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;
}
// 發送訊號
ret = send_sig_info(sig_no, &info, my_task);
if (ret < 0)
{
printk("send signal failed! \n");
}
}
//中斷處理函數
static irqreturn_t myirq_handler(int irq, void * dev)
{
struct myirq mydev;
unsigned char key_code;
mydev = *(struct myirq*)dev;
// 檢查設備 id,只有當相等的時候才需要處理
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;
}
// 驅動模組初始化函數
static void myirq_init(void)
{
printk("myirq_init is called. \n");
// 註冊中斷處理函數
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);
}
// 當應用程式打開設備的時候被調用
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)
{
// 說明應用程式設置進程的 PID
pArg = (void *)arg;
if (!access_ok(VERIFY_READ, pArg, sizeof(int)))
{
printk("access failed! \n");
return -EACCES;
}
// 把用戶空間的數據複製到內核空間
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");
// 註冊中斷處理函數
if(request_irq(IRQ_NUM, myirq_handler, IRQF_SHARED, MYDEV_NAME, &mydev)!=0)
{
printk("register irq[%d] handler failed. \n", IRQ_NUM);
return -1;
}
// 動態申請設備號(嚴謹點的話,應該檢查函數返回值)
alloc_chrdev_region(&num_dev, mydev_minor, 1, MYDEV_NAME);
// 獲取主設備號
mydev_major = MAJOR(num_dev);
printk("mydev_major = %d. \n", mydev_major);
// 創建設備類
my_class = class_create(THIS_MODULE, MYDEV_NAME);
// 創建設備節點
devno = MKDEV(mydev_major, mydev_minor);
// 初始化cdev結構
cdev_init(&my_cdev, &mydev_ops);
// 註冊字元設備
cdev_add(&my_cdev, devno, 1);
// 創建設備節點
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");
// 刪除設備節點
cdev_del(&my_cdev);
device_destroy(my_class, MKDEV(mydev_major, mydev_minor));
// 釋放設備類
class_destroy(my_class);
// 註銷設備號
unregister_chrdev_region(MKDEV(mydev_major, mydev_minor), 1);
// 註銷中斷處理函數
free_irq(IRQ_NUM, &mydev);
}
MODULE_LICENSE("GPL");
module_init(mydev_driver_init);
module_exit(mydev_driver_exit);
以上程式碼主要做了兩件事情:
註冊中斷號 1 的處理函數:myirq_handler();
創建設備節點 /dev/mydev;
這裡的中斷號1,是鍵盤中斷。
因為它是共享的中斷,因此當鍵盤被按下的時候,作業系統就會依次調用所有的中斷處理函數,當然就包括我們的驅動程式所註冊的這個函數。
中斷處理部分相關的幾處關鍵程式碼如下:
//中斷處理函數
static irqreturn_t myirq_handler(int irq, void * dev)
{
...
}
// 驅動模組初始化函數
static void myirq_init(void)
{
...
request_irq(IRQ_NUM, myirq_handler, IRQF_SHARED, MYDEV_NAME, &mydev);
...
}
在中斷處理函數中,目標是發送訊號 SIGUSR1 到應用層,因此驅動程式需要知道應用程式的進程號(PID)。
根據之前的文章Linux驅動實踐:驅動程式如何發送【訊號】給應用程式?,應用程式必須主動把自己的 PID 告訴驅動模組才可以。這可以通過 write
或者ioctl
函數來實現,
驅動程式用來接收 PID 的相關程式碼是:
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));
}
}
知道了應用程式的 PID
,驅動程式就可以在中斷髮生的時候(按下鍵盤ESC
鍵),發送訊號出去了:
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
編譯、測試
首先查看一下載入驅動模組之前,1
號中斷的所有驅動程式:
再看一下設備號:
$ cat /proc/devices
因為驅動註冊在創建設備節點的時候,是動態請求系統分配的。
根據之前的幾篇文章可以知道,系統一般會分配244
這個主設備號給我們,此刻還不存在這個設備號。
編譯、載入驅動模組:
$ make
$ sudo insmod my_driver_interrupt_signal.ko
首先看一下 dmesg 的輸出資訊:
然後看一下中斷驅動程式:
可以看到我們的驅動程式( mydev )已經登記在1
號中斷的最右面。
最後看一下設備節點情況:
驅動模組已經準備妥當,下面就是應用程式了。
應用程式
應用程式的主要功能就是兩部分:
通過 ioctl 函數把自己的 PID 告訴驅動程式;
註冊訊號 SIGUSR1 的處理函數;
示例程式碼全貌
#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";
// 訊號處理函數
static void signal_handler(int signum, siginfo_t *info, void *context)
{
// 列印接收到的訊號值
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");
// 註冊訊號處理函數
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);
// 死循環,等待接收訊號
while (1)
sleep(1);
// 關閉設備
close(fd);
}
在應用程式的最後,是一個 while(1) 死循環。因為只有在按下鍵盤上的ESC
按鍵時,驅動程式才會發送訊號上來,因此應用程式需要一直存活著。
編譯、測試
新開一個中斷窗口,編譯、執行應用程式:
$ gcc my_interrupt_singal.c -o my_interrupt_singal
$ sudo ./my_interrupt_singal
open dev success!
call ioctl. pid = 12907
// 這裡進入 while 循環
由於應用程式調用了 open 和 ioctl 這兩個函數,因此,驅動程式中兩個對應的函數就會被執行。
這可以通過 dmesg 命令的輸出資訊看出來:
這個時候,按下鍵盤上的 ESC 鍵,此時驅動程式中列印如下資訊:
說明:驅動程式捕獲到了鍵盤上的 ESC 鍵,並且發送訊號給應用程式了。
在執行應用程式的終端窗口中,可以看到如下輸出資訊:
說明:應用程式接收到了驅動程式發來的訊號!
—— End ——
文中的測試程式碼和相關文檔,已經放在網盤了。
在公眾號【IOT物聯網小鎮】後台回復關鍵字:1220,即可獲取下載地址。
謝謝!
推薦閱讀
【2】C語言指針-從底層原理到花式技巧,用圖文和程式碼幫你講解透徹
星標公眾號,第一時間看文章!