Linux驅動實踐:驅動程序如何發送【信號】給應用程序?


作 者:道哥,10+年嵌入式開發老兵,專注於:C/C++、嵌入式、Linux

關注下方公眾號,回復【書籍】,獲取 Linux、嵌入式領域經典書籍;回復【PDF】,獲取所有原創文章( PDF 格式)。

目錄

別人的經驗,我們的階梯!

大家好,我是道哥,今天我為大伙兒解說的技術知識點是:【驅動層中,如何發送信號給應用程序】

在上一篇文章中,我們討論的是:在應用層如何發送指令來控制驅動層的 GPIOLinux驅動實踐:如何編寫【 GPIO 】設備的驅動程序?。控制的方向是從應用層到驅動層

那么,如果想讓程序的執行路徑從下往上,也就是從驅動層傳遞到應用層,應該如何實現呢?

最容易、最簡單的方式,就是通過發送信號

這篇文章繼續以完整的代碼實例來演示如何實現這個功能。

kill 命令和信號

使用 kill 命令發送信號

關於 Linux 操作系統的信號,每位程序員都知道這個指令:使用 kill 工具來“殺死”一個進程:

$ kill -9 <進程的 PID>

這個指令的功能是:向指定的某個進程發送一個信號 9,這個信號的默認功能是:是停止進程。

雖然在應用程序中沒有主動處理這個信號,但是操作系統默認的處理動作是終止應用程序的執行。

除了發送信號 9kill 命令還可以發送其他的任意信號。

Linux 系統中,所有的信號都使用一個整型數值來表示,可以打開文件 /usr/include/x86_64-linux-gnu/bits/signum.h(你的系統中可能位於其他的目錄) 查看一下,比較常見的幾個信號是:

/* Signals.  */
#define	SIGINT		2	/* Interrupt (ANSI).  */
#define	SIGKILL		9	/* Kill, unblockable (POSIX).  */
#define	SIGUSR1		10	/* User-defined signal 1 (POSIX).  */
#define	SIGSEGV		11	/* Segmentation violation (ANSI).  */
#define	SIGUSR2		12	/* User-defined signal 2 (POSIX).  */
...
...
#define SIGSYS		31	/* Bad system call.  */
#define SIGUNUSED	31

#define	_NSIG		65	/* Biggest signal number + 1
				   (including real-time signals).  */

/* These are the hard limits of the kernel.  These values should not be
   used directly at user level.  */
#define __SIGRTMIN	32
#define __SIGRTMAX	(_NSIG - 1)

信號 9 對應着 SIGKILL,而信號11SIGSEGV)就是最令人討厭的Segmentfault

這里還有一個地方需要注意一下:實時信號和非實時信號,它倆的主要區別是:

  1. 非實時信號:操作系統不確保應用程序一定能接收到(即:信號可能會丟失);

  2. 實時信號:操作系統確保應用程序一定能接收到;

如果我們的程序設計,通過信號機制來完成一些功能,那么為了確保信號不會丟失,肯定是使用實時信號的。

從文件 signum.h 中可以看到,實時信號從 __SIGRTMIN(數值:32) 開始。

多線程中的信號

我們在編寫應用程序時,雖然沒有接收並處理 SIGKILL 這個信號,但是一旦別人發送了這個信號,我們的程序就被操作系統停止掉了,這是默認的動作。

那么,在應用程序中,應該可以主動聲明接收並處理指定的信號,下面就來寫一個最簡單的實例。

在一個應用程序中,可能存在多個線程;

當有一個信號發送給此進程時,所有的線程都可能接收到,但是只能有一個線程來處理;

在這個示例中,只有一個主線程來接收並處理信號;

信號注冊和處理函數

按照慣例,所有應用程序文件都創建在 ~/tmp/App 目錄中。

// 文件:tmp/App/app_handle_signal/app_handle_signal.c

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys ioctl.h="">
#include <signal.h>

// 信號處理函數
static void signal_handler(int signum, siginfo_t *info, void *context)
{
	// 打印接收到的信號值
    printf("signal_handler: signum = %d \n", signum);
}

int main(void)
{
	int count = 0;
	// 注冊信號處理函數
	struct sigaction sa;
	sigemptyset(&sa.sa_mask);
	sa.sa_sigaction = &signal_handler;
	sa.sa_flags = SA_SIGINFO;
	sigaction(SIGUSR1, &sa, NULL);
	sigaction(SIGUSR2, &sa, NULL);

	// 一直循環打印信息,等待接收發信號
	while (1)
	{
		printf("app_handle_signal is running...count = %d \n", ++count);
		sleep(5);
	}

	return 0;
}

這個示例程序接收的信號是 SIGUSR1SIGUSR2,也就是數值 1012

編譯、執行:

$ gcc app_handle_signal.c -o app_handle_signal
$ ./app_handle_signal

此時,應用程序開始執行,等待接收信號

在另一個終端中,使用kill指令來發送信號SIGUSR1或者 SIGUSR2

kill 發送信號,需要知道應用程序的 PID,可以通過指令: ps -au | grep app_handle_signal 來查看。

其中的15428就是進程的 PID

執行發送信號SIGUSR1指令:

$ kill -10 15428

此時,在應用程序的終端窗口中,就能看到下面的打印信息:

說明應用程序接收到了 SIGUSR1 這個信號

注意:我們是使用kill命令來發送信號的,kill 也是一個獨立的進程,程序的執行路徑如下:

在這個執行路徑中,我們可控的部分是應用層,至於操作系統是如何接收kill的操作,然后如何發送信號給 app_handle_signal 進程的,我們不得而知。

下面就繼續通過示例代碼來看一下如何在驅動層主動發送信號。

驅動程序代碼示例:發送信號

功能需求

在剛才的簡單示例中,可以得出下面這些信息:

  1. 信號發送方:必須知道向誰[PID]發送信號,發送哪個信號;

  2. 信號接收方:必須定義信號處理函數,並且向操作系統注冊:接收哪些信號;

發送方當然就是驅動程序了,在示例代碼中,繼續使用 SIGUSR1 信號來測試。

那么,驅動程序如何才能知道應用程序的PID呢?可以讓應用程序通過oictl函數,把自己的PID主動告訴驅動程序

驅動程序

這里的示例代碼,是在上一篇文章的基礎上修改的,改動部分的內容,使用宏定義 MY_SIGNAL_ENABLE 控制起來,方便查看和比較。

以下所有操作的工作目錄,都是與上一篇文章相同的,即:~/tmp/linux-4.15/drivers/

$ cd ~/tmp/linux-4.15/drivers/
$ mkdir my_driver_signal
$ cd my_driver_signal
$ touch my_driver_signal.c

my_driver_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="">

// GPIO 硬件相關宏定義
#define MYGPIO_HW_ENABLE

// 新增部分,使用這個宏控制起來
#define MY_SIGNAL_ENABLE

// 設備名稱
#define MYGPIO_NAME			"mygpio"

// 一共有4個GPIO
#define MYGPIO_NUMBER		4

// 設備類
static struct class *gpio_class;

// 用來保存設備
struct cdev gpio_cdev[MYGPIO_NUMBER];

// 用來保存設備號
int gpio_major = 0;
int gpio_minor = 0;

#ifdef MY_SIGNAL_ENABLE
// 用來保存向誰發送信號,應用程序通過 ioctl 把自己的進程 ID 設置進來。
static int g_pid = 0;
#endif

#ifdef MYGPIO_HW_ENABLE
// 硬件初始化函數,在驅動程序被加載的時候(gpio_driver_init)被調用
static void gpio_hw_init(int gpio)
{
	printk("gpio_hw_init is called: %d. \n", gpio);
}

// 硬件釋放
static void gpio_hw_release(int gpio)
{
	printk("gpio_hw_release is called: %d. \n", gpio);
}

// 設置硬件GPIO的狀態,在控制GPIO的時候(gpio_ioctl)被調研
static void gpio_hw_set(unsigned long gpio_no, unsigned int val)
{
	printk("gpio_hw_set is called. gpio_no = %ld, val = %d. \n", gpio_no, val);
}
#endif

#ifdef MY_SIGNAL_ENABLE
// 用來發送信號給應用程序
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");
	}
}
#endif

// 當應用程序打開設備的時候被調用
static int gpio_open(struct inode *inode, struct file *file)
{
	
	printk("gpio_open is called. \n");
	return 0;	
}

#ifdef MY_SIGNAL_ENABLE
static long gpio_ioctl(struct file* file, unsigned int cmd, unsigned long arg)
{
	void __user *pArg;
	printk("gpio_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;
		}

		printk("save g_pid success: %d \n", g_pid); 
		if (g_pid > 0)
		{
			// 發送信號
			send_signal(SIGUSR1);
			send_signal(SIGUSR2);
		}
	}

	return 0;
}
#else
// 當應用程序控制GPIO的時候被調用
static long gpio_ioctl(struct file* file, unsigned int val, unsigned long gpio_no)
{
	printk("gpio_ioctl is called. \n");
	
	if (0 != val && 1 != val)
	{
		printk("val is NOT valid! \n");
		return 0;
	}

	if (gpio_no >= MYGPIO_NUMBER)
	{
		printk("dev_no is invalid! \n");
		return 0;
	}

	printk("set GPIO: %ld to %d. \n", gpio_no, val);

#ifdef MYGPIO_HW_ENABLE
	gpio_hw_set(gpio_no, val);
#endif

	return 0;
}
#endif

static const struct file_operations gpio_ops={
	.owner = THIS_MODULE,
	.open  = gpio_open,
	.unlocked_ioctl = gpio_ioctl
};

static int __init gpio_driver_init(void)
{
	int i, devno;
	dev_t num_dev;

	printk("gpio_driver_init is called. \n");

	// 動態申請設備號(嚴謹點的話,應該檢查函數返回值)
	alloc_chrdev_region(&num_dev, gpio_minor, MYGPIO_NUMBER, MYGPIO_NAME);

	// 獲取主設備號
	gpio_major = MAJOR(num_dev);
	printk("gpio_major = %d. \n", gpio_major);

	// 創建設備類
	gpio_class = class_create(THIS_MODULE, MYGPIO_NAME);

	// 創建設備節點
	for (i = 0; i < MYGPIO_NUMBER; ++i)
	{
		// 設備號
		devno = MKDEV(gpio_major, gpio_minor + i);
		
		// 初始化cdev結構
		cdev_init(&gpio_cdev[i], &gpio_ops);

		// 注冊字符設備
		cdev_add(&gpio_cdev[i], devno, 1);

		// 創建設備節點
		device_create(gpio_class, NULL, devno, NULL, MYGPIO_NAME"%d", i);
	}

#ifdef MYGPIO_HW_ENABLE
	for (i = 0; i < MYGPIO_NUMBER; ++i)
	{
		// 初始硬件GPIO
		gpio_hw_init(i);
	}
#endif

	return 0;
}

static void __exit gpio_driver_exit(void)
{
	int i;
	printk("gpio_driver_exit is called. \n");

	// 刪除設備節點
	for (i = 0; i < MYGPIO_NUMBER; ++i)
	{
		cdev_del(&gpio_cdev[i]);
		device_destroy(gpio_class, MKDEV(gpio_major, gpio_minor + i));
	}

	// 釋放設備類
	class_destroy(gpio_class);

#ifdef MYGPIO_HW_ENABLE
	for (i = 0; i < MYGPIO_NUMBER; ++i)
	{
		gpio_hw_release(i);
	}
#endif

	// 注銷設備號
	unregister_chrdev_region(MKDEV(gpio_major, gpio_minor), MYGPIO_NUMBER);
}

MODULE_LICENSE("GPL");
module_init(gpio_driver_init);
module_exit(gpio_driver_exit);

這里大部分的代碼,在上一篇文章中已經描述的比較清楚了,這里把重點關注放在這兩個函數上:gpio_ioctlsend_signal

(1)函數 gpio_ioctl

當應用程序調用 ioctl() 的時候,驅動程序中的 gpio_ioctl 就會被調用。

這里定義一個簡單的協議:當應用程序調用參數中 cmd 為 100 的時候,就表示用來告訴驅動程序自己的 PID

驅動程序定義了一個全局變量 g_pid,用來保存應用程序傳入的參數PID

需要調用函數 copy_from_user(&g_pid, pArg, sizeof(int)),把用戶空間的參數復制到內核空間中;

成功取得PID之后,就調用函數 send_signal 向應用程序發送信號。

這里僅僅是用於演示目的,在實際的項目中,可能會根據接收到硬件觸發之后再發送信號。

(2)函數 send_signal

這個函數主要做了3件事情:

  1. 構造一個信號結構體變量:struct siginfo info;

  2. 通過應用程序傳入的 PID,獲取任務信息:pid_task(find_vpid(g_pid), PIDTYPE_PID);

  3. 發送信號:send_sig_info(sig_no, &info, my_task);

驅動模塊 Makefile

$ touch Makefile

內容如下:

ifneq ($(KERNELRELEASE),)
	obj-m := my_driver_signal.o
else
	KERNELDIR ?= /lib/modules/$(shell uname -r)/build
	PWD := $(shell pwd)
default:
	$(MAKE) -C $(KERNELDIR) M=$(PWD) modules
clean:
	$(MAKE) -C $(KERNEL_PATH) M=$(PWD) clean
endif

編譯驅動模塊

$ make

得到驅動程序: my_driver_signal.ko

加載驅動模塊

$ sudo insmod my_driver_signal.ko

通過 dmesg 指令來查看驅動模塊的打印信息:

因為示例代碼是在上一篇GPIO的基礎上修改的,因此創建的設備節點文件,與上篇文章是一樣的:

應用程序代碼示例:接收信號

注冊信號處理函數

應用程序仍然放在 ~/tmp/App/ 目錄下。

$ mkdir ~/tmp/App/app_mysignal
$ cd ~/tmp/App/app_mysignal
$ touch mysignal.c

文件內容如下:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <assert.h>
#include <fcntl.h>
#include <sys ioctl.h="">
#include <signal.h>

#define MY_GPIO_NUMBER		4

char gpio_name[MY_GPIO_NUMBER][16] = {
	"/dev/mygpio0",
	"/dev/mygpio1",
	"/dev/mygpio2",
	"/dev/mygpio3"
};

// 信號處理函數
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/mygpio0", 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);
	sigaction(SIGUSR2, &sa, NULL);

	// set PID 
	printf("call ioctl. pid = %d \n", pid);
	ioctl(fd, 100, &pid);

	// 休眠1秒,等待接收信號
	sleep(1);

	// 關閉設備
	close(fd);
}

可以看到,應用程序主要做了兩件事情:

(1)首先通過函數 sigaction() 向操作系統注冊了信號 SIGUSR1 和 SIGUSR2,它倆的信號處理函數是同一個:signal_handler()

除了 sigaction 函數,應用程序還可以使用 signal 函數來注冊信號處理函數;

(2)然后通過 ioctl(fd, 100, &pid); 向驅動程序設置自己的 PID

編譯應用程序:

$ gcc mysignal.c -o mysignal

執行應用程序:

$ sudo ./mysignal

根據剛才驅動程序的代碼,當驅動程序接收到設置PID的命令之后,會立刻發送兩個信號

先來看一下 dmesg 中驅動程序的打印信息:

可以看到:驅動把這兩個信號(10 和 12),發送給了應用程序(PID=6259)。

應用程序的輸出信息如下:

可以看到:應用程序接收到信號 10 和 12,並且正確打印出信號中攜帶的一些信息!


------ End ------

文中的測試代碼,已經放在網盤了。

在公眾號【IOT物聯網小鎮】后台回復關鍵字:1205,即可獲取下載地址。

謝謝!

推薦閱讀

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

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

【3】原來gdb的底層調試原理這么簡單

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

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

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

</signal.h></fcntl.h></assert.h></unistd.h></stdlib.h></stdio.h></signal.h></unistd.h></stdlib.h></stdio.h>


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM