Linux driver practice: how does the interrupt processing function [send a signal] to the application layer?

Posted by miasma on Mon, 10 Jan 2022 13:57:48 +0100

Author: Daoge, a 10 + year embedded development veteran, focusing on: C/C + +, embedded and Linux.

Pay attention to the official account below, reply to books, get classic books in Linux and embedded field. Reply to [PDF] to obtain all original articles (PDF format).

catalogue

Other people's experience, our ladder!

Hello, I'm brother Dao. The technical knowledge I'll explain to you today is: [how an interrupt program sends a signal to the application layer].

Several articles shared recently are more basic, about character device drivers and interrupt handlers.

Perhaps such technology is not used in modern projects, but ten thousand tall buildings rise from the ground.

Only after understanding these most basic knowledge points, and then looking at the advanced gadgets evolved, can we have a sense of gain step by step.

Without these basic links, many deep-seated things will feel like castles in the air.

It's like studying the Linux kernel. If you start from Linux 4 x/5. After studying the X kernel version, you can see a lot of "historical" code.

These codes witness the development history of Linux step by step. Some people even study the kernel source code of Linux version 0.11, because many basic ideas are the same.

Today's article mainly focuses on code examples and combines the previous two knowledge points:

In the interrupt processing function, a signal is sent to the application layer to notify the application layer to process the interrupt service in response.

Driver

Sample code overview

All operations are completed in ~ / tmp/linux-4.15/drivers directory.

First create the driver module Directory:

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

The contents of the document are as follows:

#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>

// interrupt number
#define IRQ_NUM			1

// Defines the ID of the driver, which is used in the interrupt processing function to determine whether it needs to be processed	
#define IRQ_DRIVER_ID	1234

// Equipment name
#define MYDEV_NAME		"mydev"

// Driver data structure
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

// Equipment class
static struct class *my_class;

// Used to save the device
struct cdev my_cdev;

// Used to save the equipment number
int mydev_major = 0;
int mydev_minor = 0;

// It is used to save who to send signals to. The application sets its own process ID through ioctl.
static int g_pid = 0;

// Used to send signals to applications
static void send_signal(int sig_no)
{
	int ret;
	struct siginfo info;
	struct task_struct *my_task = NULL;
	if (0 == g_pid)
	{
		// This indicates that the application has not set its own PID
	    printk("pid[%d] is not valid! \n", g_pid);
	    return;
	}

	printk("send signal %d to pid %d \n", sig_no, g_pid);

	// Construct signal structure
	memset(&info, 0, sizeof(struct siginfo));
	info.si_signo = sig_no;
	info.si_errno = 100;
	info.si_code = 200;

	// Obtain your own task information and use the RCU lock
	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;
	}

	// Send signal
	ret = send_sig_info(sig_no, &info, my_task);
	if (ret < 0) 
	{
	       printk("send signal failed! \n");
	}
}

//Interrupt handling function
static irqreturn_t myirq_handler(int irq, void * dev)
{
    struct myirq mydev;
    unsigned char key_code;
    mydev = *(struct myirq*)dev;	
	
	// Check the device id and process it only when it is equal
	if (IRQ_DRIVER_ID == mydev.devid)
	{
		// Read keyboard scan code
		key_code = inb(KBD_DATA_REG);
	
		if (key_code == 0x01)
		{
			printk("EXC key is pressed! \n");
			send_signal(SIGUSR1);
		}
	}	

	return IRQ_HANDLED;
}

// Driver module initialization function
static void myirq_init(void)
{
    printk("myirq_init is called. \n");

	// Register interrupt handler
    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);
}

// Called when the application opens the device
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)
	{
		// Describes the PID of the application setup process 
		pArg = (void *)arg;
		if (!access_ok(VERIFY_READ, pArg, sizeof(int)))
		{
		    printk("access failed! \n");
		    return -EACCES;
		}

		// Copy user space data to kernel space
		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");

	// Register interrupt handler
    if(request_irq(IRQ_NUM, myirq_handler, IRQF_SHARED, MYDEV_NAME, &mydev)!=0)
    {
        printk("register irq[%d] handler failed. \n", IRQ_NUM);
        return -1;
    }

	// Dynamically apply for the equipment number (if it is more rigorous, the return value of the function should be checked)
	alloc_chrdev_region(&num_dev, mydev_minor, 1, MYDEV_NAME);

	// Get master device number
	mydev_major = MAJOR(num_dev);
	printk("mydev_major = %d. \n", mydev_major);

	// Create device class
	my_class = class_create(THIS_MODULE, MYDEV_NAME);

	// Create device node
	devno = MKDEV(mydev_major, mydev_minor);
	
	// Initialize cdev structure
	cdev_init(&my_cdev, &mydev_ops);

	// Register character device
	cdev_add(&my_cdev, devno, 1);

	// Create device node
	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");

	// Delete device node
	cdev_del(&my_cdev);
	device_destroy(my_class, MKDEV(mydev_major, mydev_minor));

	// Release device class
	class_destroy(my_class);

	// Logout equipment number
	unregister_chrdev_region(MKDEV(mydev_major, mydev_minor), 1);

	// Logoff interrupt handler
	free_irq(IRQ_NUM, &mydev);
}

MODULE_LICENSE("GPL");
module_init(mydev_driver_init);
module_exit(mydev_driver_exit);

The above code mainly does two things:

  1. Handler for registering interrupt number 1: myirq_handler();

  2. Create device node / dev/mydev;

The interrupt number 1 here is the keyboard interrupt.

Because it is a shared interrupt, when the keyboard is pressed, the operating system will call all interrupt handling functions in turn, including the function registered by our driver.

Several key codes related to interrupt processing are as follows:

//Interrupt handling function
static irqreturn_t myirq_handler(int irq, void * dev)
{
    ...
}

// Driver module initialization function
static void myirq_init(void)
{
    ...
    request_irq(IRQ_NUM, myirq_handler, IRQF_SHARED, MYDEV_NAME, &mydev);
    ...
}

In the interrupt processing function, the goal is to send the signal SIGUSR1 to the application layer, so the driver needs to know the process number (PID) of the application.

According to the previous article Linux driver practice: how does the driver send [signal] to the application? , the application must actively tell its PID to the driver module. This can be achieved through the write or ioctl functions,

The relevant codes used by the driver to receive PID are:

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));
    }
}

Knowing the PID of the application, the driver can send a signal when an interrupt occurs (press the ESC key on the keyboard):

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 file

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

Compilation and testing

First, check all drivers of interrupt 1 before loading the driver module:

Take another look at the equipment number:

$ cat /proc/devices

Because the driver registration dynamically requests the system to allocate when creating the device node.

According to the previous articles, the system will generally assign 244 to us, which does not exist at the moment.

Compile and load driver module:

$ make
$ sudo insmod my_driver_interrupt_signal.ko

First, take a look at the output information of dmesg:

Then look at the interrupt driver:

You can see that our driver (mydev) has been registered on the far right of interrupt 1.

Finally, let's take a look at the equipment nodes:

The driver module is ready. Here is the application.

application program

The main functions of the application are two parts:

  1. Tell your PID to the driver through ioctl function;

  2. The processing function of the registration signal SIGUSR1;

Sample code overview

#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";

// Signal processing function
static void signal_handler(int signum, siginfo_t *info, void *context)
{
	// Print the received signal value
    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();

	// Open GPIO
	if((fd = open(dev_name, O_RDWR | O_NDELAY)) < 0){
		printf("open dev failed! \n");
		return -1;
	}

	printf("open dev success! \n");
	
	// Register signal processing function
	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);

	// Dead cycle, waiting for signal to be received
	while (1)
		sleep(1);

	// Turn off the device
	close(fd);
}

At the end of the application, there is a while(1) loop. Because the driver will send a signal only when the ESC key on the keyboard is pressed, the application needs to be alive all the time.

Compilation and testing

Open a new interrupt window to compile and execute the application:

$ gcc my_interrupt_singal.c -o my_interrupt_singal
$ sudo ./my_interrupt_singal
open dev success! 
call ioctl. pid = 12907

// Here we enter the while loop

Since the application calls the open and ioctl functions, the two corresponding functions in the driver will be executed.

This can be seen from the output information of the dmesg command:

At this time, press the ESC key on the keyboard, and the following information will be printed in the driver:

Note: the driver captures the ESC key on the keyboard and sends a signal to the application.

In the terminal window executing the application, you can see the following output information:

Description: the application received the signal from the driver!


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

The test code and related documents in this article have been put on the network disk.

In the official account [IOT Internet of things] background reply keyword: 1220, you can get download address.

thank you!

Recommended reading

[1] Linux ab initio series

[2] C language pointer - from the underlying principle to fancy skills, use graphics and code to help you explain thoroughly

[3] The underlying debugging principle of gdb is so simple

[4] Is inline assembly terrible? Finish reading this article and end it!

Other albums: Selected articles,Application design,Internet of things, C language.

The official account of star standard is the first time to read articles.

Topics: Linux Linux Driver