Linux驅(qū)動實踐:驅(qū)動程序如何發(fā)送【信號】給應(yīng)用程序?
別人的經(jīng)驗,我們的階梯!
大家好,我是道哥,今天我為大伙兒解說的技術(shù)知識點是:【驅(qū)動層中,如何發(fā)送信號給應(yīng)用程序】。
在上一篇文章中,我們討論的是:在應(yīng)用層如何發(fā)送指令來控制驅(qū)動層的 GPIOLinux驅(qū)動實踐:如何編寫【 GPIO 】設(shè)備的驅(qū)動程序???刂频姆较蚴菑膽?yīng)用層到驅(qū)動層:
那么,如果想讓程序的執(zhí)行路徑從下往上,也就是從驅(qū)動層傳遞到應(yīng)用層,應(yīng)該如何實現(xiàn)呢?
最容易、最簡單的方式,就是通過發(fā)送信號!
這篇文章繼續(xù)以完整的代碼實例來演示如何實現(xiàn)這個功能。
kill 命令和信號
使用 kill 命令發(fā)送信號
關(guān)于 Linux 操作系統(tǒng)的信號,每位程序員都知道這個指令:使用 kill 工具來“殺死”一個進(jìn)程:
- $ kill -9 <進(jìn)程的 PID>
 
這個指令的功能是:向指定的某個進(jìn)程發(fā)送一個信號 9,這個信號的默認(rèn)功能是:是停止進(jìn)程。
雖然在應(yīng)用程序中沒有主動處理這個信號,但是操作系統(tǒng)默認(rèn)的處理動作是終止應(yīng)用程序的執(zhí)行。
除了發(fā)送信號 9,kill 命令還可以發(fā)送其他的任意信號。
在 Linux 系統(tǒng)中,所有的信號都使用一個整型數(shù)值來表示,可以打開文件 /usr/include/x86_64-linux-gnu/bits/signum.h(你的系統(tǒng)中可能位于其它的目錄) 查看一下,比較常見的幾個信號是:
- /* 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 對應(yīng)著 SIGKILL,而信號11(SIGSEGV)就是最令人討厭的Segmentfault!
這里還有一個地方需要注意一下:實時信號和非實時信號,它倆的主要區(qū)別是:
- 非實時信號:操作系統(tǒng)不確保應(yīng)用程序一定能接收到(即:信號可能會丟失);
 - 實時信號:操作系統(tǒng)確保應(yīng)用程序一定能接收到;
 
如果我們的程序設(shè)計,通過信號機(jī)制來完成一些功能,那么為了確保信號不會丟失,肯定是使用實時信號的。
從文件 signum.h 中可以看到,實時信號從 __SIGRTMIN(數(shù)值:32) 開始。
多線程中的信號
我們在編寫應(yīng)用程序時,雖然沒有接收并處理 SIGKILL 這個信號,但是一旦別人發(fā)送了這個信號,我們的程序就被操作系統(tǒng)停止掉了,這是默認(rèn)的動作。
那么,在應(yīng)用程序中,應(yīng)該可以主動聲明接收并處理指定的信號,下面就來寫一個最簡單的實例。
在一個應(yīng)用程序中,可能存在多個線程;
當(dāng)有一個信號發(fā)送給此進(jìn)程時,所有的線程都可能接收到,但是只能有一個線程來處理;
在這個示例中,只有一個主線程來接收并處理信號;
信號注冊和處理函數(shù)
按照慣例,所有應(yīng)用程序文件都創(chuàng)建在 ~/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>
 - // 信號處理函數(shù)
 - static void signal_handler(int signum, siginfo_t *info, void *context)
 - {
 - // 打印接收到的信號值
 - printf("signal_handler: signum = %d \n", signum);
 - }
 - int main(void)
 - {
 - int count = 0;
 - // 注冊信號處理函數(shù)
 - 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);
 - // 一直循環(huán)打印信息,等待接收發(fā)信號
 - while (1)
 - {
 - printf("app_handle_signal is running...count = %d \n", ++count);
 - sleep(5);
 - }
 - return 0;
 - }
 
這個示例程序接收的信號是 SIGUSR1 和 SIGUSR2,也就是數(shù)值 10 和 12。
編譯、執(zhí)行:
- $ gcc app_handle_signal.c -o app_handle_signal
 - $ ./app_handle_signal
 
此時,應(yīng)用程序開始執(zhí)行,等待接收信號。
在另一個終端中,使用kill指令來發(fā)送信號SIGUSR1或者 SIGUSR2。
kill 發(fā)送信號,需要知道應(yīng)用程序的 PID,可以通過指令: ps -au | grep app_handle_signal 來查看。
其中的15428就是進(jìn)程的 PID。
執(zhí)行發(fā)送信號SIGUSR1指令:
- $ kill -10 15428
 
此時,在應(yīng)用程序的終端窗口中,就能看到下面的打印信息:
說明應(yīng)用程序接收到了 SIGUSR1 這個信號!
注意:我們是使用kill命令來發(fā)送信號的,kill 也是一個獨立的進(jìn)程,程序的執(zhí)行路徑如下:
在這個執(zhí)行路徑中,我們可控的部分是應(yīng)用層,至于操作系統(tǒng)是如何接收kill的操作,然后如何發(fā)送信號給 app_handle_signal 進(jìn)程的,我們不得而知。
下面就繼續(xù)通過示例代碼來看一下如何在驅(qū)動層主動發(fā)送信號。
驅(qū)動程序代碼示例:發(fā)送信號
功能需求
在剛才的簡單示例中,可以得出下面這些信息:
- 信號發(fā)送方:必須知道向誰[PID]發(fā)送信號,發(fā)送哪個信號;
 - 信號接收方:必須定義信號處理函數(shù),并且向操作系統(tǒng)注冊:接收哪些信號;
 
發(fā)送方當(dāng)然就是驅(qū)動程序了,在示例代碼中,繼續(xù)使用 SIGUSR1 信號來測試。
那么,驅(qū)動程序如何才能知道應(yīng)用程序的PID呢?可以讓應(yīng)用程序通過oictl函數(shù),把自己的PID主動告訴驅(qū)動程序:
驅(qū)動程序
這里的示例代碼,是在上一篇文章的基礎(chǔ)上修改的,改動部分的內(nèi)容,使用宏定義 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 文件的內(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>
 - // GPIO 硬件相關(guān)宏定義
 - #define MYGPIO_HW_ENABLE
 - // 新增部分,使用這個宏控制起來
 - #define MY_SIGNAL_ENABLE
 - // 設(shè)備名稱
 - #define MYGPIO_NAME "mygpio"
 - // 一共有4個GPIO
 - #define MYGPIO_NUMBER 4
 - // 設(shè)備類
 - static struct class *gpio_class;
 - // 用來保存設(shè)備
 - struct cdev gpio_cdev[MYGPIO_NUMBER];
 - // 用來保存設(shè)備號
 - int gpio_major = 0;
 - int gpio_minor = 0;
 - #ifdef MY_SIGNAL_ENABLE
 - // 用來保存向誰發(fā)送信號,應(yīng)用程序通過 ioctl 把自己的進(jìn)程 ID 設(shè)置進(jìn)來。
 - static int g_pid = 0;
 - #endif
 - #ifdef MYGPIO_HW_ENABLE
 - // 硬件初始化函數(shù),在驅(qū)動程序被加載的時候(gpio_driver_init)被調(diào)用
 - 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);
 - }
 - // 設(shè)置硬件GPIO的狀態(tài),在控制GPIO的時候(gpio_ioctl)被調(diào)研
 - 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
 - // 用來發(fā)送信號給應(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)造信號結(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ā)送信號
 - ret = send_sig_info(sig_no, &info, my_task);
 - if (ret < 0)
 - {
 - printk("send signal failed! \n");
 - }
 - }
 - #endif
 - // 當(dāng)應(yīng)用程序打開設(shè)備的時候被調(diào)用
 - 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)
 - {
 - // 說明應(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;
 - }
 - printk("save g_pid success: %d \n", g_pid);
 - if (g_pid > 0)
 - {
 - // 發(fā)送信號
 - send_signal(SIGUSR1);
 - send_signal(SIGUSR2);
 - }
 - }
 - return 0;
 - }
 - #else
 - // 當(dāng)應(yīng)用程序控制GPIO的時候被調(diào)用
 - 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");
 - // 動態(tài)申請設(shè)備號(嚴(yán)謹(jǐn)點的話,應(yīng)該檢查函數(shù)返回值)
 - alloc_chrdev_region(&num_dev, gpio_minor, MYGPIO_NUMBER, MYGPIO_NAME);
 - // 獲取主設(shè)備號
 - gpio_major = MAJOR(num_dev);
 - printk("gpio_major = %d. \n", gpio_major);
 - // 創(chuàng)建設(shè)備類
 - gpio_class = class_create(THIS_MODULE, MYGPIO_NAME);
 - // 創(chuàng)建設(shè)備節(jié)點
 - for (i = 0; i < MYGPIO_NUMBER; ++i)
 - {
 - // 設(shè)備號
 - devno = MKDEV(gpio_major, gpio_minor + i);
 - // 初始化cdev結(jié)構(gòu)
 - cdev_init(&gpio_cdev[i], &gpio_ops);
 - // 注冊字符設(shè)備
 - cdev_add(&gpio_cdev[i], devno, 1);
 - // 創(chuàng)建設(shè)備節(jié)點
 - 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");
 - // 刪除設(shè)備節(jié)點
 - for (i = 0; i < MYGPIO_NUMBER; ++i)
 - {
 - cdev_del(&gpio_cdev[i]);
 - device_destroy(gpio_class, MKDEV(gpio_major, gpio_minor + i));
 - }
 - // 釋放設(shè)備類
 - class_destroy(gpio_class);
 - #ifdef MYGPIO_HW_ENABLE
 - for (i = 0; i < MYGPIO_NUMBER; ++i)
 - {
 - gpio_hw_release(i);
 - }
 - #endif
 - // 注銷設(shè)備號
 - unregister_chrdev_region(MKDEV(gpio_major, gpio_minor), MYGPIO_NUMBER);
 - }
 - MODULE_LICENSE("GPL");
 - module_init(gpio_driver_init);
 - module_exit(gpio_driver_exit);
 
這里大部分的代碼,在上一篇文章中已經(jīng)描述的比較清楚了,這里把重點關(guān)注放在這兩個函數(shù)上:gpio_ioctl 和 send_signal。
(1)函數(shù) gpio_ioctl
當(dāng)應(yīng)用程序調(diào)用 ioctl() 的時候,驅(qū)動程序中的 gpio_ioctl 就會被調(diào)用。
這里定義一個簡單的協(xié)議:當(dāng)應(yīng)用程序調(diào)用參數(shù)中 cmd 為 100 的時候,就表示用來告訴驅(qū)動程序自己的 PID。
驅(qū)動程序定義了一個全局變量 g_pid,用來保存應(yīng)用程序傳入的參數(shù)PID。
需要調(diào)用函數(shù) copy_from_user(&g_pid, pArg, sizeof(int)),把用戶空間的參數(shù)復(fù)制到內(nèi)核空間中;
成功取得PID之后,就調(diào)用函數(shù) send_signal 向應(yīng)用程序發(fā)送信號。
這里僅僅是用于演示目的,在實際的項目中,可能會根據(jù)接收到硬件觸發(fā)之后再發(fā)送信號。
(2)函數(shù) send_signal
這個函數(shù)主要做了3件事情:
構(gòu)造一個信號結(jié)構(gòu)體變量:struct siginfo info;
通過應(yīng)用程序傳入的 PID,獲取任務(wù)信息:pid_task(find_vpid(g_pid), PIDTYPE_PID);
發(fā)送信號:send_sig_info(sig_no, &info, my_task);
驅(qū)動模塊 Makefile
- $ touch Makefile
 
內(nèi)容如下:
- 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
 
編譯驅(qū)動模塊
- $ make
 
得到驅(qū)動程序: my_driver_signal.ko 。
加載驅(qū)動模塊
- $ sudo insmod my_driver_signal.ko
 
通過 dmesg 指令來查看驅(qū)動模塊的打印信息:
因為示例代碼是在上一篇GPIO的基礎(chǔ)上修改的,因此創(chuàng)建的設(shè)備節(jié)點文件,與上篇文章是一樣的:
應(yīng)用程序代碼示例:接收信號
注冊信號處理函數(shù)
應(yīng)用程序仍然放在 ~/tmp/App/ 目錄下。
- $ mkdir ~/tmp/App/app_mysignal
 - $ cd ~/tmp/App/app_mysignal
 - $ touch mysignal.c
 
文件內(nèi)容如下:
- #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"
 - };
 - // 信號處理函數(shù)
 - 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");
 - // 注冊信號處理函數(shù)
 - 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);
 - // 關(guān)閉設(shè)備
 - close(fd);
 - }
 
可以看到,應(yīng)用程序主要做了兩件事情:
(1)首先通過函數(shù) sigaction() 向操作系統(tǒng)注冊了信號 SIGUSR1 和 SIGUSR2,它倆的信號處理函數(shù)是同一個:signal_handler()。
除了 sigaction 函數(shù),應(yīng)用程序還可以使用 signal 函數(shù)來注冊信號處理函數(shù);
(2)然后通過 ioctl(fd, 100, &pid); 向驅(qū)動程序設(shè)置自己的 PID。
編譯應(yīng)用程序:
- $ gcc mysignal.c -o mysignal
 
執(zhí)行應(yīng)用程序:
- $ sudo ./mysignal
 
根據(jù)剛才驅(qū)動程序的代碼,當(dāng)驅(qū)動程序接收到設(shè)置PID的命令之后,會立刻發(fā)送兩個信號:
先來看一下 dmesg 中驅(qū)動程序的打印信息:
可以看到:驅(qū)動把這兩個信號(10 和 12),發(fā)送給了應(yīng)用程序(PID=6259)。
應(yīng)用程序的輸出信息如下:
可以看到:應(yīng)用程序接收到信號 10 和 12,并且正確打印出信號中攜帶的一些信息!
本文轉(zhuǎn)載自微信公眾號「IOT物聯(lián)網(wǎng)小鎮(zhèn)」




























 
 
 


 
 
 
 