阻塞 *** 作是指,在执行设备 *** 作时,若不能获得资源,则进程挂起直到满足可 *** 作的条件再进行 *** 作。非阻塞 *** 作的进程在不能进行设备 *** 作时,并不挂起。被挂起的进程进入sleep状态,被从调度器的运行队列移走,直到等待的条件被满足。
在Linux驱动程序中,我们可以使用等待队列(wait queue)来实现阻塞 *** 作。wait queue很早就作为一个基本的功能单位出现在Linux内核里了,它以队列为基础数据结构,与进程调度机制紧密结合,能够用于实现核心的异步事件通知机制。等待队列可以用来同步对系统资源的访问。
下面定义设备"globalvar",它可以被多个进程打开,但是每次只有当一个进程写入了一个数据之后本进程或其它进程才可以读取该数据,否则一直阻塞。
#include <linux/module.h>#include <linux/init.h>#include <linux/fs.h>#include <asm/uaccess.h>#include <linux/wait.h>#include <asm/semaphore.h>MODULE_LICENSE("GPL")#define MAJOR_NUM 254static ssize_t globalvar_read(struct file *, char *, size_t, loff_t*)static ssize_t globalvar_write(struct file *, const char *, size_t, loff_t*)struct file_operations globalvar_fops ={ read: globalvar_read, write: globalvar_write,}static int global_var = 0static struct semaphore semstatic wait_queue_head_t outqstatic int flag = 0static int __init globalvar_init(void){ int retret = register_chrdev(MAJOR_NUM, "globalvar", &globalvar_fops)if (ret) { printk("globalvar register failure")} else { printk("globalvar register success") init_MUTEX(&sem) init_waitqueue_head(&outq)} return ret}static void __exit globalvar_exit(void){ int retret = unregister_chrdev(MAJOR_NUM, "globalvar")if (ret) { printk("globalvar unregister failure")} else { printk("globalvar unregister success")}}static ssize_t globalvar_read(struct file *filp, char *buf, size_t len, loff_t *off){ //等待数据可获得 if (wait_event_interruptible(outq, flag != 0)) { return - ERESTARTSYS} if (down_interruptible(&sem)) { return - ERESTARTSYS} flag = 0if (copy_to_user(buf, &global_var, sizeof(int))) { up(&sem) return - EFAULT} up(&sem)return sizeof(int)} static ssize_t globalvar_write(struct file *filp, const char *buf, size_t len,loff_t *off) { if (down_interruptible(&sem)) { return - ERESTARTSYS } if (copy_from_user(&global_var, buf, sizeof(int))) { up(&sem) return - EFAULT} up(&sem)flag = 1//通知数据可获得 wake_up_interruptible(&outq)return sizeof(int)} module_init(globalvar_init)module_exit(globalvar_exit)
编写两个用户态的程序来测试,第一个用于阻塞地读/dev/globalvar,另一个用于写/dev/globalvar。只有当后一个对/dev/globalvar进行了输入之后,前者的read才能返回。
读的程序为:
#include <sys/types.h>#include <sys/stat.h>#include <stdio.h>#include <fcntl.h>main(){ int fd, numfd = open("/dev/globalvar", O_RDWR, S_IRUSR | S_IWUSR)if (fd != - 1) { while (1) {read(fd, &num, sizeof(int))//程序将阻塞在此语句,除非有针对globalvar的输入 printf("The globalvar is %d\n", num) //如果输入是0,则退出 if (num == 0) {close(fd) break } } } else { printf("device open failure\n")}}
写的程序为:
#include <sys/types.h>#include <sys/stat.h>#include <stdio.h>#include <fcntl.h>main(){ int fd, numfd = open("/dev/globalvar", O_RDWR, S_IRUSR | S_IWUSR)if (fd != - 1) { while (1) { printf("Please input the globalvar:\n") scanf("%d", &num) write(fd, &num, sizeof(int)) //如果输入0,退出 if (num == 0) {close(fd) break } } } else { printf("device open failure\n")} }
打开两个终端,分别运行上述两个应用程序,发现当在第二个终端中没有输入数据时,第一个终端没有输出(阻塞),每当我们在第二个终端中给globalvar输入一个值,第一个终端就会输出这个值。
关于上述例程,我们补充说一点,如果将驱动程序中的read函数改为:
static ssize_t globalvar_read(struct file *filp, char *buf, size_t len, loff_t *off){ //获取信号量:可能阻塞 if (down_interruptible(&sem)) { return - ERESTARTSYS } //等待数据可获得:可能阻塞 if (wait_event_interruptible(outq, flag != 0)) { return - ERESTARTSYS} flag = 0//临界资源访问 if (copy_to_user(buf, &global_var, sizeof(int))) { up(&sem) return - EFAULT} //释放信号量 up(&sem)return sizeof(int)}
即交换wait_event_interruptible(outq, flag != 0)和down_interruptible(&sem)的顺序,这个驱动程序将变得不可运行。实际上,当两个可能要阻塞的事件同时出现时,即两个wait_event或down摆在一起的时候,将变得非常危险,死锁的可能性很大,这个时候我们要特别留意它们的出现顺序。当然,我们应该尽可能地避免这种情况的发生!
还有一个与设备阻塞与非阻塞访问息息相关的论题,即select和poll, select和 poll的本质一样,前者在BSD Unix中引入,后者在System V中引入。poll和select用于查询设备的状态,以便用户程序获知是否能对设备进行非阻塞的访问,它们都需要设备驱动程序中的poll函数支持。
驱动程序中poll函数中最主要用到的一个API是poll_wait,其原型如下:
void poll_wait(struct file *filp, wait_queue_heat_t *queue, poll_table * wait)
poll_wait函数所做的工作是把当前进程添加到wait参数指定的等待列表(poll_table)中。下面我们给globalvar的驱动添加一个poll函数:
static unsigned int globalvar_poll(struct file *filp, poll_table *wait){ unsigned int mask = 0poll_wait(filp, &outq, wait) //数据是否可获得? if (flag != 0) { mask |= POLLIN | POLLRDNORM//标示数据可获得 } return mask}
需要说明的是,poll_wait函数并不阻塞,程序中poll_wait (filp, &outq, wait)这句话的意思并不是说一直等待outq信号量可获得,真正的阻塞动作是上层的select/poll函数中完成的。select/poll会在一个循环中对每个需要监听的设备调用它们自己的poll支持函数以使得当前进程被加入各个设备的等待列表。若当前没有任何被监听的设备就绪,则内核进行调度(调用schedule)让出cpu进入阻塞状态,schedule返回时将再次循环检测是否有 *** 作可以进行,如此反复;否则,若有任意一个设备就绪, select/poll都立即返回。
我们编写一个用户态应用程序来测试改写后的驱动。程序中要用到BSD Unix中引入的select函数,其原型为:
int select(int numfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout)
其中readfds、writefds、exceptfds分别是被select ()监视的读、写和异常处理的文件描述符集合,numfds的值是需要检查的号码最高的文件描述符加1。timeout参数是一个指向struct timeval类型的指针,它可以使select()在等待timeout时间后若没有文件描述符准备好则返回。struct timeval数据结构为:
struct timeval { int tv_sec int tv_usec}
除此之外,我们还将使用下列API:
FD_ZERO(fd_set *set)――清除一个文件描述符集;
FD_SET(int fd,fd_set *set)――将一个文件描述符加入文件描述符集中FD_CLR(int fd,fd_set *set)――将一个文件描述符从文件描述符集中清除;
FD_ISSET(int fd,fd_set *set)――判断文件描述符是否被置位。
下面的用户态测试程序等待/dev/globalvar可读,但是设置了5秒的等待超时,若超过5秒仍然没有数据可读,则输出"No data within 5 seconds":
#include <sys/types.h>#include <sys/stat.h>#include <stdio.h>#include <fcntl.h>#include <sys/time.h>#include <sys/types.h>#include <unistd.h>main(){ int fd, numfd_set rfdsstruct timeval tvfd = open("/dev/globalvar", O_RDWR, S_IRUSR | S_IWUSR)if (fd != - 1) { while (1) { //查看globalvar是否有输入 FD_ZERO(&rfds) FD_SET(fd, &rfds) //设置超时时间为5s tv.tv_sec = 5 tv.tv_usec = 0 select(fd + 1, &rfds, NULL, NULL, &tv) //数据是否可获得? if (FD_ISSET(fd, &rfds)){read(fd, &num, sizeof(int)) printf("The globalvar is %d\n", num) //输入为0,退出if (num == 0) { close(fd) break}}else printf("No data within 5 seconds.\n") } } else { printf("device open failure\n") } }
开两个终端,分别运行程序:一个对globalvar进行写,一个用上述程序对 globalvar进行读。当我们在写终端给globalvar输入一个值后,读终端立即就能输出该值,当我们连续5秒没有输入时,"No data within 5 seconds"在读终端被输出。
当一个任务(进程)执行系统调用而执行内核代码时,称进程处于内核内核态,此时处理器处于特权级最高的(0级)内核代码中执行,当进程处于内核态时,执行的内核代码会使用当前进程的内核栈,每个进程都有自己的内核栈。当进程执行用户代码时,称其处于用户态,此时处理器在特权级最低的(3级)用户代码中运行。当正在执行用户程序而突然被中断程序中断时,此时用户程序也可以象征性地称为处于进程的内核态,因为中断处理程序将使用当前进程的内核栈。这与处于内核态的进程的状态有些类似。内核态与用户态是 *** 作系统的两种运行级别,跟intel cpu没有必然的联系,intel cpu提供Ring0-Ring3三种级别的运行模式,Ring0级别最高,Ring3最低。
Linux使用了Ring3级别运行用户态,Ring0作为内核态,没有使用Ring1和Ring2。Ring3状态不能访问Ring0的地址空间,包括代码和数据。Linux进程的4GB地址空间,3G-4G部分大家是共享的,是内核态的地址空间,这里存放在整个内核的代码和所有的内核模块,以及内核所维护的数据。用户运行一个程序,该程序所创建的进程开始是运行在用户态的,如果要执行文件 *** 作,网络数据发送等 *** 作,必须通过write,send等系统调用,这些系统调用会调用内核中的代码来完成 *** 作,这时,必须切换到Ring0,然后进入3GB-4GB中的内核地址空间去执行这些代码完成 *** 作,完成后,切换回Ring3,回到用户态。这样,用户态的程序就不能随意 *** 作内核地址空间,具有一定的安全保护作用。
保护模式,通过内存页表 *** 作等机制,保证进程间的地址空间不会互相冲突,一个进程的 *** 作不会修改另一个进程的地址空间中的数据。在内核态下,CPU可执行任何指令,在用户态下CPU只能执行非特权指令。当CPU处于内核态,可以随意进入用户态;而当CPU处于用户态,只能通过中断的方式进入内核态。一般程序一开始都是运行于用户态,当程序需要使用系统资源时,就必须通过调用软中断进入内核态.
使用nm查看用户态程序的符号表内容
使用System.map(内核符号表)查看内核符号表内容
1. 测试程序中打印用户态函数地址,并调用系统调用(在内核中打印系统调用函数地址),用"用户态符号表"和"内核态符号表"示例说明内核态和用户态地址空间的差异
2. 说明内核态地址映射ioremap();用户态地址映射mmap()
用户态与内核态本质区别是cpu当前的cpl是3或者0。内核态程序可以访问内核空间与用户空间的数据与代码,可以执行一些特权指令(in out sti cli...),用户态程序只能访问用户空间的数据与代码。欢迎分享,转载请注明来源:内存溢出
评论列表(0条)