为了账号安全,请及时绑定邮箱和手机立即绑定

你真的懂Linux内核中的阻塞和异步通知机制吗?(花了五天整理,墙裂推荐!)

标签:
C C++ 操作系统

@[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函数就会执行。

大家的鼓励是我继续创作的动力,如果觉得写的不错,欢迎关注,点赞,收藏,转发,谢谢!

点击查看更多内容
TA 点赞

若觉得本文不错,就分享一下吧!

评论

作者其他优质文章

正在加载中
Linux系统工程师
手记
粉丝
3
获赞与收藏
3

关注作者,订阅最新文章

阅读免费教程

  • 推荐
  • 评论
  • 收藏
  • 共同学习,写下你的评论
感谢您的支持,我会继续努力的~
扫码打赏,你说多少就多少
赞赏金额会直接到老师账户
支付方式
打开微信扫一扫,即可进行扫码打赏哦
今天注册有机会得

100积分直接送

付费专栏免费学

大额优惠券免费领

立即参与 放弃机会
意见反馈 帮助中心 APP下载
官方微信

举报

0/150
提交
取消