1. 程式人生 > 其它 >Linux驅動實踐:中斷處理函式如何【傳送訊號】給應用層?

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. 註冊中斷號 1 的處理函式:myirq_handler();

  2. 建立裝置節點 /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號中斷的最右面。

最後看一下裝置節點情況:

驅動模組已經準備妥當,下面就是應用程式了。

應用程式

應用程式的主要功能就是兩部分:

  1. 通過 ioctl 函式把自己的 PID 告訴驅動程式;

  2. 註冊訊號 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,即可獲取下載地址。

謝謝!

推薦閱讀

【1】《Linux 從頭學》系列文章

【2】C語言指標-從底層原理到花式技巧,用圖文和程式碼幫你講解透徹

【3】原來gdb的底層除錯原理這麼簡單

【4】內聯彙編很可怕嗎?看完這篇文章,終結它!

其他系列專輯:精選文章應用程式設計物聯網C語言

星標公眾號,第一時間看文章!