@[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那些事”]