栏目分类:
子分类:
返回
名师互学网用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
名师互学网 > IT > 系统运维 > 运维 > Linux

【genius

Linux 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力

【genius

  • 大家好,我是峰哥,今天给大家解说一下:驱动层发送信号给应用程序。在上一篇文章中,我讲过:应用层发送指令来控制驱动层的GPIO状态,【genius_platform软件平台开发】第六十七讲:linux系统驱动开发之-GPIO设备驱动编写,控制的方向是从应用层到驱动层。
1. kill 命令和信号
  • 简单讲下kill命令和信号,是Linux 操作系统的信号,来“杀死”一个进程的命令:
$ kill -9 <进程的 PID>
  • 指令功能是:向指定的某个进程发送一个信号 9,这个信号的默认功能是:是停止进程。虽然在应用程序中没有主动处理这个信号,但是操作系统默认的处理动作是终止应用程序的执行。除了发送信号 9,kill 命令还可以发送其他的任意信号。在 Linux 系统中,所有的信号都使用一个整型数值来表示,可以打开文件 /usr/include/x86_64-linux-gnu/bits/signum.h(你的系统中可能位于其它的目录) 查看一下,比较常见的几个信号是:
#define	SIGINT		2	
#define	SIGKILL		9	
#define	SIGUSR1		10	
#define	SIGSEGV		11	
#define	SIGUSR2		12	
...
...
#define SIGSYS		31	
#define SIGUNUSED	31

#define	_NSIG		65	


#define __SIGRTMIN	32
#define __SIGRTMAX	(_NSIG - 1)

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

2. 实时信号和非实时信号 2.1 非实时信号
  • 操作系统不确保应用程序一定能接收到(即:信号可能会丢失);从文件 signum.h 中可以看到,实时信号从 __SIGRTMIN(数值:32) 开始。
2.2 实时信号
  • 操作系统确保应用程序一定能接收到;
3. 多线程中信号
  • 我们在编写应用程序时,虽然没有接收并处理 SIGKILL 这个信号,但是一旦别人发送了这个信号,我们的程序就被操作系统停止掉了,这是默认的动作。那么,在应用程序中,应该可以主动声明接收并处理指定的信号,下面就来写一个最简单的实例。

  • 在一个应用程序中存在多个线程时;当有一个信号发送给此进程时,所有的线程都可能接收到,但是只能有一个线程来处理;

4. 信号注册和处理函数
// 文件:app_handle_signal.c

#include 
#include 
#include 
#include 
#include 

// 信号处理函数
static void my_signal_handler(int signum, siginfo_t *info, void *context)
{
	// 打印接收到的信号值
    printf("my_signal_handler: signum=[%d] info=[%p] context=[%p]n", signum, info, context);
}

int main(void)
{
	int count = 0;
	// 注册信号处理函数
	struct sigaction sa;
	sigemptyset(&sa.sa_mask);
	sa.sa_sigaction = &my_signal_handler;
	sa.sa_flags = SA_SIGINFO;
	sigaction(SIGUSR1, &sa, NULL);
	sigaction(SIGUSR2, &sa, NULL);

	// 一直循环打印信息,等待接收发信号
	while (1)
	{
		printf("my_app_handle_signal is running...count = %d n", ++count);
		sleep(5);
	}

	return 0;
}
  • 这个示例程序接收的信号是 SIGUSR1 和 SIGUSR2,也就是数值 10 和 12。编译、执行:
$ gcc app_handle_signal.c -o app_handle_signal
$ ./app_handle_signal

  • 此时,应用程序开始执行,等待接收信号。在另一个终端中,使用kill指令来发送信号SIGUSR1或者 SIGUSR2。kill 发送信号,需要知道应用程序的 进程PID,可以通过指令: ps -au | grep kill_cmd_and_signal 来查看。执行发送信号SIGUSR1指令:
$ kill -10 34037
  • 此时,在应用程序的终端窗口中,就能看到下面的打印信息
  • 说明应用程序接收到了 SIGUSR1 这个信号!注意:我们是使用kill命令来发送信号的,kill 也是一个独立的进程,程序的执行路径如下:操作系统是如何接收kill的操作,然后如何发送信号给 kill_cmd_and_signal 进程的,我们不得而知
5. 驱动程序 5.1 功能需求
  • 在刚才的简单示例中,可以得出下面这些信息:
信号发送方:必须知道向谁[PID]发送信号,发送哪个信号;
信号接收方:必须定义信号处理函数,并且向操作系统注册:接收哪些信号;
  • 发送方当然就是驱动程序了,在示例代码中,继续使用 SIGUSR1 信号来测试。那么,驱动程序如何才能知道应用程序的PID呢?可以让应用程序通过oictl函数,把自己的PID主动告诉驱动程序:
5.2 驱动程序
$ cd linux-4.15/drivers/
$ mkdir my_driver_signal
$ cd my_driver_signal
$ touch my_driver_signal.c

my_driver_signal.c 文件的内容如下:

#include 
#include 
#include 
#include 
#include 

// 新增的头文件
#include 
#include 
#include 
#include 
#include 

// 新增部分,使用这个宏控制起来
#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 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;	
}
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;
}

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

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

	// 注销设备号
	unregister_chrdev_region(MKDEV(gpio_major, gpio_minor), MYGPIO_NUMBER);
}

MODULE_LICENSE("GPL");
module_init(gpio_driver_init);
module_exit(gpio_driver_exit);
5.2.1 gpio_ioctl函数
  • 当应用程序调用 ioctl() 的时候,驱动程序中的 gpio_ioctl 就会被调用。这里定义一个简单的协议:当应用程序调用参数中 cmd 为 100 的时候,就表示用来告诉驱动程序自己的 PID。

定义全局变量 g_pid保存应用程序的PID。函数 copy_from_user(&g_pid, pArg, sizeof(int)),把用户空间的参数复制到内核空间中;函数 send_signal 向应用程序发送信号。

5.2.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);
5.2.3 驱动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
5.2.4 编译驱动模块
$ make

得到驱动程序: my_driver_signal.ko 。

5.2.5 加载驱动模块
$ sudo insmod my_driver_signal.ko

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

因为示例代码是在上一篇GPIO的基础上修改的,因此创建的设备节点文件,与上篇文章是一样的:

6. 应用程序接收信号

应用程序放在 ~/tmp/App/ 目录下

$ mkdir ~/tmp/App/app_mysignal
$ cd ~/tmp/App/app_mysignal
$ touch my_signal_app.c
6.1 my_signal_app.c
#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 my_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 = &my_signal_handler;
	sa.sa_flags = SA_SIGINFO;
	
	// 接收处理SIGUSR1和SIGUSR2信号
	sigaction(SIGUSR1, &sa, NULL);
	sigaction(SIGUSR2, &sa, NULL);

	// 通过ioctl设置应用进程pid给驱动
	printf("call ioctl. pid = %d n", pid);
	ioctl(fd, 100, &pid);

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

	// 关闭设备
	close(fd);
}
  • 可以看到,应用程序主要做了两件事情:

  • (1)、首先通过函数 sigaction() 向操作系统注册了信号 SIGUSR1 和 SIGUSR2,它俩的信号处理函数是同一个:my_signal_handler()。除了 sigaction 函数,应用程序还可以使用 signal 函数来注册信号处理函数;

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

6.2 编译应用程序
$ gcc my_signal_app.c -o my_signal_app
6.3 执行应用程序:
$ sudo ./my_signal_app

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

可以看到:驱动把这两个信号(10 和 12),发送给了应用程序(PID=6259)。
应用程序的输出信息如下:
可以看到:应用程序接收到信号 10 和 12,并且正确打印出信号中携带的一些信息!

Linux进程管理内核API:https://www.coolcou.com/linux-kernel/linux-process-management-kernel-api/the-linux-kernel-pid-task.html

转载请注明:文章转载自 www.mshxw.com
本文地址:https://www.mshxw.com/it/883593.html
我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 (c)2021-2022 MSHXW.COM

ICP备案号:晋ICP备2021003244-6号