乐趣区

关于c:你真的懂Linux内核中的阻塞和异步通知机制吗

@[TOC]

阻塞 / 非阻塞简介

  阻塞操作是指在执行设施操作时,若不能取得资源,则 挂起过程 直到满足可操作的条件后再进行操作。被挂起的过程进入 睡眠状态 ,被从调度器的运行队列移走, 直到期待的条件被满足 。而非阻塞操作的过程在不能进行设施操作时, 并不挂起,它要么放弃,要么不停地查问,直至能够进行操作为止。

阻塞 / 非阻塞例程

  阻塞形式

int fd;
int data = 0;
fd = open("/dev/xxx_dev", O_RDWR); /* 阻塞形式关上 */
ret = read(fd, &data, sizeof(data)); /* 读取数据 */

  非阻塞形式

int fd;
int data = 0; 
fd = open("/dev/xxx_dev", O_RDWR | O_NONBLOCK); /* 非阻塞形式关上 */ 
ret = read(fd, &data, sizeof(data)); /* 读取数据 */

期待队列简介

  期待队列是内核中一个重要的数据结构。阻塞形式拜访设施时,如果设施不可操作,那么过程就会进入 休眠状态。期待队列就是来实现过程休眠操作的一种数据结构。

期待队列相干函数

定义期待队列

wait_queue_head_t my_queue;

  wait_queue_head_t 是__wait_queue_head 构造体的一个 typedef。

初始化期待队列头

void init_waitqueue_head(wait_queue_head_t *q)

  参数 q 就是要初始化的期待队列头,也能够应用宏 DECLARE_WAIT_QUEUE_HEAD (name)来一次性实现期待队列头的定义的初始化。

定义并初始化一个期待队列项

DECLARE_WAITQUEUE(name, tsk)

  name就是期待队列项的名字,tsk 示意这个期待队列项属于哪个工作过程,个别设置为 current,在 Linux 内核中 current相当于一个全局变量,示意 以后过程。因而宏 DECLARE_WAITQUEUE 就是给以后正在运行的过程创立并初始化了一个期待队列项。

将队列项增加到期待队列头

void add_wait_queue(wait_queue_head_t *q, wait_queue_t *wait)

  q:期待队列项要退出的期待队列头
  wait:要退出的期待队列项
   返回值:无

将队列项从期待队列头移除

void remove_wait_queue(wait_queue_head_t *q, wait_queue_t *wait)

  q: 要删除的期待队列项所处的期待队列头
  wait:要删除的期待队列项。
   返回值:无

期待唤醒

void wake_up(wait_queue_head_t *q) 
void wake_up_interruptible(wait_queue_head_t *q)

  q: 就是要唤醒的期待队列头,这两个函数会将这个期待队列头中的所有过程都唤醒
  wake_up 函数能够唤醒处于 TASK_INTERRUPTIBLE 和 TASK_UNINTERRUPTIBLE 状态的过程,而 wake_ up_ interruptible 函数只能唤醒处于 TASK_INTERRUPTIBLE 状态的过程

期待事件

wait_event(wq, condition)

  期待以 wq 为期待队列头的期待队列被唤醒,前提是 condition 条件必须满足 (为真), 否则始终阻塞。此函数会将过程设置为 TASK _UNINTERRUPTIBLE 状态

wait_event_timeout(wq, condition, timeout)

  性能和 wait_event 相似,然而此函数能够增加 超时工夫 ,以 jiffies 为单位。此函数有返回值,如果返回 0 的话示意超时工夫到,而且 condition 为假。为 1 的话示意 condition 为真,也就是条件满足了。

wait_event_interruptible(wq, condition)

  与 wait event 函数相似,然而此函数将过程设置为 TASK_INTERRUPTIBLE,就是 能够被信号打断

wait_event_interruptible_timeout(wq, condition, timeout)

  与 wait event timeout 函数相似,此函数也将过程设置为 TASK_INTERRUPTIBLE,能够被信号打断

轮询

  当应用程序以非阻塞的形式拜访设施时,会一遍一遍的去查问咱们的设施是否能够拜访,这个查问操作就叫做轮询。内核中提供了 poll,epoll,select 函数来解决轮询操作。当应用程序在下层通过 poll,epoll,select 函数来查问设施时,驱动程序中的 poll,epoll,select 函数就要在底层实现查问,如果能够操作的话,就会从读取设施的数据或者向设施写入数据。

select

  函数原型

int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout)

  nfds:要操作的文件描述符个数。
  readifds、writefds 和 exceptfds:这三个指针指向 描述符汇合 ,这三个参数指明了关怀哪些描述符、须要满足哪些条件等等,这三个参数都是 fd_set 类型的,fd_set 类型变量的每一个位都代表了一个文件描述符。readfds 用于监督指定描述符集的 读变动 ,也就是监督这些文件是否能够读取,只有这些汇合外面有一个文件能够读取, 那么 seclect 就会返回一个大于 0 的值示意文件能够读取。如果没有文件能够读取,那么就会依据 timeout 参数来判断是否超时。能够 将 reads 设置为 NULL,示意不关怀任何文件的读变动。writefds 和 reads 相似,只是 writers 用于监督这些文件是否能够进行写操作。exceptfds 用于监督这些文件的异样
  timeout:超时工夫,当咱们调用 select 函数期待某些文件描述符能够设置超时工夫,超时工夫应用构造体 timeval 示意,构造体定义如下所示:

struct timeval { 
long tv_sec; /* 秒 */
long tv_usec; /* 奥妙 */ 
};

  当 timeout 为 NULL 的时候就示意 无限期的期待返回值。0,示意的话就示意超时产生,然而没有任何文件描述符能够进行操作;-1,产生谬误;其余值,能够进行操作的文件描述符个数。
  操作 fd_set 变量的函数

void FD_ZERO(fd_set *set) 
void FD_SET(int fd, fd_set *set) 
void FD_CLR(int fd, fd_set *set) 
int FD_ISSET(int fd, fd_set *set)

  FD_ZERO 用于将 fd set 变量的 所有位都清零 ,FD_SET 用于将 fd_set 变量的某个位 置 1 ,也就是向 fd_set 增加一个文件描述符,参数 fd 就是要退出的文件描述符。FD_CLR 用户将 fd_set 变量的 某个位清零 ,也就是将一个文件描述符从 fd_set 中删除,参数 fd 就是要删除的文件描述符。FD_ISSET 用于 测试 fd_set 的 某个位是否置 1 ,也就是判断某个文件 是否能够进行操作,参数 fd 就是要判断的文件描述符。


void main(void) 
{  int ret, fd; /* 要监督的文件描述符 */ 
    fd_set readfds; /* 读操作文件描述符集 */
    struct timeval timeout; /* 超时构造体 */ 
    fd = open("dev_xxx", O_RDWR | O_NONBLOCK); /* 非阻塞式拜访 */ 
    FD_ZERO(&readfds); /* 革除 readfds */ 
    FD_SET(fd, &readfds); /* 将 fd 增加到 readfds 外面 */ 
     /* 结构超时工夫 */ 
     timeout.tv_sec = 0; 
     timeout.tv_usec = 500000; /* 500ms */ 
     ret = select(fd + 1, &readfds, NULL, NULL, &timeout); 
     switch (ret) { 
        case 0: /* 超时 */ 
            printf("timeout!\r\n");
            break; 
        case -1: /* 谬误 */ 
            printf("error!\r\n"); 
            break; 
        default: /* 能够读取数据 */ 
        if(FD_ISSET(fd, &readfds))   /* 判断是否为 fd 文件描述符 */ 
          {/* 应用 read 函数读取数据 */} 
         break; 
    } 
 }

poll

  在单个线程中,select 函数可能监督的文件描述符数量有最大的限度,个别为 1024,能够批改内核将监督的文件描述符数量改大,然而这样会升高效率!这个时候就能够应用 poll 函数,poll 函数实质上和 select 没有太大的差异,然而 poll 函数 没有最大文件描述符限度,Linx 应用程序中 poll 函数原型如下所示:

int poll(struct pollfd *fds, nfds_t nfds, int timeout)

  函数参数和返回值含意如下
  fds:要监督的文件描述符汇合以及要监督的事件,为一个数组,数组元素都是构造体 polled 类型的,pollfd 构造体如下所示

struct pollfd 
{ 
    int fd; /* 文件描述符 文件描述符 文件描述符 */ 
    short events; /* 申请的事件 申请的事件 申请的事件 */
    short revents; /* 返回的事件 返回的事件 返回的事件 */ 
};

  fd 是要监督的文件描述符,如果 f 有效的话那么 events 监督事件也就有效,并且 revents 返回 0。events 是要监督的事件,可监督的事件类型如下所示

POLLIN        // 有数据能够读取。POLLPRI        // 有紧急的数据须要读取。POLLOUT        // 能够写数据 POLLERR 指定的文件描述符产生谬误 POLLHUP 指定的文件描述符挂起 POLLNVAL 有效的申请 POLLRDNORM 等同于 POLLIN

  revents:返回参数,也就是返回的事件,有 Linux 内核设置具体的返回事件。
  nfds:poll 函数要监督的文件描述符数量
  timeout:超时工夫,单位为 ms
   返回值:返回 revents 域中不为 0 的 polled 构造体个数,也就是产生事件或谬误的文件描述符数量;0,超时;-1,产生谬误,并且设置 errno 为谬误类型

void main(void)
{ 
   int ret; 
   int fd; /* 要监督的文件描述符 */
   struct pollfd fds;
   fd = open(filename, O_RDWR | O_NONBLOCK); /* 非阻塞式拜访 */ 
    /* 结构构造体 */ 
   fds.fd = fd; 
   fds.events = POLLIN; /* 监督数据是否能够读取 */
   ret = poll(&fds, 1, 500); /* 轮询文件是否可操作,超时 500ms */ 
    if (ret)
     { /* 数据无效 */ 
      /* 读取数据 */ 
     } else if (ret == 0) 
     {/* 超时 */} else if (ret < 0) 
     {/* 谬误 */} 
 }

epoll

  传统的 selcet 和 poll 函数都会随着所监听的 fd 数量的减少,呈现效率低下的问题,而且 poll 函数每次必须遍历所有的描述符来查看就绪的描述符,这个过程很浪费时间。为此,epoll 因运而生,epoll 就是为解决大并发而筹备的,个别经常在网络编程中应用 epoll 函数。应用程序须要先应用 epoll_create 函数创立一个 epoll 句柄,epoll create 函数原至如下.

int epoll_create(int size)

  函数参数和返回值含意如下:
  size;从 Linux2.6.8 开始此参数曾经没有意义了,轻易填写一个大于 0 的值就能够
   返回值:epoll 句柄,如果为 - 1 的话示意创立失败,epoll 句柄创立胜利当前应用,epoll ctl 函数向其中增加要监督的文件描述符以及监督的事 ct 函数原型如下所示

int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event)

  函数参数和返回值含意如下
  epfd;要操作的 epoll 句柄,也就是应用 epoll_create 函数创立的 epoll 句柄。
  p:示意要对 epfd(epoll 句柄)进行的操作,能够设置为

EPOLL CTL ADD        // 向印 fd 增加文件参数 d 示意的描述符 EPOLL CTL MOD 批改参数 fd 的 event 事件。EPOLL CTL DEL        // 从 f 中删除过 l 描述符

  fd:要监督的文件形容
  event:要监督的事件类型,为 epoll_event 构造体类型指针,epoll_event 构造体类型如下所

struct epoll_event 
{
     uint32_t events; /* epoll 事件 */ 
    epoll_data_t data; /* 用户数据 用户数据 */ 
};

  构造体 epoll_event 的 events 成员变量示意要监督的事件,可选的事件如下所示

EPOLLIN            // 有数据能够读取 EPOLLOUT 能够写数据
EPOLLPRI        // 有紧急的数据须要读取 EPOLLERI 指定的文件描述符产生谬误。EPOLLHUP        // 指定的文件描述符挂起 POLLET 设置 epo 为边际触发,默认触发模式为程度触发王
POLLONESHOT        // 一次性的监督,当监督实现当前还须要再次监督某个 fd,那么就须要将 fd 从新增加到 epoll 外面

  下面这些事件能够进行“或”操作,也就是说能够设置监督多个事件返回值:0,胜利;-1,失败,并且设置 errno 的值为相应的错误码。所有都设置好当前应用程序就能够通过 epoll_wait 函数来期待事件的产生,相似 select 函数。epoll_wait 函数原型如下所示

int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout)

  函数参数和返回值含意如下
  epfd:要期待的 epoll
  events:指向 epoll_event 构造体的数组,当有事件产生的时候 Iimx 内核会填写 events,调用者能够依据 events 判断产生了哪些事件。
  prevents:events 数组大小,必须大于 0
  timeout:超时工夫,单位为 ms 返回值:0,超时;-1,谬误;其余值,准备就绪的文件描述符数量。
  epoll 更多的是用在大规模的并发服务器上,因为在这种场合下 select 和 poll 并不适宜。当设计到的文件描述符(fd 比拟少的时候就适宜用 selcet 和 pl 本章咱们就应用 sellect 和 poll 这两个函数

异步告诉概念

  阻塞与非阻塞拜访、poll 函数提供了较好的解决设施拜访的机制,然而如果有了异步告诉,整套机制则更加残缺了。
  异步告诉的意思是:一旦设施就绪,则被动告诉应用程序 ,这样应用程序基本就不须要查问设施状态,这一点十分相似于硬件上“中断”的概念,比拟精确的称呼是“信号驱动的异步 I /O”。 信号是在软件档次上对中断机制的一种模仿 ,在原理上, 一个过程收到一个信号与处理器收到一个中断请求能够说是一样的。信号是异步的,一个过程不用通过任何操作来期待信号的达到,事实上,过程也不晓得信号到底什么时候达到。
  阻塞 I / O 意味着始终期待设施可拜访后再拜访,非阻塞 I / O 中应用 poll()意味着查问设施是否可拜访,而异步告诉则意味着设施告诉用户本身可拜访,之后用户再进行 I / O 解决。由此可见,这几种 I / O 形式能够互相补充。

Linux 信号

  异步告诉的外围就是信号,在 arch/xtensa/include/uapi/asm/signal.h 文件中定义了 Linux 所反对的所有信号

#define SIGHUP      1/* 终端挂起或管制过程终止 */ 
#define SIGINT      2/* 终端中断(Ctrl+ C 组合键) */ 
#define SIGQUIT     3 /* 终端退出(Ctrl+\ 组合键) */
#define SIGILL      4/* 非法指令 */ 
#define SIGTRAP     5/* debug 应用,有断点指令产生 */
#define SIGABRT     6/* 由 abort(3)收回的退出指令 */ 
#define SIGIOT      6 /* IOT 指令 */ 
#define SIGBUS      7 /* 总线谬误 */ 
#define SIGFPE      8 /* 浮点运算谬误 */ 
#define SIGKILL     9 /* 杀死、终止过程 */ 
#define SIGUSR1     10 /* 用户自定义信号 1 */ 
#define SIGSEGV     11 /* 段违例(有效的内存段) */
#define SIGUSR2     12 /* 用户自定义信号 2 */ 
#define SIGPIPE     13 /* 向非读管道写入数据 */ 
#define SIGALRM     14 /* 闹钟 */
#define SIGTERM     15 /* 软件终止 */
#define SIGSTKFLT   16 /* 栈异样 */
#define SIGCHLD     17 /* 子过程完结 */
#define SIGCONT     18 /* 过程持续 */
#define SIGSTOP     19 /* 进行过程的执行,只是暂停 */
#define SIGTSTP     20 /* 进行过程的运行(Ctrl+ Z 组合键) */ 
#define SIGTTIN     21 /* 后盾过程须要从终端读取数据 */ 
#define SIGTTOU     22 /* 后盾过程须要向终端写数据 */
#define SIGURG      23 /* 有 "紧急" 数据 */
#define SIGXCPU     24 /* 超过 CPU 资源限度 */ 
#define SIGXFSZ     25 /* 文件大小超额 */ 
#define SIGVTALRM   26 /* 虚构时钟信号 */ 
#define SIGPROF     27 /* 时钟信号形容 */
#define SIGWINCH    28 /* 窗口大小扭转 */ 
#define SIGIO       29 /* 能够进行输出 / 输入操作 */
#define SIGPOLL SIGIO 
 /* #define SIGLOS 29 */ 
#define SIGPWR      30 /* 断点重启 */ 
#define SIGSYS      31 /* 非法的零碎调用 */ 
#define SIGUNUSED   31 /* 未应用信号 */

异步告诉代码

  咱们应用中断的时候须要设置中断处理函数,同样的,如果要在应用程序中应用信号,那么就必须设置信号所应用的 信号处理函数,在应用程序中应用 signal 函数来设置指定信号的处理函数,signal 函数原型如下所示

void (*signal(int signum, void (*handler))(int)))(int);

  该函数原型较难了解,它能够合成为:

typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler));

  第一个参数指定信号的值,第二个参数指定针对后面信号值的处理函数,若为 SIG_IGN,示意疏忽该信号;若为 SIG_DFL,示意采纳零碎默认形式解决信号;若为用户自定义的函数,则信号被捕捉到后,该函数将被执行。
  如果 signal 调用胜利,它返回最初一次为信号 signum 绑定的处理函数的 handler 值,失败则返回 SIG_ERR。

驱动中的信号处理

fasync_struct 构造体

  首先咱们须要在驱动程序中定义个 fasync_struct 构造体指针变量,fasync_struct 构造体内容如下

struct fasync_struct 
{ spinlock_t fa_lock; 
int magic; 
int fa_fd; 
struct fasync_struct *fa_next; 
struct file *fa_file; 
struct rcu_head fa_rcu; 
};

  个别将 fasync_struct 构造体指针变量定义到设施构造体中,比方在 xxx_dev 构造体中增加一个 fasync_struct 构造体指针变量,后果如下所示

struct xxx_dev 
{ 
    struct device *dev; 
    struct class *cls;
    struct cdev cdev;
 ...... 
     struct fasync_struct *async_queue; /* 异步相干构造体 */
 }; 

fasync 函数

  如果要应用异步告诉,须要在设施驱动中实现 file_ operations 操作集中的 fasync 函数,此函数格局如下所示:

int (*fasync) (int fd, struct file *filp, int on)

  fasync 函数外面个别通过调用 fasync_helper 函数来初始化后面定义的 fasync_struct 构造体指针,fasync_helper 函数原型如下

int fasync_helper(int fd, struct file * filp, int on, struct fasync_struct **fapp)

  fasync_helper 函数的前三个参数就是 fasync 函数的那三个参数,第四个参数就是要初始化的 fasync_ struct 构造体指针变量。当应用程序通过构造体指针变量。当应用程序通过“fcntl(fd, F_SETFL, flags | FASYNC)”扭转 fasync 标记的时候,驱动程序 file_operations 操作集中的 fasync 函数就会执行。

struct xxx_dev 
{ 
    ......
    struct fasync_struct *async_queue; /* 异步相干构造体 */ 
}; 
static int xxx_fasync(int fd, struct file *filp, int on)
{struct xxx_dev *dev = (xxx_dev)filp->private_data; 
     if (fasync_helper(fd, filp, on, &dev->async_queue) < 0) 
     return -EIO; 
     return 0; 
 } 
     static struct file_operations xxx_ops =
 { 
       ...... 
      .fasync = xxx_fasync,
      ...... 
 };

  在敞开驱动文件的时候须要在 file_ operations 操作集中的 release 函数中开释 fasyn_fasync struct 的开释函数同样为 fasync_helper, release 函数参数参考实例如下

static int xxx_release(struct inode *inode, struct file *filp) 
 {return xxx_fasync(-1, filp, 0); /* 删除异步告诉 */
 }
static struct file_operations xxx_ops =
 { 
    ...... 
    .release = xxx_release, 
 };

  第 3 行通过调用示例代码 xxx_fasync 函数来实现 fasync_struct 的开释工作,然而,其最终还是通过 fasync_helper 函数实现开释工作。

kill_fasync 函数

  当设施能够拜访的时候,驱动程序须要向应用程序发出信号,相当于产生“中断”kill_fasync 函数负责发送指定的信号,kill_fasync 函数原型如下所示

void kill_fasync(struct fasync_struct **fp, int sig, int band)

  函数参数和返回值含意如下:
  fasync struct 要操作的文件指针
  sig:要发送的信号
   band:可读时设置为 POLL IN,可写时设置为 POLL OUT。
  返回值:无。

应用程序对异步告诉的解决

  应用程序对异步告诉的解决包含以下三步
  1、注册信号处理函数应用程序依据驱动程序所应用的信号来设置信号的处理函数,应用程序应用 signal 函数来设置信号的处理函数。后面曾经具体的讲过了,这里就不细讲了。
  2、将本应用程序的过程号通知给内核应用 fcntl(fd, F_SETOWN, getpid) 将本应用程序的过程号通知给内核
  3、开启异步告诉应用如下两行程序开启异步告诉:

flags = fcntl(fd, F_GETFL); /* 获取以后的过程状态 */ 
fcntl(fd, F_SETFL, flags | FASYNC); /* 开启以后过程异步告诉性能 */

  重点就是通过 fcntl 函数设置过程状态为 FASYNC,通过这一步,驱动程序中的 fasync 函数就会执行。

大家的激励是我持续创作的能源,如果感觉写的不错,欢送关注,点赞,珍藏,转发,谢谢!
如遇到排版错乱的问题,能够通过以下链接拜访我的 CSDN。

**CSDN:[CSDN 搜寻“嵌入式与 Linux 那些事”]

退出移动版