设备即文件
在本实验中,为了统一地访问设备(device),我们可以把一个设备看成一个文件,通过访问文件的接口来访问设备。目前实现了 stdin 设备文件文件、stdout 设备文件、disk0 设备。stdin 设备就是键盘,stdout 设备就是控制台终端的文本显示,而 disk0 设备是承载 SFS 文件系统的磁盘设备。下面看看 ucore 是如何让用户把设备看成文件来访问。
设备的定义
为了表示一个设备,需要有对应的数据结构,ucore 为此定义了 struct device,如下:
可以认为struct device
是一个比较抽象的“设备”的定义。一个具体设备,只要实现了d_open()
打开设备, d_close()
关闭设备,d_io()
(读写该设备,write参数是true/false决定是读还是写),d_ioctl()
(input/output control)四个函数接口,就可以被文件系统使用了。
// kern/fs/devs/dev.h
/*
* Filesystem-namespace-accessible device.
* d_io is for both reads and writes; the iobuf will indicates the direction.
*/
struct device {
size_t d_blocks;
size_t d_blocksize;
int (*d_open)(struct device *dev, uint32_t open_flags);
int (*d_close)(struct device *dev);
int (*d_io)(struct device *dev, struct iobuf *iob, bool write);
int (*d_ioctl)(struct device *dev, int op, void *data);
};
#define dop_open(dev, open_flags) ((dev)->d_open(dev, open_flags))
#define dop_close(dev) ((dev)->d_close(dev))
#define dop_io(dev, iob, write) ((dev)->d_io(dev, iob, write))
#define dop_ioctl(dev, op, data) ((dev)->d_ioctl(dev, op, data))
这个数据结构能够支持对块设备(比如磁盘)、字符设备(比如键盘)的表示,完成对设备的基本操作。
但这个设备描述没有与文件系统以及表示一个文件的 inode 数据结构建立关系,为此,还需要另外一个数据结构把 device 和 inode 联通起来,这就是 vfs_dev_t 数据结构。
利用 vfs_dev_t 数据结构,就可以让文件系统通过一个链接 vfs_dev_t 结构的双向链表找到 device 对应的 inode 数据结构,一个 inode 节点的成员变量 in_type 的值是 0x1234,则此 inode 的成员变量 in_info 将成为一个 device 结构。这样 inode 就和一个设备建立了联系,这个 inode 就是一个设备文件。
// kern/fs/vfs/vfsdev.c
// device info entry in vdev_list
typedef struct {
const char *devname;
struct inode *devnode;
struct fs *fs;
bool mountable;
list_entry_t vdev_link;
} vfs_dev_t;
#define le2vdev(le, member) \
to_struct((le), vfs_dev_t, member) //为了使用链表定义的宏, 做到现在应该对它很熟悉了
static list_entry_t vdev_list; // device info list in vfs layer
static semaphore_t vdev_list_sem; // 互斥访问的semaphore
static void lock_vdev_list(void) {
down(&vdev_list_sem);
}
static void unlock_vdev_list(void) {
up(&vdev_list_sem);
}
ucore 虚拟文件系统为了把这些设备链接在一起,还定义了一个设备链表,即双向链表 vdev_list,这样通过访问此链表,可以找到 ucore 能够访问的所有设备文件。
注意这里的vdev_list
对应一个vdev_list_sem
。在文件系统中,互斥访问非常重要,所以我们将看到很多的semaphore
。
我们使用iobuf
结构体传递一个IO请求(要写入设备的数据当前所在内存的位置和长度/从设备读取的数据需要存储到的位置)
struct iobuf {
void *io_base; // the base addr of buffer (used for Rd/Wr)
off_t io_offset; // current Rd/Wr position in buffer, will have been incremented by the amount transferred
size_t io_len; // the length of buffer (used for Rd/Wr)
size_t io_resid; // current resident length need to Rd/Wr, will have been decremented by the amount transferred.
};
注意设备文件的inode也有一个inode_ops
成员, 提供设备文件应具备的接口。
// kern/fs/devs/dev.c
/*
* Function table for device inodes.
*/
static const struct inode_ops dev_node_ops = {
.vop_magic = VOP_MAGIC,
.vop_open = dev_open,
.vop_close = dev_close,
.vop_read = dev_read,
.vop_write = dev_write,
.vop_fstat = dev_fstat,
.vop_ioctl = dev_ioctl,
.vop_gettype = dev_gettype,
.vop_tryseek = dev_tryseek,
.vop_lookup = dev_lookup,
};
stdin设备
trap.c改变了对stdin
的处理, 将stdin
作为一个设备(也是一个文件), 通过sys_read()
接口读取标准输入的数据。
注意,既然我们把stdin
, stdout
看作文件, 那么也需要先打开文件,才能进行读写。在执行用户程序之前,我们先执行了umain.c
建立一个运行时环境,这里主要做的工作,就是让程序能够使用stdin
, stdout
。
// user/libs/file.c
//这是用户态程序可以使用的“系统库函数”,从文件fd读取len个字节到base这个位置。
//当fd = 0的时候,表示从stdin读取
int read(int fd, void *base, size_t len) {
return sys_read(fd, base, len);
}
// user/libs/umain.c
int main(int argc, char *argv[]);
static int initfd(int fd2, const char *path, uint32_t open_flags) {
int fd1, ret;
if ((fd1 = open(path, open_flags)) < 0) {
return fd1;
}//我们希望文件描述符是fd2, 但是分配的fd1如果不等于fd2, 就需要做一些处理
if (fd1 != fd2) {
close(fd2);
ret = dup2(fd1, fd2);//通过sys_dup让两个文件描述符指向同一个文件
close(fd1);
}
return ret;
}
void umain(int argc, char *argv[]) {
int fd;
if ((fd = initfd(0, "stdin:", O_RDONLY)) < 0) {
warn("open <stdin> failed: %e.\n", fd);
}//0用于描述stdin,这里因为第一个被打开,所以stdin会分配到0
if ((fd = initfd(1, "stdout:", O_WRONLY)) < 0) {
warn("open <stdout> failed: %e.\n", fd);
}//1用于描述stdout
int ret = main(argc, argv); //真正的“用户程序”
exit(ret);
}
这里我们需要把命令行的输入转换成一个文件,于是需要一个缓冲区:把已经在命令行输入,但还没有被读取的数据放在缓冲区里。这里遇到一个问题:每当控制台输入一个字符,我们都要及时把它放到stdin
的缓冲区里。一般来说,应当有键盘的外设中断来提醒我们。但是我们在QEMU里收不到这个中断,于是采取一个措施:借助时钟中断,每次时钟中断检查是否有新的字符,这效率比较低,不过也还可以接受。
// kern/trap/trap.c
void interrupt_handler(struct trapframe *tf) {
intptr_t cause = (tf->cause << 1) >> 1;
switch (cause) {
/*...*/
case IRQ_S_TIMER:
clock_set_next_event();
if (++ticks % TICK_NUM == 0 && current) {
// print_ticks();
current->need_resched = 1;
}
run_timer_list();
//按理说用户程序看到的stdin是“只读”的
//但是,一个文件,只往外读,不往里写,是不是会导致数据"不守恒"?
//我们在这里就是把控制台输入的数据“写到”stdin里(实际上是写到一个缓冲区里)
//这里的cons_getc()并不一定能返回一个字符,可以认为是轮询
//如果cons_getc()返回0, 那么dev_stdin_write()函数什么都不做
dev_stdin_write(cons_getc());
break;
}
}
// kern/driver/console.c
#define CONSBUFSIZE 512
static struct {
uint8_t buf[CONSBUFSIZE];
uint32_t rpos;
uint32_t wpos; //控制台的输入缓冲区是一个队列
} cons;
/* *
* cons_intr - called by device interrupt routines to feed input
* characters into the circular console input buffer.
* */
void cons_intr(int (*proc)(void)) {
int c;
while ((c = (*proc)()) != -1) {
if (c != 0) {
cons.buf[cons.wpos++] = c;
if (cons.wpos == CONSBUFSIZE) {
cons.wpos = 0;
}
}
}
}
/* serial_proc_data - get data from serial port */
int serial_proc_data(void) {
int c = sbi_console_getchar();
if (c < 0) {
return -1;
}
if (c == 127) {
c = '\b';
}
return c;
}
/* serial_intr - try to feed input characters from serial port */
void serial_intr(void) {
cons_intr(serial_proc_data);
}
/* *
* cons_getc - return the next input character from console,
* or 0 if none waiting.
* */
int cons_getc(void) {
int c = 0;
bool intr_flag;
local_intr_save(intr_flag);
{
// poll for any pending input characters,
// so that this function works even when interrupts are disabled
// (e.g., when called from the kernel monitor).
serial_intr();
// grab the next character from the input buffer.
if (cons.rpos != cons.wpos) {
c = cons.buf[cons.rpos++];
if (cons.rpos == CONSBUFSIZE) {
cons.rpos = 0;
}
}
}
local_intr_restore(intr_flag);
return c;
}
我们来看stdin
设备的实现:
// kern/fs/devs/dev_stdin.c
#define STDIN_BUFSIZE 4096
static char stdin_buffer[STDIN_BUFSIZE];
//这里又有一个stdin设备的缓冲区, 能否和之前console的缓冲区合并?
static off_t p_rpos, p_wpos;
static wait_queue_t __wait_queue, *wait_queue = &__wait_queue;
void dev_stdin_write(char c) { //把其他地方的字符写到stdin缓冲区, 准备被读取
bool intr_flag;
if (c != '\0') {
local_intr_save(intr_flag);//禁用中断
{
stdin_buffer[p_wpos % STDIN_BUFSIZE] = c;
if (p_wpos - p_rpos < STDIN_BUFSIZE) {
p_wpos ++;
}
if (!wait_queue_empty(wait_queue)) {
wakeup_queue(wait_queue, WT_KBD, 1);
//若当前有进程在等待字符输入, 则进行唤醒
}
}
local_intr_restore(intr_flag);
}
}
static int dev_stdin_read(char *buf, size_t len) { //读取len个字符
int ret = 0;
bool intr_flag;
local_intr_save(intr_flag);
{
for (; ret < len; ret ++, p_rpos ++) {
try_again:
if (p_rpos < p_wpos) {//当前队列非空
*buf ++ = stdin_buffer[p_rpos % STDIN_BUFSIZE];
}
else {
//希望读取字符, 但是当前没有字符, 那么进行等待
wait_t __wait, *wait = &__wait;
wait_current_set(wait_queue, wait, WT_KBD);
local_intr_restore(intr_flag);
schedule();
local_intr_save(intr_flag);
wait_current_del(wait_queue, wait);
if (wait->wakeup_flags == WT_KBD) {
goto try_again; //再次尝试
}
break;
}
}
}
local_intr_restore(intr_flag);
return ret;
}
static int stdin_io(struct device *dev, struct iobuf *iob, bool write) {
//对应struct device 的d_io()
if (!write) {
int ret;
if ((ret = dev_stdin_read(iob->io_base, iob->io_resid)) > 0) {
iob->io_resid -= ret;
}
return ret;
}
return -E_INVAL;
}
stdout设备
stdout
设备只需要支持写操作,调用cputchar()
把字符打印到控制台。
// kern/fs/devs/dev_stdout.c
static int stdout_io(struct device *dev, struct iobuf *iob, bool write) {
//对应struct device 的d_io()
if (write) {
char *data = iob->io_base;
for (; iob->io_resid != 0; iob->io_resid --) {
cputchar(*data ++);
}
return 0;
}
//if !write:
return -E_INVAL;//对stdout执行读操作会报错
}
disk0设备
封装了一下ramdisk
的接口,每次读取或者写入若干个block。
// kern/fs/devs/dev_disk0.c
static char *disk0_buffer;
static semaphore_t disk0_sem;
static void
lock_disk0(void) {
down(&(disk0_sem));
}
static void
unlock_disk0(void) {
up(&(disk0_sem));
}
static void disk0_read_blks_nolock(uint32_t blkno, uint32_t nblks) {
int ret;
uint32_t sectno = blkno * DISK0_BLK_NSECT, nsecs = nblks * DISK0_BLK_NSECT;
if ((ret = ide_read_secs(DISK0_DEV_NO, sectno, disk0_buffer, nsecs)) != 0) {
panic("disk0: read blkno = %d (sectno = %d), nblks = %d (nsecs = %d): 0x%08x.\n",
blkno, sectno, nblks, nsecs, ret);
}
}
static void disk0_write_blks_nolock(uint32_t blkno, uint32_t nblks) {
int ret;
uint32_t sectno = blkno * DISK0_BLK_NSECT, nsecs = nblks * DISK0_BLK_NSECT;
if ((ret = ide_write_secs(DISK0_DEV_NO, sectno, disk0_buffer, nsecs)) != 0) {
panic("disk0: write blkno = %d (sectno = %d), nblks = %d (nsecs = %d): 0x%08x.\n",
blkno, sectno, nblks, nsecs, ret);
}
}
static int disk0_io(struct device *dev, struct iobuf *iob, bool write) {
off_t offset = iob->io_offset;
size_t resid = iob->io_resid;
uint32_t blkno = offset / DISK0_BLKSIZE;
uint32_t nblks = resid / DISK0_BLKSIZE;
/* don't allow I/O that isn't block-aligned */
if ((offset % DISK0_BLKSIZE) != 0 || (resid % DISK0_BLKSIZE) != 0) {
return -E_INVAL;
}
/* don't allow I/O past the end of disk0 */
if (blkno + nblks > dev->d_blocks) {
return -E_INVAL;
}
/* read/write nothing ? */
if (nblks == 0) {
return 0;
}
lock_disk0();
while (resid != 0) {
size_t copied, alen = DISK0_BUFSIZE;
if (write) {
iobuf_move(iob, disk0_buffer, alen, 0, &copied);
assert(copied != 0 && copied <= resid && copied % DISK0_BLKSIZE == 0);
nblks = copied / DISK0_BLKSIZE;
disk0_write_blks_nolock(blkno, nblks);
}
else {
if (alen > resid) {
alen = resid;
}
nblks = alen / DISK0_BLKSIZE;
disk0_read_blks_nolock(blkno, nblks);
iobuf_move(iob, disk0_buffer, alen, 1, &copied);
assert(copied == alen && copied % DISK0_BLKSIZE == 0);
}
resid -= copied, blkno += nblks;
}
unlock_disk0();
return 0;
}
这些设备的实现看起来比较复杂,实际上属于比较麻烦的设备驱动的部分。
最后更新于