程序项目代做,有需求私信(vue、React、Java、爬虫、电路板设计、嵌入式linux等)

linux驱动移植-信号驱动IO模型

在上一节中我们介绍了linux下的五种IO模型:

  • 阻塞IO;
  • 非阻塞IO;
  • IO多路复用模型;
  • 信号驱动IO模型;
  • 异步IO;

并介绍了poll机制在按键驱动中的使用,这一节我们将重点介绍信号驱动IO模型如何在按键驱动的例子中的使用。

一、信号驱动IO模型

1.1 什么是信号驱动IO模型

我们举个例子,我们在钓鱼的时候需要不停的查看鱼竿,看看有没有鱼咬鱼浮了,这导致我们不能去做其他的事情,显然这是一个阻塞IO模型。

如果我们给鱼竿安装一个报警器,当鱼浮下沉的时候,也就说明有鱼儿咬钩了,就立刻报警。我们收到报警信号之后,去把鱼钓上来。映射到linux操作系统中,这就是信号驱动IO模型。

其实这个就有点类似事件监听模型。应用程序预先在内核中注册一个信号处理函数,本质就是一个回调函数,当IO准备好之后(读:有数据可读;写:有空间可写),内核就会给进程发送一个预先预定号的信号,进程在收到信号后,就调用信号对应的处理函数。

1.2 什么是信号

信号是linux系统响应某些条件而产生的一个事件,它可以用于进程之间的通信。

信号是在软件层次上对中断机制的一种模拟,在原理上,一个信号接收到一个信号与处理器接收到一个中断请求可以说是一样的。

信号是异步的,一个进程不必通过任何操作来等待信号的到达,事实上,进程也不知道信号到底什么时候到达。那么,进程是如何发现和接受信号呢?

实际上,信号的接收不是由用户进程来完成的,而是由内核代理。当一个进程P2向另一个进程P1发送信号后,内核接受到信号,并将其放在P1的信号队列当中。当P1再次进入内核态时,会检查信号队列,并根据相应的信号调取相应的信号处理函数。

信号是进程间通信机制中唯一的异步通信机制,可以看做是异步通知,通知接收信号的进程有哪些事情发生了。信号机制经过POSIX实时扩展后,功能更加强大,除了基本通知功能外,还可以传递附加信息。

1.3 信号来源

信号事件的发生有两个来源:

  • 硬件来源(比如我们按下了键盘或者其它硬件故障);
  • 软件来源,最常用发送信号的系统函数是kill, raise, alarm和setitimer以及sigqueue函数,软件来源还包括一些非法运算等操作;

1.4 linux产生信号的条件

(1) 当用户按下某些终端键时,将产生信号

  • 终端上按“CTRL+C”组合键通常产生中断信号 SIGINT;
  • 终端上按“CTRL+\”键通常产生中断信号 SIGQUIT;
  • 终端上按“Ctrl+Z”键通常产生中断信号 SIGSTOP 等;

(2) 硬件异常将产生信号

比如数据运算时,除数为0;或者无效的内存访问等;这些条件通常由硬件检测到,并通知内核,然后内核为该条件发生时正在运行的进程产生适当的信号;

(3) 软件异常将产生信号

当检测到某种软件条件已发生,并将其通知有关进程时,产生信号;

(4) 调用 kill函数将发送信号。

注意:接收信号进程和发送信号进程的所有者必须相同,或发送信号进程的所有者必须是超级用户;

(5) 运行shell  kill 命令将发送信号

此程序实际上是使用 kill 函数来发送信号。也常用此命令终止一个失控的后台进程;

1.5 信号的捕获和处理

刚才我们说,当进程P1再次进入内核时,会检查信号队列。那么,进程P1什么时候会再次进入内核呢?进入内核后在什么时机会检测信号队列呢?

  • 当前进程由于系统调用、中断或异常而进入内核空间以后,从内核空间返回到用户空间的前夕;
  • 当前进程在内核中进入睡眠以后刚被唤醒的时候(必定是在系统调用中),或者由于不可忽略信号的存在而提前返回到用户空间;

发现信号后,根据信号向量,就找到了信号处理函数,然后从内核态跑到用户态去执行信号处理程序,那么用户进程一般如何处理信号?用户进程可按照下列3中方式来处理:

  • 忽略信号:即对信号不做任何处理,大多数信号都可以使用这种方式处理,但信号SIGKILL和SIGSTOP绝不能被忽略,因为它们向超级用户提供了一种使进程终止的可靠方法;
  • 缺省动作:执行信号的默认动作,大多数信号的系统默认动作是终止进程.;
  • 捕捉信号:定义信号处理函数,当信号发生时,执行相应的处理函数;

Linux究竟采用上述三种方式的哪一个来响应信号,取决于传递给相应API函数的参数。

二、linux中的信号

2.1 系统支持信号

那么我们就需要知道在linux系统中有哪些可用的信号,在linux终端输入kill -l可以查看系统所支持的信号,可以看出,每个信号的名字都是以SIG开头:

root@zhengyang:/work/sambashare/linux-5.2.8# kill -l
 1) SIGHUP       2) SIGINT       3) SIGQUIT      4) SIGILL       5) SIGTRAP
 6) SIGABRT      7) SIGBUS       8) SIGFPE       9) SIGKILL     10) SIGUSR1
11) SIGSEGV     12) SIGUSR2     13) SIGPIPE     14) SIGALRM     15) SIGTERM
16) SIGSTKFLT   17) SIGCHLD     18) SIGCONT     19) SIGSTOP     20) SIGTSTP
21) SIGTTIN     22) SIGTTOU     23) SIGURG      24) SIGXCPU     25) SIGXFSZ
26) SIGVTALRM   27) SIGPROF     28) SIGWINCH    29) SIGIO       30) SIGPWR
31) SIGSYS      34) SIGRTMIN    35) SIGRTMIN+1  36) SIGRTMIN+2  37) SIGRTMIN+3
38) SIGRTMIN+4  39) SIGRTMIN+5  40) SIGRTMIN+6  41) SIGRTMIN+7  42) SIGRTMIN+8
43) SIGRTMIN+9  44) SIGRTMIN+10 45) SIGRTMIN+11 46) SIGRTMIN+12 47) SIGRTMIN+13
48) SIGRTMIN+14 49) SIGRTMIN+15 50) SIGRTMAX-14 51) SIGRTMAX-13 52) SIGRTMAX-12
53) SIGRTMAX-11 54) SIGRTMAX-10 55) SIGRTMAX-9  56) SIGRTMAX-8  57) SIGRTMAX-7
58) SIGRTMAX-6  59) SIGRTMAX-5  60) SIGRTMAX-4  61) SIGRTMAX-3  62) SIGRTMAX-2
63) SIGRTMAX-1  64) SIGRTMAX

 在头文件/usr/include/bits/signum.h中,这些信号都被定义为正整数,即每个信号和一个数字编码相对应:

/* Signals.  */
#define SIGHUP          1       /* Hangup (POSIX).  */
#define SIGINT          2       /* Interrupt (ANSI).  */
#define SIGQUIT         3       /* Quit (POSIX).  */
#define SIGILL          4       /* Illegal instruction (ANSI).  */
#define SIGTRAP         5       /* Trace trap (POSIX).  */
#define SIGABRT         6       /* Abort (ANSI).  */
#define SIGIOT          6       /* IOT trap (4.2 BSD).  */
#define SIGBUS          7       /* BUS error (4.2 BSD).  */
#define SIGFPE          8       /* Floating-point exception (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 SIGPIPE         13      /* Broken pipe (POSIX).  */
#define SIGALRM         14      /* Alarm clock (POSIX).  */
#define SIGTERM         15      /* Termination (ANSI).  */
#define SIGSTKFLT       16      /* Stack fault.  */
#define SIGCLD          SIGCHLD /* Same as SIGCHLD (System V).  */
#define SIGCHLD         17      /* Child status has changed (POSIX).  */
#define SIGCONT         18      /* Continue (POSIX).  */
#define SIGSTOP         19      /* Stop, unblockable (POSIX).  */
#define SIGTSTP         20      /* Keyboard stop (POSIX).  */
#define SIGTTIN         21      /* Background read from tty (POSIX).  */
#define SIGTTOU         22      /* Background write to tty (POSIX).  */
#define SIGURG          23      /* Urgent condition on socket (4.2 BSD).  */
#define SIGXCPU         24      /* CPU limit exceeded (4.2 BSD).  */
#define SIGXFSZ         25      /* File size limit exceeded (4.2 BSD).  */
#define SIGVTALRM       26      /* Virtual alarm clock (4.2 BSD).  */
#define SIGPROF         27      /* Profiling alarm clock (4.2 BSD).  */
#define SIGWINCH        28      /* Window size change (4.3 BSD, Sun).  */
#define SIGPOLL         SIGIO   /* Pollable event occurred (System V).  */
#define SIGIO           29      /* IO now possible (4.2 BSD).  */
#define SIGPWR          30      /* Power failure restart (System V).  */
#define SIGSYS          31      /* Bad system call.  */

下面给出一些信号的描述信息:

信号名称说明
 SIGHUP  1  连接挂断
SIGINT 2 终端中断
SIGQUIT 3 终端退出
SIGILL 非法指令
SIGABRT 5 跟踪陷阱
SIGIOT 6 IOT陷阱
SIGBUS 7 BUS错误
SIGFPE 8 浮点运算异常
SIGKILL 9 终止进程(不能被捕获或忽略)
SIGUSR1 10 用户定义信号1
SIGSEGV 11 无效内存段访问
SIGUSR2 12 用户定义信号2
SIGPIPE 13 向无读进程的管道写数据
SIGALRM 14 超时警告
SIGTERM 15 终止
SIGSTKFLT 16 堆栈错误
SIGCHLD 17 子进程已经停止或退出
SIGCONT 18 如果停止了,继续执行
SIGSTOP 19 停止执行(不能被捕获或忽略)
SIGTSTP 20 终端停止信号
SIGTTIN 21 后台进程需要从终端读取命令
SIGTTOU 22 后台进程需要从终端写出
SIGURG 23 紧急的套接字事件
SIGXCPU 24 超额使用CPU分配的时间
SIGXFSZ 25 文件尺寸超额
SIGVTALRM 26 虚拟时钟信号
SIGPROF 27 时钟信号描述
SIGWINCH 28 窗口尺寸变化
SIGIO 29 IO
SIGPWR 30 断电重启
SIGSYS 31 系统调用错误

除了SIGSTOP和SIGKILL两个信号外,进程能够忽略或捕获其他的全部信号。一个信号被捕获的意思是当一个信号到达时有相应的代码处理它,如果一个信号没有被这个进程所捕获,内核将采用默认行为处理。

2.2 信号分类

linux信号又可以按照可靠性以及实时性两个维度进行分类。

按照可靠性分类:

  • 可靠信号:支持信号队列;
  • 不可靠信息:不支持信号队列;

在早期的UNIX中信号是不可靠的,不可靠在这里指的是:信号可能丢失,一个信号发生了,但进程却可能一直不知道这一点。

现在Linux 在SIGRTMIN信号之前的都叫不可靠信号,这里的不可靠主要是不支持信号队列,就是当多个信号发生在进程中的时候(收到信号的速度超过进程处理的速度的时候),这些没来的及处理的信号就会被丢掉,仅仅留下一个信号。

可靠信号是多个信号发送到进程的时候(收到信号的速度超过进程处理信号的速度的时候),这些没来的及处理的信号就会排入进程的信号队列。等进程有机会来处理的时候,依次再处理,信号不丢失。

按照实时性分类:

  • 实时信号:实时信号用户进程一定能接收到;
  • 非实时信号:非实时信号用户进程不一定能接收到,信号可能丢失;

非实时信号都不支持信号队列,都是不可靠信号;实时信号都支持信号队列,都是可靠信号。

三、信号的安装

在用户进程中,如果想要处理某一种信号,就需要在进程中安装该信号。安装信号主要用来确定信号值及进程针对该信号值的动作之间的映射关系,即进程将要处理哪个信号;该信号被传递给进程时,将执行何种操作。

linux中有两个函数实现信号的安装:

  • signal:它只有两个参数,不支持信号传递消息,主要用于SIGRTMIN之前的非实时信号的安装;

  • sigaction:sigaction是较新的函数(由两个系统调用实现:sys_signal以及sys_rt_sigaction),有三个参数,支持信号传递信息,主要用来与 sigqueue系统调用配合使用,当然,sigqueue同样支持非实时信号的安装。sigaction优于signal主要体现在支持信号带有参数;

3.1 signal函数

在用户进程中,可以使用signal函数来设置对应信号的处理函数:

typedef void (*__sighandler_t) (int);
__sighandler_t signal (int __sig, __sighandler_t __handler);

首先定义一个函数指针sighandler_t,参数类型为int,没有返回值;

然后定义signal函数,可以看到该函数:

  • __sig:第一个参数指定信号的值,int类型,SIGRTMIN之前的信号,并且排除SIGKILL及SIGSTOP;
  • __handler:第二个参数指定信号值对应的处理函数,__sighandler_t类型,函数的参数为信号值;如果参数设置为SIG_IGN 忽略参数__sig指定的信号,参数设置为SIG_DFL将参数__sig指定的信号重设为系统默认的信号处理方式;

比如SIG_DEF定义为,即指向地址0:

#define SIG_DFL ((__sighandler_t) 0)

函数返回一个函数指针,类型为sighandler_t。如果signal调用成功,返回上一次为安装信号__sig而调用signal时指定的__handler值;失败则返回SIG_ERR。

然后演示如何使用,我们编写一个示例程序,在程序中,我们捕获SIGINT信号,然后输出"catch the signal SIGINT 信号值":

#include <stdio.h>
#include <unistd.h>
#include <signal.h>
#include <stdlib.h>

void sig_handler(int signo);
int main(void)
{
    printf("mian is waiting for a signal\n");
    if(signal(SIGINT,sig_handler) == SIG_ERR){
        perror("signal errror");
        exit(EXIT_FAILURE);
    }
    for(; ;);//有时间让我们发送信号


    return 0;
}

void sig_handler(int signo)
{
    printf("catch the signal SIGINT %d\n",signo);
}

使用gcc编译,然后在ubuntu系统运行:

root@zhengyang:/work/sambashare# ./hello
mian is waiting for a signal
^Ccatch the signal SIGINT 2
^Ccatch the signal SIGINT 2
^Ccatch the signal SIGINT 2
^Ccatch the signal SIGINT 2
^Ccatch the signal SIGINT 2
已杀死

每当我们按下CTRL+C就会发送SIGINT信号,从而执行我们安装的信号处理函数,而当我们重新打开一个终端执行kill  -9 进程号时,发送信号值为9的信号SIGKILL,执行系统默认处理函数,退出进程。

3.2 sigaction函数

int sigaction (int __sig, const struct sigaction * __act,
                     struct sigaction * __oact) 

其中:

  • __sig:第一个参数指定信号的值,int类型,可以为除SIGKILL及SIGSTOP外的任何一个特定有效的信号(为这两个信号定义自己的处理函数,将导致信号安装错误);
  • __act:第二个参数是指向结构sigaction的一个实例的指针,在结构sigaction的实例中,指定了对特定信号的处理,可以为NULL,进程会以缺省方式对信号处理;
  • _oact:第三个参数_oact指向的对象用来保存原来对相应信号的处理,可指定_oact为NULL。

如果把第二、第三个参数都设为NULL,那么该函数可用于检查信号的有效性;

第二个参数最为重要,其中包含了对指定信号的处理、信号所传递的信息、信号处理函数执行过程中应屏蔽掉哪些函数等等。sigaction结构定义如下:

struct sigaction {
   union{
        void     (*sa_handler)(int);//信号处理程序 不接受额外数据
        void     (*sa_sigaction)(int, siginfo_t *, void *);//信号处理程序,能接受额外数据,可以和sigqueue配合使用
    }_u;
    sigset_t   sa_mask;
    int        sa_flags;//影响信号的行为SA_SIGINFO表示能接受数据
    void     (*sa_restorer)(void);//废弃
};

其中:

  • sa_restorer,已过时,POSIX不支持它,不应再被使用。
  • _sa_handler指定信号对应的处理函数,和signal函数第二个参数一样,除了可以是用户自定义的处理函数外,还可以为SIG_DFL(采用缺省的处理方式),也可以为SIG_IGN(忽略信号)。
  • _sa_handler指定的处理函数只有一个参数,即信号值,所以信号不能传递除信号值之外的任何信息;
  • _sa_sigaction指定的信号处理函数带有三个参数,是为实时信号而设的(当然同样支持非实时信号),它指定一个3参数信号处理函数。第一个参数为信号值,第三个参数没有使用(posix没有规范使用该参数的标准),第二个参数是指向siginfo_t结构的指针,结构中包含信号携带的数据值。

示例hello.c:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/ioctl.h>
#include <signal.h>

// 信号处理函数
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;
}

这个示例程序接收的信号是SIGUSR1和SIGUSR2,也就是数值10和12。在ubuntu编译、执行:

gcc -o hello hello.c
./hello

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

root@zhengyang:~# ./hello
app_handle_signal is running...count = 1
app_handle_signal is running...count = 2
app_handle_signal is running...count = 3
app_handle_signal is running...count = 4
app_handle_signal is running...count = 5
app_handle_signal is running...count = 6
app_handle_signal is running...count = 7
app_handle_signal is running...count = 8

在另一个终端中,使用kill指令来发送信号SIGUSR1或者SIGUSR2:

root@zhengyang:~# ps -ef | grep hello
root     100520  66187  0 14:29 pts/2    00:00:00 ./hello
root     101076 100954  0 14:30 pts/7    00:00:00 grep --color=auto hello
root@zhengyang:~# kill -10 100520
root@zhengyang:~# kill -10 100520
root@zhengyang:~# kill -10 100520

此时在应用程序的终端窗口,可以看到:

实际上kill也是一个进程,上面说明应用程序hello接收到了kill进程发送的SIGUSR1这个信号。

更多相关内容参考linux系统编程之信号(六):信号发送函数sigqueue和信号安装函数sigactionLinux 改进捕捉信号机制(sigaction,sigqueue)

四、进程间信号的发送

发送信号的主要函数有:kill、raise、sigqueue、alarm、setitimer、pause以及abort。

在第三节中我们已经介绍了如何给一个用户进程安装信号以及信号对应的处理函数,在这一节我们将介绍进程间信号如何发送,包括:

  • 进程P2向进程P1发送信号;
  • 进程给自己发送信号;

4.1 kill

kill可以向目标进程发送信号。在ubuntu下运行man 2 kill可以查看帮助信息:

可以看到通过kill函数我们可以向进程发送信号,该函数原型:

#include <sys/types.h> 
#include <signal.h> 
int kill(pid_t pid,int signo)

该函数有两个参数:

  • pid:指定发送信号的接收进程id;
  • signo:信号值;当为0时(即空信号),实际不发送任何信号,但照常进行错误检查,因此,可用于检查目标进程是否存在,以及当前进程是否具有向目标发送信号的权限(root权限的进程可以向任何进程发送信号,非root权限的进程只能向属于同一个session或者同一个用户的进程发送信号);

其中参数pid的值:

参数pid的值信号的接收进程
pid>0 进程ID为pid的进程
pid=0 同一个进程组的进程
pid<0 pid!=-1 进程组ID为 -pid的所有进程
pid=-1 除发送进程自身外,所有进程ID大于1的进程

函数如果成功(至少发送了一个信号)返回0,出现错误返回-1,然后设置errno:

  • EINTVAL:指定了无效的信号;
  • EPERM:进程没有向任何一个目标进程发送信号的权限;
  • ESRCH:pid或者进程组不存在;

4.2 raise

raise用于向进程本身发送信号,在ubuntu下运行man 3 raise可以查看帮助信息:

函数原型如下:

#include <signal.h> 
int raise(int signo) 

该函数有一个参数:

  • signo:信号值;

在2.3.3之后的glibc库中,函数raise是由系统调用tgkill函数实现;而在老板本的glibc中,函数raise是有系统调用kill函数实现;

我们下面的程序,进程通过raise向自身发送了一个SIGINT信号:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>
 
int main(void) 
{
    int i = 0; 
    while(1) { 
        i++;  
        if(i == 3) {
           printf("I will raise SIGINT to myself...\n"); 
           raise(SIGINT); 
        } 
           printf("I am running now...\n"); 
           sleep(1); 
    } 
    return 0; 
}     

4.3 sigqueue

sigqueue函数用于信号发送。在ubuntu下运行man 2 sigqueue可以查看帮助信息:

函数原型:

#include <sys/types.h> 
#include <signal.h> 
int sigqueue(pid_t pid, int sig, const union sigval value)

sigqueue是比较新的发送信号系统调用,主要是针对实时信号提出的,支持信号带有参数,与函数sigaction配合使用。其中参数如下:

  • pid:第一个参数是指定接收信号的进程ID;
  • sig:第二个参数确定即将发送的信号;
  • value:第三个参数是一个联合数据结构union sigval,指定了信号传递的参数,即通常所说的4字节值;
typedef union sigval {
        int  sival_int;
        void *sival_ptr;
}sigval_t;

函数如果返回0,表示信号已经成功追加到进程的信号队列,出现错误返回-1,然后设置errno:

  • EAGAIN:进程信号队列已满;
  • EINVAL:无效的信号值;
  • EPERM:进程没有向目标进程发送信号的权限;
  • ESRCH:pid不存在;

sigqueue比kill传递了更多的附加信息,但sigqueue只能向一个进程发送信号,而不能发送信号给一个进程组。

如果sig=0,将会执行错误检查,但实际上不发送任何信号,0值信号可用于检查pid的有效性以及当前进程是否有权限向目标进程发送信号。

在调用sigqueue时,sigval_t指定的信息会拷贝到参数信号处理函数(参数信号处理函数指的是信号处理函数由sigaction安装,并设定了sa_sigaction指针)的siginfo_t结构中,这样信号处理函数就可以处理这些信息了。

注意:

  • sigqueue发送非实时信号时,第三个参数包含的信息仍然能够传递给信号处理函数;
  • sigqueue发送非实时信号时,仍然不支持排队,即在信号处理函数执行过程中到来的所有相同信号,都被合并为一个信号;

4.4 alarm

alarm函数用于设置SIGALRM信号传送闹铃。在ubuntu下运行man 2 alarm可以查看帮助信息:

函数原型如下:

#include <unistd.h> 
unsigned int alarm(unsigned int seconds)

专门为SIGALRM信号而设,在指定的时间seconds秒后,将向进程本身发送SIGALRM信号,又称为闹钟时间。

进程调用alarm后,任何以前的alarm调用都将无效。如果参数seconds为零,那么进程内将不再包含任何闹钟时间。

返回值,如果调用alarm前,进程中已经设置了闹钟时间,则返回上一个闹钟时间的剩余时间,否则返回0。

4.5 setitimer

setitimer函数用于设置更精确的定时信号。在ubuntu下运行man 2 setitimer可以查看帮助信息:

函数原型:

#include <sys/time.h> 
int setitimer(int which, const struct itimerval *new_value, struct itimerval *old_value));

setitimer()比alarm功能强大,支持3种类型的定时器:

定时器描述
ITIMER_REAL 设定绝对时间;经过指定的时间后,内核将发送SIGALRM信号给本进程
ITIMER_VIRTUAL 设定程序执行时间;经过指定的时间后,内核将发送SIGVTALRM信号给本进程
ITIMER_PROF 设定进程执行以及内核因本进程而消耗的时间和,经过指定的时间后,内核将发送ITIMER_VIRTUAL信号给本进程

其中参数:

  • 第一个参数which指定定时器类型(上面三种之一);
  • 第二个参数是结构itimerval的一个实例,结构itimerval形式;
  • 第三个参数可不做处理;

4.6 pause

pause函数用于让进程暂停直至信号出现。在ubuntu下运行man 2  pause可以查看帮助信息:

函数原型:

#include <unistd.h>
int pause(void);

4.7 abort

abort函数用于终止进程。在ubuntu下运行man 3  abort可以查看帮助信息:

函数原型:

#include <stdlib.h> 
void abort(void);

向进程发送SIGABRT信号,默认情况下进程会异常退出,当然可定义自己的信号处理函数。即使SIGABORT被进程设置为阻塞信号,调用abort后,SIGABRT仍然能被进程接收。该函数无返回值。

五、设备驱动向进程发送信号

在之前小节中,我们介绍了如何实现进程P2向进程P1发送一个信号,以3.2节中介绍sigaction示例程序为例,我们可以得到下面这些信息:

  • 信号的发送方:必须知道向谁(PID)发送信号,发送哪个信号;
  • 信号接收方:必须定义信号处理函数,并安装信号以及信号处理函数的映射关系;

其中信号的发送方为kill进程,信号的接收方为hello进程。

那么我们试想我们的信号发送方是否可以是设备驱动程序呢?当然可以。那这一节我们就介绍如何实现设备驱动程序向用户进程发送信号。

5.1 获取进程PID

驱动程序如何才能知道用户进程的PID呢?可以让应用程序通过fcntl函数,把自己的PID主动告诉驱动程序;

fcntl函数可以对一个已经打开的文件描述符执行一系列控制操作,在ubuntu下运行man 2  fcntl可以查看帮助信息(内容太多,只截取部分):

fcntl函数原型如下所示:

#include <unistd.h>
#include <fcntl.h>int fcntl(int fd, int cmd,... /* arg */);

函数参数:

  • fd: 文件描述符。
  • cmd: 操作命令。此参数表示我们将要对 fd 进行什么操作, cmd 参数支持很多操作命令,大家可以打开 man 手册查看到这些操作命令的详细介绍,这些命令都是以 F_XXX 开头的,譬如 F_DUPFD 、 F_GETFD 、F_SETFD 等,不同的 cmd 具有不同的作用, cmd 操作命令大致可以分为以下 5 种功能:
    •  复制文件描述符,对应的cmd:F_DUPFD、F_DUPFD_CLOEXEC。当使用这两个cmd时,需要传入第三个参数,fcntl返回复制后的文件描述符,此返回值是之前未被占用的描述符,并且必须一个大于等于第三个参数值。F_DUPFD命令要求返回的文件描述符会清除对应的FD_CLOEXEC标志;F_DUPFD_CLOEXEC要求设置新描述符的FD_CLOEXEC标志;
    • 获取/设置文件描述符标志,对应的cmd:F_GETFD、F_SETFD。用于设置FD_CLOEXEC标志,此标志的含义是:当进程执行exec系统调用后此文件描述符会被自动关闭;
    • 获取/设置文件状态标志,对应的cmd:F_GETFL、F_SETFL。获取当前打开文件的访问标志,设置对应的文件访问标志;
    • 管理IO信号,对应的cmd:F_GETOWN、F_SETOWN。获取和设置用来接收SIGIO/SIGURG信号的进程id或者进程组id,返回对应的进程id或者进程组id取负值。
    • 获取/设置记录锁,对应的cmd:F_GETLK、F_SETLK、F_SETLKW;
  • fcntl 函数是一个可变参函数,第三个参数需要根据不同的 cmd 来传入对应的实参,配合 cmd 来使用;

返回值:执行失败情况下,返回 -1 ,并且会设置 errno ;执行成功的情况下,其返回值与cmd (操作命令)有关,譬如 :

  • cmd=F_DUPFD (复制文件描述符)将返回一个新的文件描述符;
  • cmd=F_GETFD (获取文件描述符标志)将返回文件描述符标志;
  • cmd=F_GETFL (获取文件状态标志)将返回文件状态标志等;

这里以设置进程id为例:

fcntl(fd, F_SETOWN, getpid());    // 对文件描述符设置SIGIO、SIGURG信号所通知的进程

5.2 使能信号驱动IO

通过F_SETFL 控制命令设置设备文件以支持O_ASYNC, 即信号驱动IO,示例:

int fl;
fl = fcntl(fd, F_GETFL);
fcntl(fd, F_SETFL, fl | O_FASYNC );

cmd设置为F_GETFL、F_SETFL时。 args参数传入文件状态标志位,使用open函数时可以指定:O_APPEND、O_ASYNC、O_CLOEXEC、O_CREAT、O_DIRECT、O_DSYNC、O_NONBLOCK、O_SYNC等。

而fcntl只可以使用open函数中的部分标志:O_APPEND, O_DIRECT、O_NONBLOCK,O_SYNC和O_ASYNC,这里可能参考UNIX高级环境编程(14)文件IO - O_DIRECT和O_SYNC详解 < 海棠花溪 >,或者在shell运行man 2 open直接看英文描述:

  • O_NONBLOCK:非阻塞IO;如果read调用没有可读取的数据,或者没有空间可以进行write操作,read或write调用返回EAGAIN,而不会阻塞进程;
  • O_APPEND :强制每次写(write)操作都添加在文件的末尾,相当于open的O_APPEND标志         
  • O_DIRECT:绕过缓冲区高速缓存,直接IO,一般和O_SYNC搭配使用;                 
  • O_ASYNC:使能信号驱动IO,当输入缓存中的输入数据就绪时(输入数据可读),内核向用F_SETOWN来绑定的那个进程发送SIGIO信号;
  • O_SYNC :同步IO,以同步方式写入文件,每个write调用会自动将文件数据和元数据刷新到磁盘上;由于频繁IO操作,导致写入性能很低;

注意:在修改文件描述符标志或文件状态标志时必须谨慎,先要取得现在的标志值,然后按照希望修改它,最后设置新标志值。不能只是执行F_SETFD或F_SETFL命令,这样会关闭以前设置的标志位。

5.3 按键测试应用程序

#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
#include <signal.h>
#include <unistd.h>

int fd,ret;

/* 信号处理函数 */
void signal_fun(int signum)
{
    unsigned int key_val = 0;
    static int count = 0;
    printf("signal = %d, %d count\n",signum,++count);

    /* 读取按键值 */
    ret = read(fd, &key_val, 1);    
    if(ret < 0){
        printf("read error\n");
    }
    printf("key_val = 0x%x\n", key_val);    
}

int main(int argc,char **argv)
{
    int flags;
    
    fd = open("/dev/buttons", O_RDWR);
    if (fd < 0)
    {
        printf("can't open!\n");
        return -1;
    }

    /* 对文件描述符设置SIGIO、SIGURG信号所通知的进程 */
    fcntl(fd, F_SETOWN, getpid());    
    /* 获取文件状态描述符 */
    flags = fcntl(fd,F_GETFL);
    /* 设置信号驱动IO */
    fcntl(fd,F_SETFL, flags | O_ASYNC);

    /* 调用signal函数,让指定的信号SIGIO与处理函数signal_fun对应 */
    signal(SIGIO,signal_fun);  

    while (1)
    {
        sleep(10);    // 休眠10s
    }
    
    return 0;
}

这里我们编写了SIGIO的信号的处理函数signal_fun,在这个函数中,我们会在按键测试应用程序输出接受到SIGIO信号的次数,以及从设备中读取到的值。

5.4 按键驱动程序

按键测试应用程序端负责捕获信号,则设备驱动负责释放信号,需要在设备驱动程序中增加信号释放的相关代码。 

为了使设备支持信号驱动IO,设备驱动程序中涉及3项工作:

  • 支持F_SETOWN命令,能在这个控制命令处理中设置filp->f_owner为对应进程ID。不过此项工作已由内核完成,设备驱动程序中无须处理;
  • 支持F_SETFL命令的处理,添加O_ASYNC标志,会调用驱动程序中的.fasync函数。因此,我们在按键驱动中实现了button_fasync函数;
  • 当有按键发生变化时,向按键测试应用程序发送SIGIO信号,调用kill_fasync函数激发相应的信号;

上述的3项工作和按键测试应用程序中的3项是对应的,他们之间的关系,如下图:

最终完整代码:

#include <linux/module.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/io.h>
#include <linux/uaccess.h>
#include <linux/gpio.h>
#include <linux/irq.h>        // 包含了mach/irqs.h
#include <linux/interrupt.h>
#include <linux/gpio/machine.h>
#include <mach/gpio-samsung.h>
/*
 全局静态变量:希望全局变量仅限于在本源文件中使用,在其他源文件中不能引用,也就是说限制其作用域只在
 定义该变量的源文件内有效,而在同一源程序的其他源文件中不能使用
 */
#define OK   (0)
#define ERROR  (-1)
#define __IRQT_FALEDGE IRQ_TYPE_EDGE_FALLING
#define __IRQT_RISEDGE IRQ_TYPE_EDGE_RISING
#define __IRQT_LOWLVL IRQ_TYPE_LEVEL_LOW
#define __IRQT_HIGHLVL IRQ_TYPE_LEVEL_HIGH
#define IRQT_NOEDGE (0)
#define IRQT_RISING (__IRQT_RISEDGE)
#define IRQT_FALLING (__IRQT_FALEDGE)
#define IRQT_BOTHEDGE (__IRQT_RISEDGE|__IRQT_FALEDGE)
#define IRQT_LOW (__IRQT_LOWLVL)
#define IRQT_HIGH (__IRQT_HIGHLVL)
#define IRQT_PROBE IRQ_TYPE_PROBE

/* 键值: 按下时, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06 */
/* 键值: 松开时, 0x81, 0x82, 0x83, 0x84, 0x85, 0x86 */
static unsigned char key_val;

struct pin_desc{
    unsigned int pin;
    unsigned int key_val;
};

/*
 * S3C2410_GPG 定义在arch/arm/mach-s3c24xx/include/mach/gpio-samsung.h
 */
static struct pin_desc pins_desc[4] = {
    {S3C2410_GPG(0), 0x01},
    {S3C2410_GPG(3), 0x02},
    {S3C2410_GPG(5), 0x03},
    {S3C2410_GPG(6), 0x04},
    {S3C2410_GPG(7), 0x05},
    {S3C2410_GPG(11), 0x06},
};

/*  异步信号结构体变量  */
static struct fasync_struct * button_async;

/*
 * 中断处理服务
 */
static irqreturn_t button_irq(int irq, void *dev_id)
{
    struct pin_desc *pindesc = (struct pin_desc *)dev_id;
    unsigned int pinval;

   pinval = gpio_get_value(pindesc->pin);
 
    if (pinval){
        /* 松开 */
        key_val = 0x80 | pindesc->key_val;
    }
    else{
        /* 按下 */
        key_val = pindesc->key_val;
    }
 
    kill_fasync(&button_async, SIGIO, POLL_IN);   /* 发送SIGIO信号给应用层 */
 
    return IRQ_RETVAL(IRQ_HANDLED);
}

/* 
   GPG0、GPG3、GPG5、GPG6、GPG7、GPG11配置为中断功能 
   IRQ_EINTX 定义在 arch/arm/mach-s3c24xx/include/mach/irqs.h
*/
static int button_open(struct inode *inode, struct file *file)
{
    /* 注册中断 */
    int ret;
    ret = request_irq(IRQ_EINT8,button_irq,IRQT_BOTHEDGE,"K1",&pins_desc[0]);
    if(ret){
        printk("open failed K1");
    }

    ret = request_irq(IRQ_EINT11,button_irq,IRQT_BOTHEDGE,"K2",&pins_desc[1]);
    if(ret){
        printk("open failed K2");
    }

    ret = request_irq(IRQ_EINT13,button_irq,IRQT_BOTHEDGE,"K3",&pins_desc[2]);
    if(ret){
        printk("open failed K3");
    }

    ret = request_irq(IRQ_EINT14,button_irq,IRQT_BOTHEDGE,"K4",&pins_desc[3]);
    if(ret){
        printk("open failed K4");
    }

    ret = request_irq(IRQ_EINT15,button_irq,IRQT_BOTHEDGE,"K5",&pins_desc[4]);
    if(ret){
        printk("open failed K5");
    }

    ret = request_irq(IRQ_EINT19,button_irq,IRQT_BOTHEDGE,"K6",&pins_desc[5]);
    if(ret){
        printk("open failed K6");
    }

    return 0;
}

static ssize_t button_read(struct file *file, char __user *buf, size_t size, loff_t *ppos)
{
    int count;
    if (size != 1){
        printk("read error\n");
        return -EINVAL;
    }

    /* 如果有按键动作, 上传key_val给用户层 */
    count = copy_to_user(buf, &key_val, 1);
    
    return count;
}

/*
 * 释放中断资源
 */
static int button_close(struct inode *inode, struct file *file)
{
    free_irq(IRQ_EINT8, &pins_desc[0]);
    free_irq(IRQ_EINT11, &pins_desc[1]);
    free_irq(IRQ_EINT13, &pins_desc[2]);
    free_irq(IRQ_EINT14, &pins_desc[3]);
    free_irq(IRQ_EINT15, &pins_desc[4]);
    free_irq(IRQ_EINT19, &pins_desc[5]);

    return 0;
}

static int button_fasync (int fd, struct file *file, int on)
{
      printk("button_fasync ok\n");
    /* 初始化button_async结构体,就能使用kill_fasync函数 */
    return fasync_helper(fd, file, on, &button_async); 
}

static struct file_operations button_fops = {
    .owner   =   THIS_MODULE,
    .open    =   button_open,
    .read    =   button_read,
    .release =   button_close,
    .fasync  =   button_fasync, 
};

static dev_t devid;                      // 起始设备编号
static struct cdev button_cdev;          // 保存操作结构体的字符设备 
static struct class *button_cls;

static int button_init(void)
{
    
    /* 动态分配字符设备: (major,0) */
    if(OK == alloc_chrdev_region(&devid, 0, 1,"button")){   // ls /proc/devices看到的名字
        printk("register_chrdev_region ok\n");
    }else {
        printk("register_chrdev_region error\n");
        return ERROR;
    }
    
     cdev_init(&button_cdev, &button_fops);
     cdev_add(&button_cdev, devid, 1);


    /* 创建类,它会在sys目录下创建/sys/class/button这个类  */
     button_cls = class_create(THIS_MODULE, "button");
     if(IS_ERR(button_cls)){
         printk("can't create class\n");
         return ERROR;
     }
    /* 在/sys/class/button下创建buttons设备,然后mdev通过这个自动创建/dev/buttons这个设备节点 */
     device_create(button_cls, NULL, devid, NULL, "buttons"); 

     return 0;
}

static void __exit button_exit(void)
{
    printk("button driver exit\n");
    /* 注销类、以及类设备 /sys/class/button会被移除*/
    device_destroy(button_cls, devid);
    class_destroy(button_cls);

    cdev_del(&button_cdev);
    unregister_chrdev_region(devid, 1);
    return;
}

module_init(button_init);
module_exit(button_exit);
MODULE_LICENSE("GPL");

这里有一点需要注意:

我们的按键测试应用程序在最后,在while死循环中,使用了sleep进行睡眠,使用sleep进行睡眠的进程是可以被信号唤醒的。

睡眠有两种相关的进程状态:TASK_INTERRUPTIBLE 和 TASK_UNINTERRUPTIBLE 。它们唯一的区别是处于 TASK_UNINTERRUPTIBLE 的进程会忽略信号,而处于 TASK_INTERRUPTIBLE 状态的进程如果接收到一个信号,会被提前唤醒并响应该信号。使用sleep进行睡眠的进程状态会被设置为TASK_INTERRUPTIBLE。

在按键设备驱动程序中:

  • 当有按键发生变更时,触发外部中断;
  • 执行中断处理程序,在中断处理程序中向按键测试应用程序发送SIGIN信号,会唤醒我们的按键测试应用程序;
  • 在从内核态返回到用户态之前,首先检查信号队列是否有信号,如果有,执行应用程序中的信号处理函数;

5.5 测试

将驱动程序和测试应用复制到开发板nfs根文件系统,安装驱动。运行按键测试应用程序:

[root@zy:/]# ./main
button_fasync ok
signal = 29, 1 count
key_val = 0x1
signal = 29, 2 count
key_val = 0x1
signal = 29, 3 count
key_val = 0x1
signal = 29, 4 count
key_val = 0x81
signal = 29, 5 count
key_val = 0x3
signal = 29, 6 count
key_val = 0x83

参考文章:

[1]六、Linux驱动之异步通知

[2]Linux信号驱动IO 学习记录

[3]linux内核剖析(九)进程间通信之-信号signal(部分转载)

[4]Linux进程间通信之信号 

[5]linux 信号安装、signal、kill,arise讲解

[6]9.按键之使用异步通知(详解)

posted @ 2022-02-18 22:53  大奥特曼打小怪兽  阅读(361)  评论(0编辑  收藏  举报
如果有任何技术小问题,欢迎大家交流沟通,共同进步