异步I/O+异步通知

x33g5p2x  于2022-05-16 转载在 其他  
字(6.3k)|赞(0)|评价(0)|浏览(474)

linux内核笔记(四)高级I/O操作(二)

分析第一个代码:
代码第50行 struct aiocb aiow, aior;
定义了两个分别用于读和写的异步I/O控制块

代码第56行到76行初始化了这二个控制块。

  1. memset(&aiow, 0, sizeof(aiow));
  2. memset(&aior, 0, sizeof(aior));
  3. aiow.aio_fildes = fd;
  4. aiow.aio_buf = malloc(32);
  5. strcpy((char *)aiow.aio_buf, "aio test");
  6. aiow.aio_nbytes = strlen((char *)aiow.aio_buf) + 1;
  7. aiow.aio_offset = 0;
  8. aiow.aio_sigevent.sigev_notify = SIGEV_THREAD;
  9. aiow.aio_sigevent.sigev_notify_function = aiow_completion_handler;
  10. aiow.aio_sigevent.sigev_notify_attributes = NULL;
  11. aiow.aio_sigevent.sigev_value.sival_ptr = &aiow;
  12. aior.aio_fildes = fd;
  13. aior.aio_buf = malloc(32);
  14. aior.aio_nbytes = 32;
  15. aior.aio_offset = 0;
  16. aior.aio_sigevent.sigev_notify = SIGEV_THREAD;
  17. aior.aio_sigevent.sigev_notify_function = aior_completion_handler;
  18. aior.aio_sigevent.sigev_notify_attributes = NULL;
  19. aior.aio_sigevent.sigev_value.sival_ptr = &aior;

主要是文件描述符,用于读写的缓冲区,读写的字节数和异步I/O完成后的回调函数。

代码第79行发起一个异步写操作,该函数会立即返回, 具体的写操作在底层的驱动完成。

  1. while (1) {
  2. if (aio_write(&aiow) == -1)
  3. goto fail;

然后代码81行异步读操作,该函数会立即返回, 具体的写操作在底层的驱动完成。

  1. if (aio_read(&aior) == -1)
  2. goto fail;

写完成后,注册的aiow_competion_handler写完成函数将被自动自动调用,该函数通过aio_error及aio_return获取了I/O操作的错误码及实际的写操作的返回值。给sigval.sival_ptr赋值,指向了I/O控制块aiow。

sugval.sival是第67行赋值的

aiow.aio_sigevent.sigev_value.sival_ptr = &aiow;

同样,读完成后,注册的aior_completion_handler读完成函数将会被自动调用。

除了像写完成操作中可以获取完成状态,还可以从aio_buf中获取读取的数据。

sleep()是为了模拟其他操作所消耗的时间。
在一次异步操作中,可以将多个i/o请求合并,从而完成一系列的读写操作,其对应的接口函数是lio_listio

第二次代码的编译实现:

编译加载

异步通知

关于代码1的分析:

代码41到46行对应步骤1——注册信号处理函数(注册中断处理函数)

  1. //阻塞了SIGIO,防止信号处理函数的嵌套调用
  2. sigemptyset(&act.sa_mask);
  3. sigaddset(&act.sa_mask, SIGIO);
  4. act.sa_flags = SA_SIGINFO;
  5. act.sa_sigaction = sigio_handler;
  6. if (sigaction(SIGIO, &act, &oldact) == -1)
  7. goto fail;

sigaction 比signal更高级,主要是信号阻塞和提供信号信息两方面。

使用sigaction 注册的信号处理函数的参数有三个,而第二个参数act就是关于信号的一些信息,我们随后会用到里面的内容。

另外,代码第41行和第42行

  1. sigemptyset(&act.sa_mask);
  2. sigaddset(&act.sa_mask, SIGIO);

阻塞了SIGIO自己,防止信号处理函数的嵌套调用。

  1. //设置文件属主
  2. fd = open("/dev/vser0", O_RDWR | O_NONBLOCK);
  3. if (fd == -1)
  4. goto fail;
  5. if (fcntl(fd, F_SETOWN, getpid()) == -1)
  6. goto fail;

代码第48行至第53行对应步骤(2)——打开设备文件,设置文件属主(目的是是驱动根据打开文件的file结构,找到相应的进程,从而向该进程发送信号)

即设置文件属主驱动在发信号时,处于一个所谓的任意进程上下文,即不知道当前运行的进程,要给一个特定的进程发信号,则需要一些额外的信息,可以通过fcntl将所属的进程信息保存在file 结构中,从而驱动可以根据file结构来找到对应的进程。

代码第54行和第55行对应步骤(3)——设置设备资源可用可用时驱动向进程发送的信号(非必须,但要用sigaction的高级特性需要)

  1. //设置当前资源可用时,向进程发送SIGIO信号
  2. if (fcntl(fd, F_SETSIG, SIGIO) == -1)
  3. goto fail;

设置了当设备资源可用时,向进程发送SIGIO信号,虽然这是默认发送的信号,但是为了使用信号的更多信息(主要是发送信号的原因,或者说是具体资源的情况),需要显式地进行这一步操作 。

代码第56行和第59行对应步骤(4)——设置文件的FASYNC标志,使能异步通知机制(相当于中断使能位)

  1. //打开异步通知机制
  2. if ((flag = fcntl(fd, F_GETFL)) == -1)
  3. goto fail;
  4. if (fcntl(fd, F_SETFL, flag | FASYNC) == -1)
  5. goto fail;

首先获取了文件的标志,然后再添加FASYNC标志,这就打开了异步通知的机制。

之后主函数初始化后一直休眠,等待驱动发来的信号。当进程收到驱动发来的信号后,注册的信号处理函数 sigio_handler自动被调用,函数的第一个参数是信号值,第二个参数是信号的附带信息(ID号、发送时间等)si_band成员将记录资源是可读还是可写,从而进行操作。

补充异步通知方面的内核代码
fs/fcnl.c

  1. static int setfl(int fd, struct file * filp, unsigned long arg)
  2. {
  3. ........
  4. if (((arg ^ filp->f_flags) & FASYNC) && filp->f_op->fasync) {
  5. error = filp->f_op->fasync(fd, filp, (arg & FASYNC) != 0);
  6. ........
  7. }
  8. static long do_fcntl(int fd, unsigned int cmd, unsigned long arg,
  9. struct file *filp)
  10. {
  11. long err = -EINVAL;
  12. switch (cmd) {
  13. .......
  14. case F_SETFL:
  15. err = setfl(fd, filp, arg);
  16. break;
  17. ......
  18. }
  19. .......
  20. SYSCALL_DEFINE3(fcntl, unsigned int, fd, unsigned int, cmd, unsigned long, arg)
  21. {
  22. .......
  23. err = do_fcntl(fd, cmd, arg, f.file);
  24. .......
  25. }
  26. .......
  27. static int fasync_add_entry(int fd, struct file *filp, struct fasync_struct **fapp)
  28. {
  29. struct fasync_struct *new;
  30. new = fasync_alloc();
  31. if (!new)
  32. return -ENOMEM;
  33. /*
  34. * fasync_insert_entry() returns the old (update) entry if
  35. * it existed.
  36. *
  37. * So free the (unused) new entry and return 0 to let the
  38. * caller know that we didn't add any new fasync entries.
  39. */
  40. if (fasync_insert_entry(fd, filp, fapp, new)) {
  41. fasync_free(new);
  42. return 0;
  43. }
  44. return 1;
  45. }
  46. .......
  47. int fasync_helper(int fd, struct file * filp, int on, struct fasync_struct **fapp)
  48. {
  49. if (!on)
  50. return fasync_remove_entry(filp, fapp);
  51. return fasync_add_entry(fd, filp, fapp);
  52. }
  53. .......
  54. static void kill_fasync_rcu(struct fasync_struct *fa, int sig, int band)
  55. {
  56. while (fa) {
  57. struct fown_struct *fown;
  58. unsigned long flags;
  59. if (fa->magic != FASYNC_MAGIC) {
  60. printk(KERN_ERR "kill_fasync: bad magic number in "
  61. "fasync_struct!\n");
  62. return;
  63. }
  64. spin_lock_irqsave(&fa->fa_lock, flags);
  65. if (fa->fa_file) {
  66. fown = &fa->fa_file->f_owner;
  67. /* Don't send SIGURG to processes which have not set a
  68. queued signum: SIGURG has its own default signalling
  69. mechanism. */
  70. if (!(sig == SIGURG && fown->signum == 0))
  71. send_sigio(fown, fa->fa_fd, band);
  72. }
  73. spin_unlock_irqrestore(&fa->fa_lock, flags);
  74. fa = rcu_dereference(fa->fa_next);
  75. }
  76. }
  77. void kill_fasync(struct fasync_struct **fp, int sig, int band)
  78. {
  79. /* First a quick test without locking: usually
  80. * the list is empty.
  81. */
  82. if (*fp) {
  83. rcu_read_lock();
  84. kill_fasync_rcu(rcu_dereference(*fp), sig, band);
  85. rcu_read_unlock();
  86. }
  87. }

分析:

代码

  1. SYSCALL_DEFINE3(fcntl, unsigned int, fd, unsigned int, cmd, unsigned long, arg)

是fcntl系统调用对应的代码,它调用了do_fcntl来完成具体的操作。

代码

  1. case F_SETFL:
  2. err = setfl(fd, filp, arg);
  3. break;

代码判断如果是F_ SETFL 则调用setfl 函数,setfl 会调用驱动代码中的fasync接口函数(代码第68行)error = filp->f_op->fasync(fd, filp, (arg & FASYNC) != 0);

,并传递FASYNC标志是否被设置。驱动中的fasync接口函数会调用fasync_ helper 函数(代码第680行)
int fasync_helper(int fd, struct file * filp, int on, struct fasync_struct **fapp)

, fasync_helper 函数根据FASYNC标志是否设置来决定在链表中添加一个struct fasync_ struct 节点还是删除一个节点,而这个结构中最主要的成员就是fa_ file, 它是一个打开文件的结构,还包含了进程信息(前面设置的文件属主)。当资源可用时,驱动调用kill fasync函数发送信号,该函数会遍历struct fasync_struct 链表,从而找到所有要接收信号的进程,并调用send_sigio 依次发送信号(代码第692行至第714行)。如下:

  1. static void kill_fasync_rcu(struct fasync_struct *fa, int sig, int band)
  2. {
  3. while (fa) {
  4. struct fown_struct *fown;
  5. unsigned long flags;
  6. if (fa->magic != FASYNC_MAGIC) {
  7. printk(KERN_ERR "kill_fasync: bad magic number in "
  8. "fasync_struct!\n");
  9. return;
  10. }
  11. spin_lock_irqsave(&fa->fa_lock, flags);
  12. if (fa->fa_file) {
  13. fown = &fa->fa_file->f_owner;
  14. /* Don't send SIGURG to processes which have not set a
  15. queued signum: SIGURG has its own default signalling
  16. mechanism. */
  17. if (!(sig == SIGURG && fown->signum == 0))
  18. send_sigio(fown, fa->fa_fd, band);
  19. }
  20. spin_unlock_irqrestore(&fa->fa_lock, flags);
  21. fa = rcu_dereference(fa->fa_next);
  22. }
  23. }

从上面了解后,总结出驱动代码的完成以下几个操作:

  1. 构造struct fasync_ struct 链表的头。
  2. 实现fasync接口函数,调用fasync_ helper 函数来构造struct fasyne_struct 节点,并加入到链表。
  3. 在资源可用时,调用kill fasync发送信号,并设置资源的可用类型是可读还是可写。
  4. 在文件最后一次关闭时,即在release接口中,需要显式调用驱动实现的fasyne接口函数,将节点从链表中删除,这样进程就不会再收到信号。

实现了众多接口的虛拟串口驱动的完整代码如下:
看顶头博客

分析:
代码第30行定义了链表的指针,实现了步骤(1);代码第170行至第173行和代码第185行,实现了步骤(2); 代码第66行和第90行发送信号,实现了步骤(3),注意此时资源状态为POLL_ IN和POLL_OUT, 在信号发送函数中会转换成POLLIN和POLLOUT; 代码第45行完成了步骤(4)。

编译和测试的命令如下:

注意:在编译应用程序时,需要在命令行中加入-D_GNU_SOURCE来定义 GNU_SOURCE, 因为F SETSIG不是POSIX标准。

这里就是gcc -o test test.c -D_GNU_SOURCE

相关文章