一个字符设备的主要功能是用来实现I/O操作,反映到应用程序中就是进行读/写等相关的操作。在对一个设备进行读写操作时,由于设备在实际的操作中响应速速各不相同,因此数据并不总是在任何时候都可用:
此时对读写操作来说,要么放弃等待直到返回一个错误码给上层,要么让发起读写操作的进程进入等待状态直到数据可用为止。
根据不用的需求和使用场景,Linux内核支持几种不同的I/O操作模式,称为字符设备的I/O模型,该模型根据同步与异步、阻塞与非阻塞分为四大来。
休眠的意义?
如何将进程安全的进入休眠状态?
在Linux设备驱动中,可以使用等待队列(wait queue)来实现阻塞进程的休眠和唤醒。等待队列以进程调度紧密结合,能够用于实现内核中的异步事件通知机制。Linux提供了有关等待队列的操作:
1) 定义等待队列头
wait_queue_head_t my_queue; //定义等待队列头
2) 初始化队列头
init_waitqueue_head(&my_queue); //初始化队列头 如果觉得上边两步来的麻烦,可以直接使用DECLARE_WAIT_QUEUE_HEAD(name,tsk)
3) DECLARE_WAITQUEUE(name,tsk); //定义并初始化等待队列
4) 添加/移除等待队列
5) 等待事件
6) 唤醒
7)睡眠
sleep_on作用是把目前进程的状态置成TASK_UNINTERRUPTIBLE,并定义一个等待队列,之后把它附属到等待队列头q,直到资源可用,q引导的等待队列被唤醒。interruptible_sleep_on作用和sleep_on是一样的, 只不过它把进程状态置为TASK_INTERRUPTIBLE。
这两个函数的流程:首先,定义并初始化等待队列,把进程的状态置成TASK_UNINTERRUPTIBLE或TASK_INTERRUPTIBLE,并将对待队列添加到等待队列头;然后通过schedule放弃CPU,调度其他进程执行;最后,当进程被其他地方唤醒,将等待队列移除等待队列头。
在Linux内核中,使用set_current_state()和__add_current_state()函数来实现目前进程状态的改变,使用current->state = TASK_UNINTERRUPTIBLE类似的语句也是可以的。
因此我们有时也可能在许多驱动中看到,它并不调用sleep_on或interruptible_sleep_on(),而是亲自进行进程的状态改变和切换。
我们通过一个例子来讲解同步阻塞型I/O:
struct xxx_pipe{
wait_queue_head_t inq,outq; /*读取和写入队列*/
char *buffer,*end; /*缓冲区的起始和结尾*/
int buffersize; /*用于指针计算*/
char *rp,*wp; /*读取和写入的位置*/
int nreaders,nwriters; /*用于读写打开的数量*/
struct fasync_struct *async_queue; /*异步读取者*/
struct semaphore sem; /*互斥信号量*/
struct cdev cdev; /*字符设备结构*/
};
static ssize_t xxx_read(struct file *filp,char __user *buf,
size_t count,loff_t *f_pos)
{
struct xxx_pipe *dev=filp->private_data;
if(down_interruptible(&dev->sem))
return -ERESTARTSYS;
while(dev->rp == dev->wp){ /*无数据可读取*/
up(&dev->sem); /*释放锁*/
if(filp->f-flags & O_NONBLOCK)
return -EAGAIN;
if(wait_event_interruptible(dev->inq,(dev->rp!=dev->wp))
return -ERESTARTSYS;/*信号,通知fs层做相应处理*/
/*先获取锁*/
if(down_interruptible(&dev->sem))
return -ERESTARTSYS;
} /*数据已就绪,返回*/
if(dev->wp > dev->rp)
count=min(count,(size_t)(dev->wp - dev->rp));
else/*写入指针回卷,返回数据直到dev->end*/
count=min(count,(size_t)(dev->end – dev->rp));
if(copy_to_user(buf,dev->rp,count)){
up(&dev->sem);
return -EFAULT;
}
dev->rp += count:
if(dev->rp == dev->end)
dev->rp = dev->buffer;/*回卷*/
up(&dev->sem);
/*最后,唤醒所有写入者并返回*/
wake_up_interruptible(&dev->outq);
return count;
}
使用异步阻塞I/O的应用程序通常会使用select()或poll()系统调用查询是否可对设备进行无阻塞的访问,这两个系统调用最终又会引发设备驱动中的poll()函数被执行,所以我们的问题就集中到了如何编写设备驱动中的poll()函数就可以了。
先来看看设备驱动中的poll()函数原型:
unsigned int (*poll)(struct file *filp, struct poll_table *wait)
这个函数要进行下面两项工作。首先,对可能引起设备文件状态变化的等待队列调用poll_wait(),将当前进程添加到自己管理的等待队列中。然后,返回表示是否能对设备进行无阻塞读写访问的掩码。
在上面提到了一个poll_wait()函数,它的原型:
void poll_wait(struct file *filp, wait_queue_head_t *queue, poll_table *wait)
它的作用就是把当前进程添加到queue参数指定的等待队列中。需要注意的是这个函数是不会引起阻塞的。
经过以上驱动程序的poll()函数应该返回设备资源的可获取状态,即POLLIN、POLLOUT、POLLPRI、POLLERR、POLLNVAL等宏的位“或”结果。每个宏的含义都表示设备的一种状态,如
驱动程序poll函数的典型模板
static unsigned int xxx_poll(struct file *filp,poll_table *wait)
{
struct xxx_pipe *dev = filp->private_data;
unsigned int mask=0;
down(&dev->sem);
poll_wait(filp,&dev->inq,wait);
poll_wait(filp,&dev->outq,wait);
if(read_buffer_not_empty) //如果接收buffer不为空,可读
mask |= POLLIN | POLLRDNORM; /*可读取*/
if(write_buffer_not_full) //如果写buffer不满,可写
mask |= POLLOUT | POLLWRNORM; /*可写入*/
up(&dev->sem);
return mask;
}
在用户程序中,select()和poll()本质上是一样的, 不同只是引入的方式不同,前者是在BSD UNIX中引入的,后者是在System V中引入的。用的比较广泛的是select系统调用。原型如下:
int select(int numfds, fd_set *readfds, fd_set *writefds, fd_set *exceptionfds, struct timeval *timeout);
其中readfs,writefds,exceptfds分别是select()监视的读,写和异常处理的文件描述符集合,numfds的值是需要检查的号码最高的文件描述符加1,timeout则是一个时间上限值,超过该值后,即使仍没有描述符准备好也会返回。
struct timeval {
int tv_sec; //秒
int tv_usec; //微秒
}
涉及到文件描述符集合的操作主要有以下几种:
1)清除一个文件描述符集 FD_ZERO(fd_set *set);
2)将一个文件描述符加入文件描述符集中 FD_SET(int fd,fd_set *set);
3)将一个文件描述符从文件描述符集中清除 FD_CLR(int fd,fd_set *set);
4)判断文件描述符是否被置位 FD_ISSET(int fd,fd_set *set);
最后我们利用上面的文件描述符集的相关来写个验证添加了设备轮询的驱动,把上边两块联系起来:
int fd;
fd_set rfds,wfds;//读/写文件描述符集
/*以非阻塞方式打开/dev/xxx设备文件*/
fd = open(“/dev/xxx”, O_RDWR | O_NONBLOCK);
FD_ZERO(&rfds);
FD_ZERO(&wfds);
FD_SET(fd, &rfds);
FD_SET(fd, &wfds);
select(fd + 1, &rfds, &wfds, NULL, NULL);
/*数据可获得*/
if(FD_ISSET(fd, &rfds))
{
//读数据
}
if(FD_ISSET(fd, &wfds))
{
//写数据
}
异步通知:很简单,一旦设备准备好,就主动通知应用程序,这种情况下应用程序就不需要查询设备状态,这和硬件上常提的“中断”的概念相似。准确的说法其实应该叫做“信号驱动的异步I/O”,信号是在软件层次上对中断机制的一种模拟。
在应用程序中,为了捕获信号可以使用signal()函数来设置对应的信号的处理函数。函数原型是
void (*signal(int signo,void (*func)(int))) (int)
该函数原型可分解为:
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum,sighandler_t handler);
第一个参数指定信号的值,第二个参数指定信号值的处理函数;若为SIG_IGN,则表示忽略该信号;若为SIG_DFL,则表示采用系统默认方式处理该信号;若为用户自定义的函数,则信号捕获后调用该函数。
在进程执行时,按下“Ctrl+C”将向其发出SIGINT信号;kill正在运行的进程将向其发出SIGTERM信号。
为了一个用户在用户空间中能处理一个设备释放的信号,它必须完成一下3份工作:
1)通过F_SETOWN控制指令设置设备文件的拥有者为本进程,这样从设备驱动中发出的信号才能被本进程收到。
2)通过F_SETFLIO控制命令设置设备文件支持FASYNC,即异步通知模式。
3)通过signal()链接信号和信号处理函数。
void sigterm_handler(int signo)
{
char data[MAX_LEN];
int len;
len=read(STDIN_FILENO, &data,MAX_LEN);
data[len]=0;
printf("Input available:%s\n",data);
exit(0);
}
int main(void)
{
int oflags;
//启动信号驱动机制
signal(SIGIO, sigterm_handler);
fcntl(STDIN_FILENO, F_SETOWN, getpid());
oflags = fcntl(STDIN_FILENO, F_GETFL);
fcntl(STDIN_FILENO, F_SETFL, oflags | FASYNC);
//建立一个死循环,防止程序结束
while(1);
return 0;
}
有了信号的发送,那么就一定得有信号的释放。 在设备驱动和应用程序的异步通知交互中,仅仅在应用程序端捕获信号是不够的,因为信号的源头是在驱动端,因此要在适当的时机让设备驱动释放信号。 为了使设备支持异步通知机制,驱动程序中涉及三个操作:
设备驱动中异步通知编程还是比较简单的,主要就是一个据结构,和两个函数:
数据结构:fasync_struct结构体
fasync_struct
struct fasync_struct {
int magic;
int fa_fd;
struct fasync_struct *fa_next;
struct file *fa_file;
};
设备驱动中异步通知编程还是比较简单的,主要就是一个数据结构,和两个函数: 函数:
1)处理FASYNC标志变更的函数int fasync_helper(int fd, struct file *filp, int mode ,struct fasync_struct **fa);
2)释放信号用的函数void kill_fasync(struct fasync_struct **fa, int sig, int band);
和其他设备驱动一样,一般将fasync_struct放到设备结构体中。 下边是典型模版:
struct xxx_dev
{
struct cdev cdev;
...
struct fasync_struct *async_queue; //异步结构体
}
而在驱动的fasync()函数中,只需要简单的将该参数的3个参数以及fasync_struct结构体指针的指针作为第4个参数传给fasync_helper函数即可。下边是典型模版:
static int xxx_fasync(int fd, struct file *filp, int mode)
{
struct xxx_dev *dev = filp->private_data;
return fasync_helper(fd,filp,mode,&dev->async_queue);
}
一旦设备资源可以获得时,应该调用kill_fasync()释放SIGIO信号,可读时第三个参数设置为POLL_IN,可写时第三个参数设置为POLL_OUT,下面是释放信号的典型模版:
static ssize_t xxx_write(struct file *filp, const char __user *buf, size_t count,loff_t *f_ops)
{
struct xxx_dev *dev = filp->private_data;
....
//产生异步信号
if(dev->async_queue)
{
kill_fasync(&dev->async_queue, SIGIO, POLL_IN);
} ..
}
最后,在文件关闭时,即在设备驱动的release函数中,应调用设备驱动的fasync()函数将文件从异步通知的列表中删除,下边是设备驱动的释放函数的典型模版:
static int xxx_release(struct inode *inode, struct file *filp)
{
struct xxx_dev *dev = filp->private_data;
//将文件从异步通知列表中删除
xxx_fasync(-1,filp,0); ... return 0;
}
所谓AIO就是Asynchronous Input/Output异步输入/输出,基本思想是允许进程发起很多的I/O操作,而不用阻塞或等待任何操作的完成,稍后或在接收到I/O操作完成的通知时,进程就可以检索I/O操作的结果。 AIO机制为服务器端高并发应用程序提供了一种性能优化的手段。加大了系统吞吐量。在异步非阻塞IO中,可以同时发起多个传输操作。这需要每个操作都有一个唯一的上下文,这样才能在它们完成时区分到底是哪个传输操作完成了。在AIO中,通过aiocb(AIO IO control Block)结构体进行区分,这个结构体如下:
struct aiocb
{
int aio_fildes; /* File descriptor */
off_t aio_offset; /* File offset */
volatile void * aio_buf; /* Location of buffer */
size_t aio_nbytes; /* Length of transfer */
int aio_reqprio; /* Request priority offset */
struct sigevent aio_sigevent; /* Signal number and value */
int aio_lio_opcode; /* Operation to be performed */
};
从上面的结构体,我们可以看到,这个结构体包含了有关传输的所有信息,包括数据准备的用户缓冲区。在产生IO通知时,aiocb结构就被用来唯一标识所完成的IO操作。
AIO系列API中主要有下边几个函数:
1.int aio_read(struct aiocb *aiocbp)
该函数请求对一个有效的文件描述符进行异步读操作。在请求进行排队之后会立即返回,如果执行成功,返回值就为0,错误则返回-1并设置errno的值。
2.int aio_write(struct aiocb *aiocbp)
该函数请求一个异步写操作,它会立即返回说明请求已经进行排队,成功返回0,失败返回为-1,并设置相应的error值。
3.int aio_error(struct aiocb *aiocbp)
该函数用来确定请求的状态,可以返回EINPROGRESS(说明请求尚未完成),ECANCELLED(请求被应用程序取消了),-1(说明发生了错误,具体错误原因由error记录)。
4.ssize_t aio_return(struct aiocb *aiocbp)
由于并没有阻塞在read调用上,所以我们不能立即返回这个函数的返回状态,这是就要使用这个函数了,需要注意的是只有在aio_error调用确定请求已经完成(可能已经完成,也可能发生了错误)之后,才能调用这个函数,这个函数的返回值就相当于同步情况下read或write系统调用的返回值(所传输的字节数,如果发生错误,则返回-1)。
5.int aio_suspend(const struct aiocb *const cblist[], int n ,const struct timespec *timeout)
用户可以通过这个函数来来挂起(或阻塞)调用进程,直到异步请求完成为止,此时会产生一个信号,或者发生其他超时操作。调用者提供了一个aiocb引用列表,其中任何一个完成都会导致给函数返回。
6.int aio_cancel(int fd ,struct aiocb *aiocbp) 该函数允许用户取消对某个文件描述符执行的一个或所有的IO请求。如果要取消一个请求,用户需提供文件描述符和aiocb引用,如果这个请求被成功取消了,则返回AIO_CANCELED,如果该请求完成了,返回AIO_NOTCANCELED。 如果要取消对某个给定文件描述符的所有请求,用户需要提供这个文件的描述符以及一个aiocbp的NULL引用,如果所有请求被成功取消了,则返回AIO_CANCELED ,只要至少有一个没被取消,这个函数就返回AIO_NOT_CANCELED.如果没有一个请求可以被取消,该函数就会返回AIO_ALLDONE。然后,可以使用aio_error来验证每个AIO请求,如果某个请求已经被返回了,那么aio_error就返回-1,并且error会被设置为ECANCELED.
7.int lio_listio(int mode ,struct aiocb *list[], int nent ,struct sigevent *sig) 这个操作使得用户可以在一个系统调用(一次内核上下文切换中启动大量的I/O操作)。其中,mode参数可以是LIO_WAIT或LIO_NOWAIT,前者会阻塞这个调用,直到所有的IO都完成为止,在操作进行排队之后,LIO_NOWAIT就会返回,list是一个aiocb引用的列表,最大元素的个数有nent定义的。如果list的元素为NULL,lio_listio()将被忽略。
8. 设置AIO的通知机制,有两种通知机制:信号和回调
struct sigevent
{
int sigev_notify; //notification type
int sigev_signo; //signal number
union sigval sigev_value; //signal value
void (*sigev_notify_function)(union sigval);
pthread_attr_t *sigev_notify_attributes;
}
sigev_notify的取值:
(1).信号机制 首先我们应该捕获SIGIO信号,对其作处理:
struct sigaction sig_act;
sigempty(&sig_act.sa_mask);
sig_act.sa_flags = SA_SIGINFO;
sig_act.sa_sigaction = aio_handler;
struct aiocb myaiocb;
bzero( (char *)&myaiocb, sizeof(struct aiocb) );
myaiocb.aio_fildes = fd;
myaiocb.aio_buf = malloc(BUF_SIZE+1);
myaiocb.aio_nbytes = BUF_SIZE;
myaiocb.aio_offset = next_offset;
myaiocb.aio_sigevent.sigev_notify = SIGEV_SIGNAL;
myaiocb.aio_sigevent.sigev_signo = SIGIO;
myaiocb.aio_sigevent.sigev_value.sival_ptr = &myaiocb;
ret = sigaction( SIGIO, &sig_act, NULL );
void aio_handler( int signo, siginfo_t *info, void *context )
{
struct aiocb *req;
if (info->si_signo == SIGIO) {
req = (struct aiocb *)info->si_value.sival_ptr;
if (aio_error( req ) == 0) {
ret = aio_return( req );
}
}
return;
}
(2). 回调机制
需要设置:
myaiocb.aio_sigevent.sigev_notify = SIGEV_THREAD
my_aiocb.aio_sigevent.notify_function = aio_handler;
回调函数的原型:
typedef void (* FUNC_CALLBACK)(sigval_t sigval);