目录
- 2、多路复用
- 2.1 函数select相关
- 2.1.1 应用层select()
- 2.1.2 FD_ZERO
- 2.1.3 FD_SET
- 2.1.4 FD_ISSET
- 2.2 函数poll相关
- 2.2.1 poll函数
- 2.3 驱动层 函数
- 2.4 实例
接上篇,继续内核 I/O的五种模式的解读。
2、多路复用
select,poll,epoll都是IO多路复用的机制。I/O多路复用就是通过一种机制,一个进程可以监视多个描述符,一旦某个描述符就绪(一般是读就绪或者写就绪),能够通知程序进行相应的读写操作。
2.1 函数select相关
2.1.1 应用层select()
select函数是Linux中的一个非常重要的I/O多路复用函数。它可以同时监听多个文件描述符,一旦其中的一个文件描述符就绪(有数据 可以读、可以写、或者有except状况),它就会返回,这样程序就可以马上读取数据,或者写入数据。
select函数的原型是:
#include <sys/select.h>
int select(int n, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
参数解释:
- n:监听的文件描述符最大值+1
- readfds:待读文件描述符集合
- writefds:待写文件描述符集合
- exceptfds:等待异常文件描述符集合
- timeout:超时时间,NULL表示无限等待
返回值:
-
成功返回就绪文件描述符数
-
超时返回0
-
错误返回-1
select返回后会把以前加入的但并无事件发生的fd清空
与select相关的其他函数有: -
FD_ZERO():清零文件描述符集合
-
FD_SET():在文件描述符集合中添加一个新的文件描述符
-
FD_CLR():从文件描述符集合中删除一个文件描述符
-
FD_ISSET():检查文件描述符集合中是否包含某个文件描述符
举个简单的例子:
int main()
{
int sockfd1, sockfd2;
fd_set rfds, wfds;
// 创建文件描述符集合
FD_ZERO(&rfds);
FD_ZERO(&wfds);
// 添加sockfd1到读就绪集合,sockfd2到写就绪集合
FD_SET(sockfd1, &rfds);
FD_SET(sockfd2, &wfds);
// 监听文件描述符集合,超时时间为5秒
int ret = select(MAX_FD + 1, &rfds, &wfds, NULL, &tv);
// 检查返回值,处理就绪的文件描述符
if (ret > 0) {
if (FD_ISSET(sockfd1, &rfds)) {
// sockfd1可读,进行读取
}
if (FD_ISSET(sockfd2, &wfds)) {
// sockfd2可写,进行写入
}
}
}
select()系统调用作为一个重要的I/O多路复用接口,它可以同时监听多个文件描述符,并在任何一个文件描述符就绪时立即返回,这避免了无谓的轮询和资源浪费。
它常用于服务器程序中,监听多个客户端Socket,一旦任何一个Socket就绪,即刻处理,提高程序的运行效率。
2.1.2 FD_ZERO
FD_ZERO()函数用于清空一个文件描述符集合。
文件描述符集合用来存储多个文件描述符,以方便select()等函数监听和操作这组文件描述符。文件描述符集合由fd_set类型表示,其定义如下:
typedef struct _fd_set {
unsigned int fds_bits[FD_SETSIZE / ULONG_BIT];
} fd_set;
FD_SETSIZE是一个默认的大小,通常为1024。所以fd_set包含1024/32=32个unsigned int。
每个unsigned int都包含32个bit,代表32个文件描述符。所以一共可以存储32 * 32 = 1024个文件描述符。
当我们要监听某个文件描述符时,需要先将其添加到fd_set中,这时使用FD_SET()函数。而当不再监听某个文件描述符时,需要从fd_set中删除,这时使用FD_CLR()函数。
FD_ZERO()函数就是用来将fd_set中的所有bit设置为0,表示清空集合,不监听任何文件描述符。
所以,当我们要重新监听一组文件描述符时,最好先调用FD_ZERO()清空原来的集合,然后再调用FD_SET()添加需要监听的文件描述符。这样可以避免残留不需要监听的文件描述符。
一个例子:
c
fd_set rfds;
// 清空文件描述符集合
FD_ZERO(&rfds);
// 添加文件描述符0和1
FD_SET(0, &rfds);
FD_SET(1, &rfds);
// 使用select监听rfds
select(2, &rfds, NULL, NULL, NULL);
// 检查是否有就绪文件描述符
if (FD_ISSET(0, &rfds)) {
// 文件描述符0就绪
}
if (FD_ISSET(1, &rfds)) {
// 文件描述符1就绪
}
2.1.3 FD_SET
FD_SET()函数用于在文件描述符集合中添加一个文件描述符。
函数原型为:
void FD_SET(int fd, fd_set *set);
- fd: 要添加的文件描述符
- set: 文件描述符集合
添加成功后,set中的对应bit会被设置为1,表示正在监听该文件描述符。
例如,如果要添加文件描述符31 , set的内部结构大概如下:
元素 | 31 | 30 | 29 | 28 | 27 | 26 | 25 | 24 | 23 | 22 | 21 | 20 | 19 | 18 | 17 | 16 | 15 | 14 | 13 | 12 | 11 | 10 | 9 | 8 | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
fds_bits[0] | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
<从左到右从低位到高位, 第3个bit设置为1,代表文件描述符3
那么如果文件描述符大于32,就需要设置set的更高位,比如 文件描述符65,所以65描述符实际为第66位(因为从0开始算第一位)
66=2*32+2
元素 | 31 | 30 | 29 | 28 | 27 | 26 | 25 | 24 | 23 | 22 | 21 | 20 | 19 | 18 | 17 | 16 | 15 | 14 | 13 | 12 | 11 | 10 | 9 | 8 | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
fds_bits[0] | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
fds_bits[1] | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
fds_bits[2] | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 0 |
…
所以通过FD_SET()我们可以很方便地在文件描述符集合中添加和删除文件描述符,以便select()和其他函数进行监听和操作。
一个例子:
fd_set rfds;
int fd1 = 3, fd2 = 65;
// 清空文件描述符集合
FD_ZERO(&rfds);
// 添加文件描述符3和65
FD_SET(fd1, &rfds);
FD_SET(fd2, &rfds);
// 使用select监听rfds
select(66, &rfds, NULL, NULL, NULL);
// 检查是否有就绪文件描述符
if (FD_ISSET(fd1, &rfds)) {
// 文件描述符3就绪
}
if (FD_ISSET(fd2, &rfds)) {
// 文件描述符65就绪
}
2.1.4 FD_ISSET
FD_ISSET()函数的源码在/usr/include/sys/select.h中,Linux内核版本4.14定义如下:
/* According to earlier standards */
static inline int FD_ISSET(int fd, fd_set *fdset)
{
return (fdset->fds_bits[fd / FD_SETSIZE/ULONG_BIT] &
(1UL << (fd % FD_SETSIZE/ULONG_BIT))) != 0;
}
这个实现比较简单,主要分两步:
- 计算fd对应在fdset的哪个unsigned long中,用fd / FD_SETSIZE/ULONG_BIT。
FD_SETSIZE通常为1024,ULONG_BIT为32(32位系统下unsigned long的bit数),
所以fd / FD_SETSIZE/ULONG_BIT的结果会是0-31,表示fdset->fds_bits的索引。 - 在得到的unsigned long中,计算fd对应哪一位,用fd % FD_SETSIZE/ULONG_BIT。
然后检查那一位是否为1,使用"&"和"1UL << "操作。
如果是1,则返回1,表示fd在fdset中;如果是0,则返回0,表示fd不在fdset中。
举个例子:
- 检查文件描述符5:
fd / FD_SETSIZE/ULONG_BIT = 5 / 32 = 0 // 对应fds_bits[0]
fd % FD_SETSIZE/ULONG_BIT = 5 % 32 = 5 // 在fds_bits[0]中对应第5位
fds_bits[0] & (1UL << 5) // 检查第5位是否为1
如果第5位为1,则返回1,否则返回0 - 检查文件描述符65:
fd / FD_SETSIZE/ULONG_BIT = 65 / 32 = 2 // 对应fds_bits[2]
fd % FD_SETSIZE/ULONG_BIT = 65 % 32 = 1 // 在fds_bits[2]中对应第1位
fds_bits[2] & (1UL << 1) // 检查第1位是否为1
如果第1位为1,则返回1,否则返回0
所以这段实现代码简单地将fd映射到unsigned long的某一位上,然后检查那一位是否置1,以判断fd是否在指定的fdset中。
2.2 函数poll相关
2.2.1 poll函数
头文件
#include <poll.h>
功能
poll() 系统调用用于监视文件描述符的活动状况,它可以监视多个文件描述符,当某个文件描述符就绪时,它能够通知调用者哪个文件描述符就绪了。
poll() 的原型是:
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
- fds: 是一个结构体数组,每个元素包含一个要监视的文件描述符和监视事件。
- nfds: 表示fds数组的大小。
- timeout: 超时时间,以毫秒为单位。如果timeout为-1,则poll()会一直阻塞直到有描述符就绪;如果timeout为0,则poll()会立即返回,不管描述符是否就绪。
struct pollfd 结构如下:
struct pollfd {
int fd; /* 文件描述符 */
short events; /* 监视事件 */
short revents; /* 返回事件 */
};
其中的, events 用于指定要监视的事件, - revents 返回时,设置发生的具体事件。详细见下:
详细
****
返回值:
- 该函数成功调用时,poll()返回结构体中revents域不为0的文件描述符个数;
- 如果在超时前没有任何事件发生,poll()返回0;
- 失败时,poll()返回-1,并设置errno为下列值之一:
EBADF 一个或多个结构体中指定的文件描述符无效。
EFAULTfds 指针指向的地址超出进程的地址空间。
EINTR 请求的事件之前产生一个信号,调用可以重新发起。
EINVALnfds 参数超出PLIMIT_NOFILE值。
ENOMEM 可用内存不足,无法完成请求。
使用示例:
#include <poll.h>
int main() {
struct pollfd fds[2];
int nfds = 2;
int timeout = 5000; // 5s 超时
fds[0].fd = STDIN_FILENO; //标准输入
fds[0].events = POLLIN;
fds[1].fd = socket_fd; //socket
fds[1].events = POLLIN;
int ret = poll(fds, nfds, timeout);
if (ret == 0) {
printf("poll timeout!\n");
} else if (ret > 0) {
if (fds[0].revents & POLLIN) {
// 标准输入有数据可读
}
if (fds[1].revents & POLLIN) {
// socket 有数据可读
}
} else {
perror("poll");
}
}
2.3 驱动层 函数
对应于应用层的系统调用 select 、poll、epoll 这两个函数,对应驱动层的内核调用都是xxx_poll()函数,该函数的声明已在struct file_operations 结构体中定义了。
因此,驱动层的xxx_poll协助这些多路监控函数判断本设备是否有数据可读写,需要由开发者完成。这些操作实质是一些程式化的内容。具体如下:
.poll原函数原形模板:
unsigned int (*poll) (struct file *, struct poll_table_struct *);
开发者需要在这个函数内完成三件事:
static unsigned int char_poll(struct file *file, poll_table *wait)
{
1. 调用poll_wait() 注册一个waitqueue,以便内核可以唤醒该文件在poll()调用中睡眠的进程。
2. 检查设备状态,设置mask来表示该文件可进行的IO事件。如可读、可写等。
并设置mask,判断是否可读,如可读则mask |= POLLIN | POLLRDNORM;
判断是否可写,如可写则mask |= POLLOUT | POLLWRNORM;
3、 返回mask值,poll()调用会根据这个mask值来判断哪些文件描述符就绪。 return mask;
}
其典型模板如下:
当内核需要唤醒在该设备文件上睡眠的应用进程时,会调用wake_up_poll() 来唤醒进程,这时应用进程从poll()系统调用返回,并可以进行相应的IO操作。
所以XX_poll方法为基于设备文件的poll实现提供了一机制,让设备与内核的poll机制结合起来,实现设备状态的监听与唤醒。
poll方法实现:
例如:实现一个环形缓冲区,当用户空间进程调用poll时,内核需要根据环形缓冲区的状态来唤醒进程。
首先定义环形缓冲区结构:
struct circ_buf {
char *buf;
int head;
int tail;
int size;
};
字符设备结构体:
struct char_dev {
struct circ_buf circ_buf;
wait_queue_head_t waitq; // 等待队列
bool written;
};
在open方法中申请 buffer:
static int char_open(struct inode *inode, struct file *file)
{
struct char_dev *dev;
dev = kmalloc(sizeof(*dev), GFP_KERNEL);
dev->circ_buf.buf = kmalloc(SIZE, GFP_KERNEL);
dev->circ_buf.head = 0;
dev->circ_buf.tail = 0;
dev->circ_buf.size = SIZE;
dev->written = false;
init_waitqueue_head(&dev->waitq);
file->private_data = dev;
return 0;
}
写入方法中向环形缓冲区写入数据,并唤醒等待的进程:
static ssize_t char_write(struct file *file, const char __user *buf, size_t count, loff_t *ppos)
{
struct char_dev *dev = file->private_data;
int len = circ_buf_write(&dev->circ_buf, buf, count);
dev->written = true;
wake_up_interruptible(&dev->waitq);
return len;
}
最后是关键的poll
static unsigned int char_poll(struct file *file, poll_table *wait)
{
struct char_dev *dev = file->private_data;
unsigned int mask = 0;
poll_wait(file, &dev->waitq, wait); /
if (dev->written) // 如果有数据写入
mask |= POLLIN | POLLRDNORM; // 表示可读
if (!circ_buf_full(&dev->circ_buf)) // 如果接收缓冲区没满
mask |= POLLOUT | POLLWRNORM; // 表示可写
return mask;
}
当用户调用poll系统调用时,内核会调用char_poll方法。char_poll函数这里调用了poll_wait(),将当前进程加入dev->waitq等待队列,并标记为可以被IO唤醒。
然后如果没有就绪事件,进程将在char_poll中睡眠。这时,如果有写入操作(这个写入操作就是上面的write函数内的wake_up_interreupible())在dev->waitq上调用了wake_up操作,内核会唤醒在该等待队列上睡眠的进程, char_poll也会返回,poll()调用也会返回。
被唤醒后,char_poll继续运行,接下来根据环形缓冲区的状态来设置mask, 如果此时 written 为true(在write函数中设置),表示有数据可读,mask被设置为POLLIN,这时应用层poll系统调用会退出阻塞直接返回。
关联函数poll_wait
poll_wait() 函数是设备驱动中实现poll方法时常用的函数。它的作用是:
- 将当前进程加入一个等待队列中。
- 标记当前进程正在睡眠,并可以被IO唤醒。
- 向内核注册一个唤醒源,一旦该唤醒源(等待队列)被唤醒,内核会唤醒等待队列上睡眠的进程。
它的原型是:
void poll_wait(struct file * filp, wait_queue_head_t * wait_address, poll_table *p)
- filp: 文件结构,表示当前正在调用的文件。
- wait_address: 等待队列头,当前进程将加入这个等待队列中。
- p: poll_table结构,该结构用于标记进程可以被IO唤醒。
当一个进程调用poll_wait()时,即表示它正在该(设备?)文件上睡眠,并可以被IO唤醒。这时内核会将其加入等待队列wait_address中,并标记为可被IO唤醒状态。
一旦有其他操作在该等待队列上调用wake_up()等唤醒函数(如上个示例中的write函数中的wake_up_interruptible(&dev->waitq);),内核会唤醒waiting队列上的睡眠进程,被唤醒的进程将从原来的睡眠态(如调用poll()进入的睡眠)中返回,这时(对应的应用层)poll()调用也会返回。
所以,通过调用poll_wait(),我们可以很方便的把进程睡眠状态与设备等待队列关联起来,这是实现设备的poll方法的基础。
源码追踪:
/include/linux/poll.h
typedef struct poll_table_struct {
poll_queue_proc _qproc;
unsigned long _key;
} poll_table;
static inline void poll_wait(struct file * filp, wait_queue_head_t * wait_address, poll_table *p)
{
if (p && p->_qproc && wait_address)
p->_qproc(filp, wait_address, p);
}
poll_wait参数poll_table是来自于系统调用->poll时传递进来的。这样,就要去找select函数如何把这个poll_table构造的。见下面这段代码
void poll_initwait(struct poll_wqueues *pwq)
{
init_poll_funcptr(&pwq->pt, __pollwait);
pwq->polling_task = current;
pwq->triggered = 0;
pwq->error = 0;
pwq->table = NULL;
pwq->inline_index = 0;
}
EXPORT_SYMBOL(poll_initwait);
这个函数把__pollwait赋给了上面提到wait这个poll_table变量,所以实际就是__pollwait才是poll_wait()函数的本体。
fs/select.c
/* Add a new entry */
static void __pollwait(struct file *filp, wait_queue_head_t *wait_address,
poll_table *p)
{
struct poll_wqueues *pwq = container_of(p, struct poll_wqueues, pt);
struct poll_table_entry *entry = poll_get_entry(pwq);
if (!entry)
return;
entry->filp = get_file(filp);
entry->wait_address = wait_address;
entry->key = p->_key;
init_waitqueue_func_entry(&entry->wait, pollwake);
entry->wait.private = pwq;
add_wait_queue(wait_address, &entry->wait);
}
这段代码做了以下几件事:
- 通过container_of()得到poll_wqueues结构体pwq,该结构体包含了poll_table以及相关等待队列信息。
- 调用poll_get_entry()获取一个poll_table_entry结构体entry。如果获取失败,直接返回。
- 设置entry->filp为当前文件filp,entry->wait_address为等待队列头wait_address,entry->key为poll_table的_key。
- 调用init_waitqueue_func_entry()初始化entry->wait。并设置回调函数为pollwake(),private数据为pwq。
- 调用add_wait_queue()将entry->wait加入到等待队列wait_address中。
- 此时,当前进程会在poll()的睡眠中等待唤醒。如果在超时时间内没有唤醒,会进入真正的阻塞状态。
- 如果wait_address等待队列被唤醒,则会调用entry->wait.func,也就是pollwake()函数。
- pollwake()函数会获取私有数据pwq,并标记pwq->triggered值为true,表示有就绪事件。这会导致当前进程从睡眠中唤醒,并从poll()系统调用返回。
所以,这个__pollwait()实现主要做了两件事:
- 将当前进程加入等待队列wait_address,同时设置回调函数pollwake()。
- pollwake()函数用于在有就绪事件时唤醒等待队列上的进程,它会标记pwq->triggered来通知__pollwait()已经有就绪事件,从而避免进程不必要的等待。
所以总的来说是用于在就绪事件来临 唤醒进程,以实现poll()的监听功能。
2.4 实例
/*************************************************************************
> File Name:block-memory-1.c
驱动程序根据应用层的flag标志决定是否采用阻塞或非阻塞的工作方式
本例用的是add_wait_queue函数
************************************************************************/
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/cdev.h>
#include <linux/fs.h>
#include <linux/slab.h>
#include <linux/uaccess.h>
#include <linux/wait.h>
#include <linux/poll.h>
/*1、定义重要的变量及结构体*/
#define MEM_SIZE 500 //每个虚拟设备内存大小
#define DEV_NUM 3 //创建的设备总个数
struct mem_dev_t{
struct cdev my_dev; //cdev设备描述结构体变量
char mem[MEM_SIZE]; //fifo内存池,当成虚拟设备
int curpos; //内存当前数据最后位置指示,从0开始记
struct semaphore sem; //信号量
wait_queue_head_t write_queue; //写等待队列
wait_queue_head_t read_queue; //读等待队列
};
struct mem_dev_t *mem_dev;
/*所有驱动函数声明*/
loff_t llseek (struct file *, loff_t, int);
ssize_t read (struct file *, char __user *, size_t, loff_t *);
ssize_t write (struct file *, const char __user *, size_t, loff_t *);
ssize_t aio_read (struct kiocb *, const struct iovec *, unsigned long, loff_t);
ssize_t aio_write (struct kiocb *, const struct iovec *, unsigned long, loff_t);
int iterate (struct file *, struct dir_context *);
unsigned int poll (struct file *, struct poll_table_struct *);
long unlocked_ioctl (struct file *, unsigned int, unsigned long);
long compat_ioctl (struct file *, unsigned int, unsigned long);
int mmap (struct file *, struct vm_area_struct *);
int open (struct inode *, struct file *);
int flush (struct file *, fl_owner_t id);
int release (struct inode *, struct file *);
int fsync (struct file *, loff_t, loff_t, int datasync);
int aio_fsync (struct kiocb *, int datasync);
int fasync (int, struct file *, int);
int lock (struct file *, int, struct file_lock *);
ssize_t sendpage (struct file *, struct page *, int, size_t, loff_t *, int);
unsigned long get_unmapped_area(struct file *, unsigned long, unsigned long, unsigned long, unsigned long);
int check_flags(int);
int flock (struct file *, int, struct file_lock *);
ssize_t splice_write(struct pipe_inode_info *, struct file *, loff_t *, size_t, unsigned int);
ssize_t splice_read(struct file *, loff_t *, struct pipe_inode_info *, size_t, unsigned int);
int setlease(struct file *, long, struct file_lock **);
long fallocate(struct file *file, int mode, loff_t offset, loff_t len);
int show_fdinfo(struct seq_file *m, struct file *f);
//驱动操作函数结构体,成员函数为需要实现的设备操作函数指针
//简单版的模版里,只写了open与release两个操作函数。
struct file_operations fops={
.open = open,
.release = release,
.read = read,
.write = write,
.poll = poll,
};
/*3、初始化 cdev结构体,并将cdev结构体与file_operations结构体关联起来*/
/*这样在内核中就有了设备描述的结构体cdev,以及设备操作函数的调用集合file_operations结构体*/
static int cdev_setup(struct mem_dev_t *mem_dev , dev_t devno ){
int unsucc =0;
cdev_init(&mem_dev->my_dev , &fops);
mem_dev->my_dev.owner = THIS_MODULE;
/*4、注册cdev结构体到内核链表中*/
unsucc = cdev_add(&mem_dev->my_dev , devno , 1);
if (unsucc){
printk("driver : cdev add faild \n");
return -1;
}
sema_init( &mem_dev->sem,1); //初始化信号量,为1
mem_dev->curpos = 0; //初始化缓冲数据位置为0
init_waitqueue_head(&mem_dev->write_queue);
init_waitqueue_head(&mem_dev->read_queue);
return 0;
}
static int __init my_init(void){
int major , minor;
dev_t devno;
int unsucc =0;
int i=0;
mem_dev = kzalloc(sizeof(struct mem_dev_t)*DEV_NUM , GFP_KERNEL);
if (!mem_dev){
printk(" driver : allocating memory is failed");
return -1;
}
/*2、创建 devno */
unsucc = alloc_chrdev_region(&devno , 0 , DEV_NUM , "select_memory");
if (unsucc){
printk(" driver : creating devno is failed\n");
return -1;
}else{
major = MAJOR(devno);
minor = MINOR(devno);
printk("diver : major = %d ; minor = %d\n",major,minor);
}
/*3、 初始化cdev结构体,并联cdev结构体与file_operations.*/
/*4、注册cdev结构体到内核链表中*/
for (i=0;i<DEV_NUM;i++){
devno = MKDEV(major , i);
if (cdev_setup(mem_dev+i , devno) == 0){
printk("deiver : the driver select_memory[%d] initalization completed\n", i);
} else
printk("deiver : the driver select_memory[%d] initalization failed\n", i);
}
return 0;
}
static void __exit my_exit(void)
{
int i=0;
dev_t devno;
devno = mem_dev->my_dev.dev;
for (i=0 ; i<DEV_NUM ; i++){
cdev_del(&(mem_dev+i)->my_dev);
}
unregister_chrdev_region(devno , DEV_NUM);
printk("***************the driver operate_memory exit************\n");
}
/*5、驱动函数的实现*/
/*file_operations结构全成员函数.open的具体实现*/
int open(struct inode *pnode , struct file *pf){
int minor = MINOR(pnode->i_rdev);
int major = MAJOR(pnode->i_rdev);
struct mem_dev_t *p = container_of(pnode->i_cdev , struct mem_dev_t , my_dev);
pf->private_data = p; //把全局变量指针放入到struct file结构体里
if (pf->f_flags & O_NONBLOCK){ //非阻塞
printk("driver : select_memory[%d , %d] is opened by nonblock mode\n",major , minor);
}else{
printk("driver : select_memory[%d , %d] is opened by block mode\n",major,minor);
}
return 0;
}
/*file_operations结构全成员函数.release的具体实现*/
int release(struct inode *pnode , struct file *pf){
printk("select_memory is closed \n");
return 0;
}
/*file_operations结构全成员函数.read的具体实现*/
ssize_t read (struct file * pf, char __user * buf, size_t size , loff_t * ppos){
//本例中,因为是fifo,所以ppos参数不用。
struct mem_dev_t *pdev = pf->private_data;
int count = 0; //存储读到多少数据
int ret = 0;
/*******************************************************/
DECLARE_WAITQUEUE(wait , current); //定义等待队列项目元素
/*******************************************************/
down(&pdev->sem);
/*******************************************************/
add_wait_queue(&pdev->read_queue , &wait); //把元素加入读等待队列
/*******************************************************/
while (pdev->curpos == 0){
if ((pf->f_flags & O_NONBLOCK) == 0){
//当前没有数据,进入阻塞睡眠
/*******************************************************/
set_current_state(TASK_INTERRUPTIBLE); //设置当前进程为可中断睡眠态
up(&pdev->sem); //退出前释放信号量,V操作
schedule(); //调度程序
/*******************************************************/
}else{
ret = 0;
goto out;
}
down(&pdev->sem);
}
if (size > pdev->curpos){
count = pdev->curpos;
}else{
count = size;
}
//copy_from_user返回值大于0失败
if ( copy_to_user(buf , &pdev->mem , count )){ //读取失败
ret = 0;
goto out;
}else{ //成功读取
memcpy(&pdev->mem , &pdev->mem[count] , pdev->curpos-count);
pdev->curpos -= count;
up(&pdev->sem); //退出前释放信号量,V操作
/*******************************************************/
wake_up_interruptible(&pdev->write_queue); //唤醒可能睡眠的write
/*******************************************************/
ret = count;
}
out:
up(&pdev->sem); //退出前释放信号量,V操作
/********************************************************/
remove_wait_queue(&pdev->read_queue , &wait);
set_current_state(TASK_RUNNING);
/*******************************************************/
return ret;
}
/*file_operations结构全成员函数.write的具体实现*/
ssize_t write (struct file * pf, const char __user *buf, size_t size , loff_t *ppos){
struct mem_dev_t *pdev = pf -> private_data;
int count =0;
int ret = 0;
/*******************************************************/
DECLARE_WAITQUEUE(wait , current); //定义等待队列项目元素
/*******************************************************/
down(&pdev->sem);
/*******************************************************/
add_wait_queue(&pdev->write_queue , &wait); //把元素加入读等待队列
/*******************************************************/
while (pdev->curpos == (MEM_SIZE -1)){
if ((pf->f_flags & O_NONBLOCK) == 0){
/*******************************************************/
set_current_state(TASK_INTERRUPTIBLE);
up(&pdev->sem);
schedule();
/*******************************************************/
}else{
ret = 0;
goto out;
}
down(&pdev->sem);
}
if (size > (MEM_SIZE-pdev->curpos)){
count = MEM_SIZE-pdev->curpos;
}else{
count = size;
}
if (copy_from_user(&pdev->mem[pdev->curpos],buf,count)){
ret = 0;
goto out;
}else{
pdev->curpos +=count;
/*******************************************************/
wake_up_interruptible(&pdev->read_queue);
/*******************************************************/
ret = count;
}
out:
up(&pdev->sem);
/*******************************************************/
remove_wait_queue(&pdev->write_queue, &wait);
set_current_state(TASK_RUNNING);
/*******************************************************/
return ret;
}
unsigned int poll (struct file *pf, struct poll_table_struct * pts){
struct mem_dev_t *p = pf->private_data;
unsigned int mark = 0;
poll_wait(pf, &p->read_queue , pts);
poll_wait(pf , &p->write_queue , pts);
//测试这里是否是阻塞还是轮询
//printk("poll in waiting......or .....poll.");
//
if (p->curpos > 0){
mark |= POLLIN | POLLRDNORM;
}
if (p->curpos < MEM_SIZE-1){
mark |= POLLOUT | POLLWRNORM;
}
return mark;
}
module_init(my_init);
module_exit(my_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("");
测试程序
/*************************************************************************
> File Name: op_mem.c
************************************************************************/
#include<stdio.h>
#include <string.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/time.h>
#include <sys/select.h>
#include <errno.h>
#include <sys/ioctl.h>
#include <sys/stat.h>
int main(int argc , char **argv){
int fd = 0;
int size = 0;
char buf[20] = {0};
char * mesg ="this is test\n";
int pos = 0;
fd_set rfd;
int ret=0;
int j=0;
if (argc < 2){
printf("argument is less!\n");
return 0;
}
fd = open(argv[1] , O_RDWR|O_APPEND );
if (fd < 0){
perror("open ");
}
//从设备循环读出数据
while (1){
j++;
printf("while times is %d:\n" , j);
FD_ZERO(&rfd);
FD_SET(fd,&rfd);
ret = select(fd+1 , &rfd,NULL,NULL,NULL);
if (ret<0){
if(errno == EINTR){
continue;
}else{
printf("select error \n");
break;
}
}else{
if (FD_ISSET(fd,&rfd)){
read(fd, buf, 10);
printf("buf = %s \n", buf);
}
}
memcpy(buf,"\0\0\0\0\0\0\0\0\0\0",10);
}
close(fd);
return 0;
}
测试方法:
1、在一个终端窗口将驱动加载 sudo insmod select-memory.ko
2、查看主设备号 cat /proc/devices ,找到select-memory设备可以看到主设备号
3、创建设备文件 sudo mknod /dev/select0 c 主设备号 0
4、修改设备文件权限 sudo chmod 777 /dev/select0
5、加载后运行测试程序: ./op_mem.elf ,这时可以看到设试程序会阻塞在当前。
6、打开另一个终端窗口,输出 : echo “hello” > /dev/select0
7、这时在原窗口会看到有 buf = hello 字样的输出,并继续阻塞。