I/O 系统
I/O 系统又称作输入输出系统,SylixOS 兼容 POSIX 标准输入输出系统,SylixOS 的 I/O 概念继承了 UNIX 操作系统的 I/O 概念,认为一切皆为文件。与 UNIX 操作系统相同,SylixOS 中的文件也分为不同的类型。
文件类型
SylixOS 最常见的文件是普通文件和目录文件,但也有另外一些特殊文件类型,这些文件类型包括以下几种:
- 普通文件,这是最常见的文件类型之一,这种文件包含了某种形式的数据。这种数据无论是普通文本还是二进制,对于 SylixOS 来说没什么区别。需要注意的是,一个二进制可执行文件,内核必须理解其格式。SylixOS 二进制可执行文件都遵循一种标准化的格式,这种格式使得 SylixOS 能够确定程序代码和数据加载的位置(详细介绍见 动态装载)。
- 目录文件,这种文件包含了其他文件的名字以及指向与这些文件有关信息的指针。
- 块设备文件,这种文件提供的 I/O 接口标准符合 SylixOS 对块设备的定义。
- 字符设备文件,这是一种标准的不带缓冲的设备文件,在系统中最为常见的设备文件就是字符设备文件。
- FIFO 文件,这种类型的文件用于进程间通信,有时也称为命名管道。
- 套接字(socket)文件,这种文件可以用于进程间的网络通信。
- 符号链接文件,这种类型的文件指向另一个文件。
文件类型的信息包含在 stat 结构体的 st_mode 成员中。可通过下表所列的宏来判断,这些宏的参数都是成员 st_mode 的类型值。
struct stat {
dev_t st_dev; /* device */
ino_t st_ino; /* inode */
mode_t st_mode; /* protection */
nlink_t st_nlink; /* number of hard links */
uid_t st_uid; /* user ID of owner */
gid_t st_gid; /* group ID of owner */
dev_t st_rdev; /* device type (if inode device) */
off_t st_size; /* total size, in bytes */
time_t st_atime; /* time of last access */
time_t st_mtime; /* time of last modification */
time_t st_ctime; /* time of last create */
blksize_t st_blksize; /* blocksize for filesystem I/O */
blkcnt_t st_blocks; /* number of blocks allocated */
...
};
宏名 | 文件类型 |
---|---|
S_ISDIR(mode) | 目录文件 |
S_ISCHR(mode) | 字符设备文件 |
S_ISBLK(mode) | 块设备文件 |
S_ISREG(mode) | 普通文件 |
S_ISLNK(mode) | 符号链接文件 |
S_ISFIFO(mode) | 管道或命名管道 |
S_ISSOCK(mode) | 套接字文件 |
下面是一个打印文件类型的例子:
#include <stdio.h>
#include <sys/stat.h>
int main (int argc, char *argv[])
{
struct stat mystat;
int ret;
if (argc < 2) {
fprintf(stderr, "argc error.\n");
return (-1);
}
ret = stat(argv[1], &mystat);
if (ret < 0) {
perror("stat");
return (-1);
}
if (S_ISDIR(mystat.st_mode)) {
fprintf(stdout, "file: %s is dir file.\n", argv[1]);
}
if (S_ISCHR(mystat.st_mode)) {
fprintf(stdout, "file: %s is char file.\n", argv[1]);
}
if (S_ISBLK(mystat.st_mode)) {
fprintf(stdout, "file: %s is block file.\n", argv[1]);
}
if (S_ISREG(mystat.st_mode)) {
fprintf(stdout, "file: %s is general file.\n", argv[1]);
}
if (S_ISLNK(mystat.st_mode)) {
fprintf(stdout, "file: %s is link file.\n", argv[1]);
}
if (S_ISSOCK(mystat.st_mode)) {
fprintf(stdout, "file: %s is socket file.\n", argv[1]);
}
return (0);
}
在 SylixOS Shell 下运行程序:
# ./PrintFileType /dev/socket
file: /dev/socket is socket file.
# ./PrintFileType /dev/null
file: /dev/null is char file.
以下程序用到了 stat 函数,从程序的结果可以看出,文件的类型可通过下表中所示的宏进行判断。该段程序同时展示了下表中宏的使用方法。
宏名 | 文件类型 |
---|---|
S_ISDIR(mode) | 目录文件 |
S_ISCHR(mode) | 字符设备文件 |
S_ISBLK(mode) | 块设备文件 |
S_ISREG(mode) | 普通文件 |
S_ISLNK(mode) | 符号链接文件 |
S_ISFIFO(mode) | 管道或命名管道 |
S_ISSOCK(mode) | 套接字文件 |
文件描述符
对于内核而言,所有打开的文件都通过文件描述符引用。文件描述符是一个非负整数。当打开一个现有文件或创建一个新文件时,内核向进程返回一个文件描述符。当读、写一个文件时,使用 open 函数或者 creat 函数返回的文件描述符标识该文件,可将此文件描述符作为参数传递给 read 函数或 write 函数。
SylixOS 的文件描述符与 POSIX 定义兼容,它是从 0 开始一直到一个最大值的整型数字(_POSIX_OPEN_MAX),每一个打开的文件都有一个或者多个(dup)文件描述符与之对应。SylixOS 和绝大多数操作系统相同,打开文件时总是使用一个最小的且未使用的文件描述符作为新分配的文件描述符。
根据习惯,0(STDIN_FILENO)号文件描述符代表标准输入,1(STDOUT_FILENO)号文件描述符为标准输出,2(STDERR_FILENO)号文件描述符为标准错误。这里需要说明的是,SylixOS 每个进程都拥有自己的文件描述符表,各个进程间互不冲突。如果子进程存在父进程,则继承父进程所有文件描述符;如果子进程是孤儿进程,则只继承系统的 3 个标准文件描述符。一个进程内的所有线程都共享进程文件描述符。内核中存在一个全局的文件描述符表,这个文件描述符表不包含 0,1,2 号标准文件描述符,这三个文件描述符在内核中为重映射标志,即 SylixOS 允许内核中每个内核任务都拥有自己的标准文件描述符。
在符合 POSIX.1 的应用程序中,0、1、2 虽然已被标准化,但应当把它们替换成符号常量 STDIN_FILENO、STDOUT_FILENO 和 STDERR_FILENO 以提高可读性,在 SylixOS 中可通过包含 <unistd.h> 头文件来使用这些常量。
IO 系统结构
SylixOS 的 IO 系统结构,由于历史原因分为 ORIG 型驱动结构和 NEW_1 型驱动结构。NEW_1 型驱动结构在 ORIG 型驱动结构的基础上增加了文件访问权限、文件记录锁等功能。
如下图所示是 SylixOS ORIG 驱动结构图:
SylixOS 中每一个文件描述符对应一个文件结构,不同的文件描述符可以对应同一个文件结构,当对应同一个文件结构的所有文件描述符被关闭时,操作系统会释放对应的文件结构,同时调用相应的驱动程序。不同的文件结构可以指向同一个逻辑设备,例如一个 FAT 文件系统设备就可以被打开很多个文件结构。不同的逻辑设备也可以对应同一个驱动程序,例如物理结构相同的串口 0、串口 1 可以对应一组为其服务的驱动程序,每一组驱动程序具体服务的硬件设备则由底层 BSP 决定。
如下图所示是 SylixOS NEW_1 驱动结构图:
NEW_1 型驱动结构在 ORIG 的基础上增加了文件节点,从而引入文件访问权限、文件用户信息、文件记录锁(将在下文的“文件记录锁”详细介绍)等内容。
从上图中我们发现 SylixOS 支持在不同进程间共享打开的文件。下图是 NEW_1 型内核数据结构图。从下图可以看出 SylixOS 内核使用三种数据结构(文件描述符表项、文件结构、文件节点)来表示打开的文件,它们的关系决定了在文件共享方面,一个进程对另一个进程可能产生的影响。
- 每个进程都维护着自己的一个文件描述符表,每个文件描述符占其中一项,与每个文件描述符相关联的是:
- 指向文件结构的指针
- 文件引用计数
- 文件描述符标志(FD_CLOEXEC)
- 内核为所有打开的文件维护一个文件结构表,每一个文件结构表项包括(部分)。
- 设备头指针(这个指针指向设备节点)
- 文件名
- 文件节点指针
- 文件属性标志(读、写等,更多信息见下表)
- 文件当前指针(指示文件偏移)
- 每个打开的文件都有一个文件节点,文件节点包括(部分)。
- 设备描述符
- inode(同一个文件只有一个 inode)
- 文件权限信息(可读、可写、可执行)
- 文件用户信息
- 当前文件大小
- 文件记录锁指针
上图显示了一个进程对应的三种数据结构之间的关系。该进程打开两个不同的文件,一个从文件描述符 3 打开,另一个从文件描述符 4 打开。
选项名 | 说明 |
---|---|
O_RDONLY | 以只读的方式打开文件 |
O_WRONLY | 以只写的方式打开文件 |
O_RDWR | 以可读、可写的方式打开文件 |
O_CREAT | 如果文件不存在,则创建文件,且open函数的第三个参数指定文件权限模式 |
O_TRUNC | 如果文件存在,而且如果以只写或读写方式成功打开,则将其长度截断为0 |
O_APPEND | 将读写指针追加到文件的尾端 |
O_EXCL | 如果指定了O_CREAT,而文件存在,则出错。如果文件不存在,则创建 |
O_NONBLOCK | 以非阻塞的方式打开文件 |
O_SYNC | 使每次write等待物理I/O操作完成,包括由该write操作引起的文件属性更新 |
O_DSYNC | 使每次write等待物理I/O操作完成,但是如果该写操作并不影响读取刚写入的数据,则不许等待文件属性被更新 |
O_NOCTTY | 如果cpcName引用的是终端设备,则不将该设备分配作为此进程的控制终端 |
O_NOFOLLOW | 如果cpcName引用的是符号链接,则出错 |
O_CLOEXEC | 把FD_CLOEXEC标志设置为文件描述符标志 |
O_LARGEFILE | 打开大文件标志 |
O_DIRECTORY | 如果打开的文件是一个非目录文件,则返回错误并设置错误号为ENOTDIR |
如下图所示,展示了两个独立进程各自打开了同一个文件的情景。
我们假定第一个进程在文件描述符 3 上打开该文件,而另一个进程在文件描述符 4 上打开相同文件。打开该文件的每个进程都获得各自的一个文件结构,但对于一个给定的文件只有一个文件节点。之所以每个进程都获得自己的文件结构,是因为这可以使每个进程都有它自己的对该文件的当前读写指针(文件操作偏移量)。
文件描述符标志和文件属性标志在作用范围方面是有区别的,前者只用于一个进程中的一个文件描述符,而后者则应用于指向该给定文件结构的任何进程中的所有文件描述符,在后文将介绍如何调用 fcntl 函数获得和修改文件描述符标志和文件属性标志。