内核IO 阻塞/非阻塞、等待队列、同步/异步--随记

阻塞/非阻塞IO

**阻塞IO:**执行IO操作时,如果有数据返回数据,没有数据就休眠等待数据然后返回。这里的执行IO操作是用户态去对内核态做IO操作,数据返回是指用户态使用系统调用函数去访问内核时的数据拷贝,休眠是指内核中利用等待队列实现的进程休眠
**非阻塞IO:**执行IO操作时,如果有数据返回数据,没有数据返回0。就是用户态访问内核态在操作数据时发现内核还没有准备好数据,那这个用户进程就取做其他的事情,等一会再来看数据是否准备好,好了就做数据拷贝的工作
在创建fd时,设置flag是O_NONBLOCK来设置fd是否阻塞

等待队列

等待队列在内核中是一个数据结构,当一个进程在等待一个外部事件发生时,它将从运行队列中移除并放到等待队列中

struct wait_queue_head {
	spinlock_t		lock;				//自旋锁 对等待队列做互斥访问
	struct list_head	head;				//内核的一个双循环列表
};
typedef struct wait_queue_head wait_queue_head_t;

wait_queue_t: 等待队列的节点 [等待实体]
wait_queue_head_t: 等待队列的头节点

将进程加入等待队列步骤:
1、声明wait_queue_head_t和wait_queue_t
wait_queue_head_t声明:
void init_waitqueue_head(wait_queue_head_t *queue);[at runtime]
DECLARE_WAIT_QUEUE_HEAD(queue); (at compile time)
wait _queue_t声明:
DECLARE_WAITQUEUE(name, tsk)or DEFINE_WAIT(name)
init_waitqueue_entry (wait_queue_t *q, struct task_struct *p)
2、将wait_queue_t加入到wait_queue_head_t中:
void add_wait_queue(wait_queue_head_t *q, wait_queue_t * wait)
void add_wait_queue_exclusive(wait_queue_head_t *q, wait_queue_t * wait)
3、设置进程状态为TASK_INTERRUPTIBLE 或者 TASK_UNINTERRUPTIBLE:
set_current_state(TASK_INTERRUPTIBLE);
4、测试条件,如果条件为假,调用schedule();进入休眠状态,wait
5、从schedule();返回,设置进程状态为TASK_RUNNING
6、当外部事件发生时,从等待队列移除:
remove_wait_queue(wait_queue_head_t *q, wait_queue_t * wait);

/*
	 * Reading returns if the pretimeout has gone off, and it only does
	 * it once per pretimeout.
	 */
	spin_lock_irq(&ipmi_read_lock);
	if (!data_to_read) {
		if (file->f_flags & O_NONBLOCK) {
			rv = -EAGAIN;
			goto out;
		}

		init_waitqueue_entry(&wait, current);
		add_wait_queue(&read_q, &wait);
		while (!data_to_read) {
			set_current_state(TASK_INTERRUPTIBLE);
			spin_unlock_irq(&ipmi_read_lock);
			schedule();
			spin_lock_irq(&ipmi_read_lock);
		}
		remove_wait_queue(&read_q, &wait);

		if (signal_pending(current)) {
			rv = -ERESTARTSYS;
			goto out;
		}
	}
	data_to_read = 0;

 out:
	spin_unlock_irq(&ipmi_read_lock);

使用wait_event系列函数操作:
1、A 定义 wait_queue_head_t
#define DECLARE_WAIT_QUEUE_HEAD(name)
wait_queue_head_t name = __WAIT_QUEUE_HEAD_INITIALIZER(name)

DECLARE_WAIT_QUEUE_HEAD(myqueue);

2、调用wait_event系列函数休眠:
wait_event_timeout(wq, condition, timeout)
condition: 等待条件 [C表达式]
timeout: 超时时间 [jiffes 为单位]
wq: 等待队列头
返回值:
0 超时
>0 剩余的超时时间 等待条件成立
wait_event(wq, condition)
condition: 等待条件 [C表达式]
wait_event_interruptible(wq, condition);
condition: 等待条件 [C表达式]
返回值:
-ERESTARTSYS 被信号打断
0 等待条件成立
wait_event_interruptible_timeout(wq, condition, timeout)
返回值:
-ERESTARTSYS 被信号打断
0 超时
>0 剩余的超时时间 等待条件成立
3、唤醒,使用wake_up系列函数:
唤醒普通的等待实体
#define wake_up(x) __wake_up(x, TASK_NORMAL, 1, NULL)
#define wake_up_nr(x, nr) __wake_up(x, TASK_NORMAL, nr, NULL)
#define wake_up_all(x) __wake_up(x, TASK_NORMAL, 0, NULL)
#define wake_up_locked(x) __wake_up_locked((x), TASK_NORMAL)
唤醒可中断的等待实体
#define wake_up_interruptible(x) __wake_up(x, TASK_INTERRUPTIBLE, 1, NULL)
#define wake_up_interruptible_nr(x, nr) __wake_up(x, TASK_INTERRUPTIBLE, nr, NULL)
#define wake_up_interruptible_all(x) __wake_up(x, TASK_INTERRUPTIBLE, 0, NULL)
#define wake_up_interruptible_sync(x) __wake_up_sync((x), TASK_INTERRUPTIBLE, 1)
主要是看在加入等待队列时设置的任务状态,设置了TASK_INTERRUPTIBLE,则唤醒时用wake_up_interruptible系列宏。而wake_up系列宏则会把设置了TASK_INTERRUPTIBLE 和TASK_UNINTERRUPTIBLE的任务唤醒

轮询、多路复用

多路复用:同时监控多个fd的变化,通过select、poll、epoll这些系统调用来对单线程做IO复用来处理多个请求
select、poll、epoll本质上有相同的功能:每个都允许一个进程来决定它是否可读或者可写一个或者多个文件而不阻塞
其实select(或pselect)、poll(或epoll、ppoll)都是通过poll系统调用进入内核的,所以需要在驱动程序中实现poll函数
1、select:轮询的方式,从多个文件描述符中获取状态变化后的情况
int select( int nfds, fd_set *readfds,fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);

nfds: 要检测的文件描述符的范围,为文件最大描述符+1
readfds: 包含所有可能因状态变成可读而触发select函数返回的文件描述符
writefds: 包含所有可能因状态变成可写而触发select函数返回的文件描述符
exceptfds: 包含所有可能因状态发生异常而触发select函数返回的文件描述符

fd_set :表示文件描述符集合的一个结构体
void FD_CLR(int fd, fd_set *set); //把文件描述符fd, 从集合里清掉。
int FD_ISSET(int fd, fd_set *set); //判断一个文件描述符fd, 是否在集合set里
void FD_SET(int fd, fd_set *set); //把文件描述符fd,加到集合set中去
void FD_ZERO(fd_set *set); //把文件描述符集合set清空

timeout: 超时时间。函数返回时这个变量为剩余的时间

当超时或者一个或多个文件描述符状态发生变化时返回

2、poll:等待一个或多个文件描述符的状态变化
int poll(struct pollfd *fds,  nfds_t nfds,  int timeout);
fds:指向要监听的fd对应的结构体struct pollfd数组
nfds:struct pollfd结构的数目,就是要监听的fd的数目

struct pollfd {
int fd ; /* file descriptor /
short events; /
requested events /
short revents; /
returned events*/
};
要监听的事件(events)和返回的事件(revents),是由一些位掩码组成的

总结:
前面说过,无论是select还是epoll,都是通过调用poll进内核的。所以要想实现 多路复用, 驱动中都是是要实现poll的
其实就是用一个文件句柄handle多个socket,如果这个socket已经准备好数据,那就调用读写函数来操作数据。调用 select/poll时,会去轮询所有在监听的 fd,如果这个fd对应的数据ok了,就放到fs_set中 ,最后返回这些fd,将 他们从内核空间 拷贝到用户空间,用户就拿到了这些fd,对每个fd,用户需要抵用recvfrom这些读写函数去将数据从内核空间拷贝到用户空间 。
poll方法:
1、unsigned int (poll) (struct filefilp, poll_table *wait);
2、无论何时,用户空间 程序进行一个poll、select或者 epoll系统 调用,涉及一个和驱动 相关的文件描述符时,这个驱动方法 被调用
poll实现:
1、在一个或多个可指示查询状态变化 的等待队列上调用 poll_wait,如果没有文件描述符可用作I/O,内核使这个进程在等待队列上等待所有的传递给系统调用的文件描述符
2、void poll_wait (struct file *, wait_queue_head_t *, poll_table *);
第二参数是驱动中定义的等待队列指针,其他参数和poll一样

驱动中调用poll_wait是不会阻塞的,真正的 轮询是通过虚拟文件系统层的sys_poll实现的
流程:(linux/fs/select.c)
1、poll -->sys_poll–>do_sys_poll–>poll_initwait。poll_initwait函数注册回调函数__pollwait,这就是驱动程序执行poll_wait时,真正被调用的函数
2、file->f_op->poll,是驱动程序自己实现的poll函数,会调用poll_wait将自己加到某个队列,这个队列也是驱动 自己定义的,还会判断设备是否就绪
3、设备未就绪,do_sys_poll里会让进程休眠一定的时间
4、进程唤醒:时间到了,或者是,被驱动程序唤醒。驱动发现条件就绪时,九就将某个队列上挂着的进程唤醒,这个队列就是前面通过poll_wait将 本进程 挂过去的队列
5、如果驱动没有唤醒 进程,那schedule_timeout(__timeout)超时后,重复2 3动作,直到应用程序的poll调用传入的时间到达

同步/异步

同步IO:
同步IO是指,用户进程会去等待把数据从内核空间 拷贝到用户空间 ,只要用户进程 需要等待拷贝,就是同步
异步IO:
调用aio_read,让内核等数据准备好,并且复制到用户进程空间后执行事先指定好的函数。就是设备数据就绪了,驱动可以发送一个信号给进程,不需要自己去轮询等待

前面说的阻塞IO、非阻塞IO、IO复用(select、poll)都属于是同步IO,因为都会有等待内核空间数据拷贝到用户空间的操作

异步通知操作:
应用程序应该做的事:
1、注册信号处理函数,通过signal/sigaction
2、使进程成为该文件的属主进程
通过fcntl的F_SETOWN命令实现,fsntl (fd, F_SETOWN, getpid());
3、启用异步通知功能
通过fcntl的F_SETFL命令设置FASYNC标志
(linux/fs/fcntl.c)

signal(SIGIO, &input_handler); /* dummy sample; sigaction() is better */
fcntl(STDIN_FILENO, F_SETOWN, getpid());
oflags = fcntl(STDIN_FILENO, F_GETFL);
fcntl(STDIN_FILENO, F_SETFL, oflags | FASYNC);

驱动应该做的事:
内核中异步通知是通过异步通知队列struct fasync_struct结构实现的,需要用到内核提供的两个异步通知队列函数就行 :
fasync_helper和kill_fasync:

fasync_helper 用于把文件指针加到(或移除,当参数on = 0)异步通知队列
/*
 * fasync_helper() is used by almost all character device drivers
 * to set up the fasync queue, and for regular files by the file
 * lease code. It returns negative on error, 0 if it did no changes
 * and positive if it added/deleted the entry.
 */
int fasync_helper(int fd, struct file * filp, int on, struct fasync_struct **fapp)
{
	if (!on)
		return fasync_remove_entry(filp, fapp);
	return fasync_add_entry(fd, filp, fapp);
}

EXPORT_SYMBOL(fasync_helper);

kill_fasync用于发送信号给应用程序 
void kill_fasync(struct fasync_struct **fp, int sig, int band)
{
	/* First a quick test without locking: usually
	 * the list is empty.
	 */
	if (*fp) {
		rcu_read_lock();
		kill_fasync_rcu(rcu_dereference(*fp), sig, band);
		rcu_read_unlock();
	}
}
EXPORT_SYMBOL(kill_fasync);

步骤:
1、实现struct file_operations的.fasync函数指针
这个函数,会在应用程序里,设置/删除FASYNC文件标记时会调用。该函数指针的原型为:
int (*fasync)(int fd, struct file *filp, int mode) ;
在函数实现时,我们只需要调用:
int fasync_helper(int fd, struct file * filp, int on, struct fasync_struct **fapp)
2、在数据就绪时,调用kill_fasync通知应用程序
3、在用户关闭文件(在realease函数里)时,把该文件指针从异步通知队列里删除。同样也是调用fasync_helper

总结:
IO分为两个阶段:
1、数据准备阶段
2、内核空间数据拷贝到用户空间进程缓冲区阶段
前面说的阻塞IO、非阻塞IO、IO复用(select、poll)都属于是同步IO,因为都会有等待内核空间数据拷贝到用户空间的操作,阶段2是阻塞的。只有异步IO是可以在阶段1和阶段2都可以做其他的事情,符合POSIX异步IO操作(POSIX(可移植操作系统接口)把同步IO操作定义为导致进程阻塞直到IO完成的操作,反之则是异步IO)。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值