当前位置: 首页 > 科技观察

Linux驱动实战:中断处理函数如何向应用层[发送信号]?

时间:2023-03-15 23:46:07 科技观察

别人的经历就是我们的阶梯!大家好,我是刀哥。今天给大家讲解的技术知识点是:【中断程序如何向应用层发送信号】。最近分享的几篇文章比较基础,关于字符设备的驱动,中断处理等。也许这样的技术没有用在现代工程中,但摩天大楼是建在地上的。只有了解了这些最基础的知识点,再看看进化出来的高级玩意儿,才会一步步有获得感。如果少了这些基础环节,很多深层次的东西在学习的时候就会有点空中楼阁的感觉。这就像研究Linux内核一样。如果你从Linux4.x/5.x内核版本开始研究,你可以看到很多“遗留”代码。这些代码见证了Linux一步步发展的历史,甚至有人去研究Linux0.11版本的内核源代码,因为很多基本思想都是一样的。今天的文章主要以代码示例为主,结合前面两个知识点:在中断处理函数中,向应用层发送信号,通知应用层处理响应的中断服务。驱动示例代码概述所有操作都在~/tmp/linux-4.15/drivers目录下完成。首先创建驱动模块目录:$cd~/tmp/linux-4.15/drivers$mkdirmy_driver_interrupt_signal$touchmy_driver_interrupt_signal.c文件内容如下:#include#include#include#include#include#include#include#include#include#include#include//中断号#defineIRQ_NUM1//定义驱动的ID,使用tojudgeintheinterruptprocessingfunction是否需要处理#defineIRQ_DRIVER_ID1234//设备名称#defineMYDEV_NAME"mydev"//驱动程序数据结构structmyirq{intdevid;};structmyirqmydev={IRQ_DRIVER_ID};#defineKBD_DATA_REG0x60#defineKBD_STATUS_REG0x64#defineKBD_SCANCODE_MASK0x7f#defineKBD_STATUS_MASK0x80//设备类staticstructclass*my_class;//用来保存设备structcdevmy_cdev;//用来保存设备号intmydev_major=0;intmydev_minor=0;//用来保存给谁发送信号,应用设置它自己的进程ID通过ioctl。staticintg_pid=0;//用来给应用发送信号staticvoidsend_signal(intsig_no){intret;structsiginfoinfo;structtask_struct*my_task=NULL;if(0==g_pid){//表示应用没有设置自己的PIDprintk("pid[%d]isnotvalid!\n",g_pid);return;}printk("sendsignal%dtopid%d\n",sig_no,g_pid);//构造信号结构memset(&info,0,sizeof(structsiginfo));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("getpid_taskfailed!\n");return;}//发送信号ret=send_sig_info(sig_no,&info,my_task);if(ret<0){printk("sendsignalfailed!\n");}}//中断处理函数staticirqreturn_tmyirq_handler(intirq,void*dev){structmyirqmydev;unsignedcharkey_code;mydev=*(structmyirq*)dev;//检查设备id,只需要处理if(IRQ_DRIVER_ID==mydev.devid){//读取键盘扫描码key_code=inb(KBD_DATA_REG);if(key_code==0x01){printk("EXCkeyispressed!\n");send_signal(SIGUSR1);}}returnIRQ_HANDLED;}//驱动模块初始化函数staticvoidmyirq_init(void){printk("myirq_initiscalled.\n");//注册中断处理程序if(request_irq(IRQ_NUM,myirq_handler,IRQF_SHARED,MYDEV_NAME,&mydev)!=0){printk("registerirq[%d]handlerfailed.\n",IRQ_NUM);return-1;}printk("registerirq[%d]handlersuccess.\n",IRQ_NUM);}//应用打开设备时,调用staticintmydev_open(structinode*inode,structfile*file){printk("mydev_openiscalled.\n");return0;}staticlongmydev_ioctl(structfile*file,unsignedintcmd,unsignedlongarg){void__user*pArg;printk("mydev_ioctliscalled.cmd=%d\n",cmd);if(100==cmd){//描述应用设置processpArg=(void*)arg;if(!access_ok(VERIFY_READ,pArg,sizeof(int))){printk("accessfailed!\n");return-欧洲访问委员会;}//将数据从用户空间复制到内核空间owner=THIS_MODULE,.open=mydev_open,.unlocked_ioctl=mydev_ioctl};staticint__initmydev_driver_init(void){intdevno;dev_tnum_dev;printk("mydev_driver_initiscalled.\n");//注册中断处理程序if(request_irq(IRQ_NUM,myirq_handler,IRQF_SHARED,MYDEV_NAME,&mydev)!=0){printk("registerirq[%d]handlerfailed.\n",IRQ_NUM);return-1;}//动态申请设备号(严格的话要检查函数的返回值)alloc_chrdev_region(&num_dev,mydev_minor,1,MYDEV_NAME);//获取主设备号mydev_major=MAJOR(num_dev);printk("mydev_major=%d.\n",mydev_major);//创建设备类my_class=class_create(THIS_MODULE,MYDEV_NAME);//创建设备节点devno=MKDEV(mydev_major,mydev_minor);//初始化cdev结构cdev_init(&my_cdev,&mydev_ops);//注册字符设备cdev_add(&my_cdev,devno,1);//创建设备节点device_create(my_class,NULL,devno,NULL,MYDEV_NAME);return0;}staticvoid__exitmydev_driver_exit(void){printk("mydev_driver_exitiscalled.\n");//删除设备节点cdev_del(&my_cdev);device_destroy(my_class,MKDEV(mydev_major,mydev_minor));//释放设备类class_destroy(my_class);//注销设备号unregister_chrdev_region(MKDEV(mydev_major,mydev_minor),1);//注销中断处理程序free_irq(IRQ_NUM,&mydev);}MODULE_LICENSE("GPL");module_init(mydev_driver_init);module_exit(mydev_driver_exit);上面的代码主要做了两件事:注册1号中断的处理函数:myirq_handler();创建设备节点/dev/mydev;这里的中断号1是键盘中断,因为是共享中断,所以当键盘按下时,操作系统会依次调用所有的中断处理函数,包括我们驱动注册的那个。功能。中断处理部分相关的几个关键代码如下://中断处理函数staticirqreturn_tmyirq_handler(intirq,void*dev){...}//驱动模块初始化函数staticvoidmyirq_init(void){...request_irq(IRQ_NUM,myirq_handler,IRQF_SHARED,MYDEV_NAME,&mydev);...}在中断处理程序中,目标是将信号SIGUSR1发送到应用层,因此驱动程序需要知道应用程序的进程ID(PID)。根据上一篇Linux驱动实践:驱动如何向应用发送[信号]?,应用必须主动告诉驱动模块自己的PID。这可以通过write或ioctl函数来实现。驱动接收PID使用的相关代码为:staticlongmydev_ioctl(structfile*file,unsignedintcmd,unsignedlongarg){...if(100==cmd){pArg=(void*)arg;...copy_from_user(&g_pid,pArg,sizeof(int));}}知道应用程序的PID,驱动程序可以在中断发生时发送信号(按键盘上的ESC键):staticvoidsend_signal(intsig_no){structsiginfo;...send_sig_info(...);}staticirqreturn_tmyirq_handler(intirq,void*dev){...send_signal(SIGUSR1);}Makefileifneq($(KERNELRELEASE),)obj-m:=my_driver_interrupt_signal。oelseKERNELDIR?=/lib/modules/$(shelluname-r)/buildPWD:=$(shellpwd)default:$(MAKE)-C$(KERNELDIR)M=$(PWD)modulesclean:rm-rf*.o*。ko*.mod.*modules.*Module.*$(MAKE)-C$(KERNEL_PATH)M=$(PWD)cleanendif编译测试首先在加载驱动模块前检查所有被1号打断的驱动:再看一遍看一下设备号:$cat/proc/devices因为驱动注册是在创建设备节点时由系统动态请求的。根据前面的文章我们可以知道,系统一般会给我们分配主设备号244,这个主设备号目前还不存在。编译加载驱动模块:$make$sudoinsmodmy_driver_interrupt_signal.ko首先看dmesg的输出信息:再看中断驱动:可以看到我们的驱动(mydev)已经注册在1号中断最右边.最后查看设备节点的状态:驱动模块已经准备好,下面是应用程序。应用程序的主要功能有两部分:通过ioctl函数告诉驱动程序自己的PID;注册信号SIGUSR1的处理函数;示例代码全貌>#includechar*dev_name="/dev/mydev";//信号处理函数staticvoidsignal_handler(intsignum,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);}intmain(intargc,char*argv[]){intfd,count=0;intpid=getpid();//打开GPIOif((fd=open(dev_name,O_RDWR|O_NDELAY))<0){printf("opendevfailed!\n");return-1;}printf("opendevsuccess!\n");//注册信号处理函数structsigactionsa;sigemptyset(&sa.sa_mask);sa.sa_sigaction=&signal_handler;sa.sa_flags=SA_SIGINFO;sigaction(SIGUSR1,&sa,NULL);//setPIDprintf("callioctl.pid=%d\n",pid);ioctl(fd,100,&pid);//无限循环,等待信号while(1)sleep(1);//关闭设备close(fd);}在应用程序结束时,是一个while(1)死循环,因为驱动程序只有在按下键盘上的ESC键时才会发送信号,所以应用程序需要一直处于活动状态。编译测试打开一个新的中断窗口,编译并执行应用程序:$gccmy_interrupt_singal.c-omy_interrupt_singal$sudo./my_interrupt_singalopendevsuccess!callioctl.pid=12907//这里进入while循环是因为应用程序调用了open和ioctl这两个函数,因此,驱动程序中的两个相应函数将被执行。从dmesg命令的输出可以看出:此时按下键盘上的ESC键,驱动会打印如下信息:说明:驱动已经捕获到键盘上的ESC键,并发送信号给应用程序。在应用程序执行的终端窗口中,可以看到如下输出信息:说明:应用程序收到了来自驱动程序的信号!