开发后端

在上一篇文章中,我们讨论的是:在应用层怎样发送指令来控制驱动层的GPIOLinux驱动实践:怎么编撰【GPIO】设备的驱动程序。明天我为大家儿解说的技术知识点是:【驱动层中,怎样发送讯号给应用程序】

[[438633]]

他人的经验,我们的阶梯!

你们好,我是道哥,明天我为大家儿解说的技术知识点是:【驱动层中,怎样发送讯号给应用程序】。

在上一篇文章中,我们讨论的是:在应用层怎样发送指令来控制驱动层的GPIOLinux驱动实践:怎么编撰【GPIO】设备的驱动程序?。控制的方向是从应用层到驱动层:

linux应用和驱动哪个更好_linux 驱动通知应用层_驱动调用应用层函数

这么,假如想让程序的执行路径从下往上,也就是从驱动层传递到应用层,应当怎样实现呢?

linux 驱动通知应用层_驱动调用应用层函数_linux应用和驱动哪个更好

最容易、最简单的方法,就是通过发送讯号!

这篇文章继续以完整的代码实例来演示怎么实现这个功能。

kill使用kill命令发送讯号

关于Linux操作系统的讯号,每个程序员都晓得这个指令:使用kill工具来“杀死”一个进程:

复制

$ kill -9  
  • 1.

这个指令的功能是:向指定的某个进程发送一个讯号9,这个讯号的默认功能是:是停止进程。

其实在应用程序中没有主动处理这个讯号,并且操作系统默认的处理动作是中止应用程序的执行。

不仅发送讯号9,kill命令还可以发送其他的任意讯号。

在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) 
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.

讯号9对应着SIGKILL,而讯号11(SIGSEGV)就是最令人厌恶的Segmentfault!

这儿还有一个地方须要注意一下:实时讯号和非实时讯号,它俩的主要区别是:

非实时讯号:操作系统不确保应用程序一定能接收到(即:讯号可能会遗失);实时讯号:操作系统确保应用程序一定能接收到;

假如我们的程序设计,通过讯号机制来完成一些功能,这么为了确保讯号不会遗失,肯定是使用实时讯号的。

从文件signum.h中可以见到,实时讯号从__SIGRTMIN(数值:32)开始。

多线程中的讯号

我们在编撰应用程序时,即使没有接收并处理SIGKILL这个讯号,然而一旦他人发送了这个讯号,我们的程序就被操作系统停止掉了,这是默认的动作。

这么,在应用程序中,应当可以主动申明接收并处理指定的讯号linux 驱动通知应用层,下边就来写一个最简单的实例。

在一个应用程序中,可能存在多个线程;

当有一个讯号发送给此进程时,所有的线程都可能接收到,并且只能有一个线程来处理;

在这个示例中,只有一个主线程来接收并处理讯号;

讯号注册和处理函数

依照惯例,所有应用程序文件都创建在~/tmp/App目录中。

复制

// 文件:tmp/App/app_handle_signal/app_handle_signal.c 
 
#include  
#include  
#include  
#include  
#include  
 
// 信号处理函数 
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; 

  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.
  • 34.
  • 35.

这个示例程序接收的讯号是SIGUSR1和SIGUSR2,也就是数值10和12。

编译、执行:

复制

$ gcc app_handle_signal.c -o app_handle_signal 
$ ./app_handle_signal 
  • 1.
  • 2.

此时,应用程序开始执行,等待接收讯号。

在另一个终端中,使用kill指令来发送讯号SIGUSR1或则SIGUSR2。

kill发送讯号,须要晓得应用程序的PID,可以通过指令:ps-au|grepapp_handle_signal来查看。

其中的15428就是进程的PID。

执行发送讯号SIGUSR1指令:

复制

$ kill -10 15428 
  • 1.

此时,在应用程序的终端窗口中,能够看见下边的复印信息:

linux应用和驱动哪个更好_驱动调用应用层函数_linux 驱动通知应用层

说明应用程序接收到了SIGUSR1这个讯号!

注意:我们是使用kill命令来发送讯号的,kill也是一个独立的进程,程序的执行路径如下:

linux应用和驱动哪个更好_linux 驱动通知应用层_驱动调用应用层函数

在这个执行路径中,我们可控的部份是应用层,至于操作系统是怎样接收kill的操作,之后怎样发送讯号给app_handle_signal进程的,我们不得而知。

下边就继续通过示例代码来看一下怎样在驱动层主动发送讯号。

驱动程序代码示例:发送讯号

功能需求

在昨天的简单示例中linux系统应用,可以得出下边这种信息:

讯号发送方:必须晓得向谁[PID]发送讯号,发送那个讯号;讯号接收方:必须定义讯号处理函数,而且向操作系统注册:接收什么讯号;

发送方其实就是驱动程序了,在示例代码中,继续使用SIGUSR1讯号来测试。

这么,驱动程序怎样能够晓得应用程序的PID呢?可以让应用程序通过oictl函数,把自己的PID主动告诉驱动程序:

linux 驱动通知应用层_驱动调用应用层函数_linux应用和驱动哪个更好

驱动程序

这儿的示例代码,是在上一篇文章的基础上更改的,改动部份的内容查看系统版本linux,使用宏定义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 
  • 1.
  • 2.
  • 3.
  • 4.

my_driver_signal.c文件的内容如下(不须要手敲,文末有代码下载链接):

复制

#include  
#include  
#include  
#include  
#include  
 
// 新增的头文件 
#include  
#include  
#include  
#include  
#include  
 
// 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_ioctl和send_signal。

(1)函数gpio_ioctl

当应用程序调用ioctl()的时侯,驱动程序中的gpio_ioctl都会被调用。

这儿定义一个简单的合同:当应用程序调用参数中cmd为100的时侯,就表示拿来告诉驱动程序自己的PID。

linux 驱动通知应用层_linux应用和驱动哪个更好_驱动调用应用层函数

驱动程序定义了一个全局变量g_pid,拿来保存应用程序传入的参数PID。

须要调用函数copy_from_user(&g_pid,pArg,sizeof(int)),把用户空间的参数复制到内核空间中;

成功取得PID以后,就调用函数send_signal向应用程序发送讯号。

这儿仅仅是用于演示目的,在实际的项目中,可能会按照接收到硬件触发以后再发送讯号。

(2)函数send_signal

这个函数主要做了3件事情:

构造一个讯号结构体变量:structsiginfoinfo;

通过应用程序传入的PID,获取任务信息:pid_task(find_vpid(g_pid),PIDTYPE_PID);

发送讯号:send_sig_info(sig_no,&info,my_task);

驱动模块Makefile

复制

$ touch Makefile 
  • 1.

内容如下:

复制

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 
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.

编译驱动模块

复制

$ make 
  • 1.

得到驱动程序:my_driver_signal.ko。

加载驱动模块

复制

$ sudo insmod my_driver_signal.ko 
  • 1.

通过dmesg指令来查看驱动模块的复印信息:

驱动调用应用层函数_linux 驱动通知应用层_linux应用和驱动哪个更好

由于示例代码是在上一篇GPIO的基础上更改的,因而创建的设备节点文件,与下篇文章是一样的:

linux 驱动通知应用层_驱动调用应用层函数_linux应用和驱动哪个更好

应用程序代码示例:接收讯号

注册讯号处理函数

应用程序一直置于~/tmp/App/目录下。

复制

$ mkdir ~/tmp/App/app_mysignal 
$ cd ~/tmp/App/app_mysignal 
$ touch mysignal.c 
  • 1.
  • 2.
  • 3.

文件内容如下:

复制

#include  
#include  
#include  
#include  
#include  
#include  
#include  
 
#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和SIGUSR2linux 驱动通知应用层,它俩的讯号处理函数是同一个:signal_handler()。

不仅sigaction函数,应用程序还可以使用signal函数来注册讯号处理函数;

(2)之后通过ioctl(fd,100,&pid);向驱动程序设置自己的PID。

编译应用程序:

复制

$ gcc mysignal.c -o mysignal 
  • 1.

执行应用程序:

复制

$ sudo ./mysignal 
  • 1.

按照刚刚驱动程序的代码,当驱动程序接收到设置PID的命令以后,会立即发送两个讯号:

linux 驱动通知应用层_驱动调用应用层函数_linux应用和驱动哪个更好

先来看一下dmesg中驱动程序的复印信息:

linux 驱动通知应用层_驱动调用应用层函数_linux应用和驱动哪个更好

可以看见:驱动把这两个讯号(10和12),发送给了应用程序(PID=6259)。

应用程序的输出信息如下:

linux应用和驱动哪个更好_驱动调用应用层函数_linux 驱动通知应用层

可以看见:应用程序接收到讯号10和12,而且正确复印出讯号中携带的一些信息!

Author

这篇优质的内容由TA贡献而来

刘遄

《Linux就该这么学》书籍作者,RHCA认证架构师,教育学(计算机专业硕士)。

发表回复