Unix/Linux哲学中有这么一句话:一切皆文件。
而文件实际上可以看做是字节的序列。
所有的I/O设备(例如网络、磁盘和终端)都被模型化为文件,而所有的输入和输出都被当做相应文件的读和写来执行,所以内核可以利用称为 Unix I/O 的简单接口来处理输入输出,比如使用 open() 和 close() 来打开和关闭文件,使用 read() 和 write() 来读写文件,或者利用 lseek() 来设定读取的偏移量等等(此段转载至CSAPP——系统级I/O)
@通常Linux shell创建的每个进程开始时都会自动打开三个文件:
0:标准输入
1:标准输出
2:标准错误
@文件操作有:open,close,read,write,stat,lseek,dup2
@每个文件在Linux中都有一个类型:普通文件(包含任意数据)、套接字(和另一台机器上的进程通信类型)、目录(相关一组文件的索引)等
接下来从文件操作开始深入理解系统级I/O
打开文件:
进程调用open函数打开一个已存在的文件或创建一个新的文件
open函数打开成功会返回一个文件描述符,出错则返回-1,且文件描述符是在当前进程中没有打开的最小描述符,根据上文所说的,每个进程的开始都会自动打开三个文件,所以用户打开文件的最小进程号是3,下面来看一段代码:(即open函数)
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int open(char* filename,int flags, mode_t mode);
- char* filename:文件名
- int flags:指明进程打算如何访问该文件
- mode_t mode:指定了新文件的访问权限位
对于flags有如下宏定义:
- O_RDONLY:只读
- O_WRONLY: 只写
- O_RDWR: 可读可写
- O_CREAT: 文件不存在,就创建一个它的截断的空文件
- O_TRUNC: 如果文件已经存在,就截断
- O_APPEND: 在每次写操作前,设置文件位置到文件结尾处
而mode参数也有如下宏定义:
- S_IRUSR:使用者(拥有者)能够读这个文件
- S_IWUSR:使用者(拥有者)能够写这个文件
- S_IXUSR:使用者(拥有者)能够执行这个文件
- S_IRGRP:拥有者所在组的成员能够读这个文件
- S_IWGRP:拥有者所在组的成员能够写这个文件
- S_IXGRP:拥有者所在组的成员能够执行这个文件
- S_IROTH:其他人(任何人)能够读这个文件
- S_IWOTH:其他人(任何人)能够写这个文件
- S_IXOTH:其他人(任何人)能够执行这个文件
关闭文件:
看如下代码:
#include <unistd.h>
int close(int fd);
不能关闭一个已经关闭了的文件,否则会报错(必须仔细检查函数的参数和返回值)
读写文件:
调用read函数和write函数进行输入和输出
上代码:
#include <unistd.h>
ssize_t read(int fd,void *buf,size_t n);
ssize_t write(int fd,const void *buf,size_t n);
read函数的作用是从描述符为fd的文件的当前位置复制最多n个字节到位置buf
而write函数的作用是从内存buf出至多复制n个字节到描述符为fd的当前文件位置
接下来就是正式跑代码的时候了,上代码:
/* $begin cpstdin */
#include "csapp.h"
int main(void)
{
char c;
while(Read(STDIN_FILENO, &c, 1) != 0)
Write(STDOUT_FILENO, &c, 1);
exit(0);
}
运行结果:
其实这个程序就是在循环读入,每次从标准输入就是键盘当中输入一个字符的时候,就自动读取一个字符到缓冲区中去,直到读到一个\n(回车)就输出所有缓冲区的内容到标准输出就是屏幕上面。
读取文件元数据:
什么是元数据? 元数据就是用来描述数据的数据,由内核维护,通过stat和fstat函数来访问
#include <unistd.h>
#include <sys/stat.h>
int stat(const char*filename,struct stat* buf);
int fstat(int fd,struct stat* buf);
在Linux中可以用 man stat来看关于它的信息,如图:
保存信息的数据结构:
struct stat
{
dev_t st_dev; // Device
ino_t st_ino; // inode
mode_t st_mode; // Protection & file type
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
unsigned long st_blksize; // Blocksize for filesystem I/O
unsigned long st_blocks; // Number of blocks allocated
time_t st_atime; // Time of last access
time_t st_mtime; // Time of last modification
time_t st_ctime; // Time of last change
}
接下来看看另一段小程序:
/* $begin statcheck */
#include "csapp.h"
int main (int argc, char **argv)
{
struct stat stat;
char *type, *readok;
/* $end statcheck */
if (argc != 2) {
fprintf(stderr, "usage: %s <filename>\n", argv[0]);
exit(0);
}
/* $begin statcheck */
Stat(argv[1], &stat);
if (S_ISREG(stat.st_mode)) /* Determine file type */
type = "regular";
else if (S_ISDIR(stat.st_mode))
type = "directory";
else
type = "other";
if ((stat.st_mode & S_IRUSR)) /* Check read access */
readok = "yes";
else
readok = "no";
printf("type: %s, read: %s\n", type, readok);
exit(0);
}
运行结果:
这个程序主要是用来判断一个文件类型是普通文件,目录还是其他;以及这个文件是否可读。
Linux在sys/stat.h中定义了宏谓词来确定st-mode成员的文件类型:
S_ISREG(m)是否为普通文件
S_ISDIR(m)是否为目录文件
S_ISSOCK(m)是否为网络套接字
所以程序的解释为:
参数"abcde.txt"先传到if的括号中,条件满足,abcde.txt是一个普通的文本文件,判断结束,printf。
共享文件:
Linux文件可以用很多方式进行共享,而内核通常用三个相关的数据结构来表示打开的文件
- 描述符表
每个进程都有独立的描述符表,表项是由进程打开的文件描述符来索引的。每个描述符表项只想文件表中的一个表项。 - 文件表
打开文件的集合是由一张文件表来表示的,所有进程共享。它记录了当前文件的位置,当前指向该表项的描述符表项数(成为引用计数)和一个指向v-node表中对应表项的指针。当引用计数为0是,内核会自动删除这个文件表表项。 - v-node表
所有进程共享,包含了stat结构中的大多数信息,包括st_mode和st_size成员。
打开了两个不同的文件,fd1和fd4通过不同的文件表表项A和B来引用两个不同的文件,这里没有共享文件,并且每个描述符对应一个不同的文件。
两次打开了同一个文件,因为每个描述符都有他自己的文件位置,所以对不同描述符的读操作可以从文件的不同位置获取数据。
还有一个比较特殊的情况,就是关于子进程和父进程的问题,如图所示:
看一个小程序:
#include "csapp.h"
int main(int argc, char *argv[])
{
int fd1;
int s = getpid() & 0x1;
char c1, c2;
char *fname = argv[1];
fd1 = Open(fname, O_RDONLY, 0);
Read(fd1, &c1, 1);
if (fork()) {
/* Parent */
sleep(s);
Read(fd1, &c2, 1);
printf("Parent: c1 = %c, c2 = %c\n", c1, c2);
} else {
/* Child */
sleep(1-s);
Read(fd1, &c2, 1);
printf("Child: c1 = %c, c2 = %c\n", c1, c2);
}
return 0;
}
其中abcde.txt里的内容为abcde
运行结果:
从结果中可以看出:
1.先打开了abcde.txt,并且读取了一个字符,现在光标停留在了ab之间
2.进行了fork,子进程复制了父进程的环境。
3.然后两个进程都遇到了sleep,接着先继续执行子进程,它读了一个字符,因为先前光标停留在ab之间,所以此时c2读到的是b
4.child执行完后回到父进程,父进程和子进程的文件表表项相同,所以光标在b后,往后读一个,读到了c
5.最后进行输出得到了如图所示的结果
I/O重定向:
I/O重定向能帮助用户将磁盘文件和标准输入输出联系起来
接下来用函数来理解:
#include <unistd.h>
int dup2(int oldfd,int newfd);
int dup(int oldfd);
- dup2函数:用oldfd的文件表表项替换掉newfd的文件表表项,此外如果newfd是打开的状态的话,会需要先关闭掉newfd。
- dup函数:直接再建一个文件,文件的文件项就是oldfd
跑一个程序看看:
#include "csapp.h"
int main(int argc, char *argv[])
{
int fd1, fd2, fd3;
char *fname = argv[1];
fd1 = Open(fname, O_CREAT|O_TRUNC|O_RDWR, S_IRUSR|S_IWUSR);
Write(fd1, "pqrs", 4);
fd3 = Open(fname, O_APPEND|O_WRONLY, 0);
Write(fd3, "jklmn", 5);
fd2 = dup(fd1); /* Allocates new descriptor */
Write(fd2, "wxyz", 4);
Write(fd3, "ef", 2);
Close(fd1);
Close(fd2);
Close(fd3);
return 0;
}
/*abcde.txt
pqrswxyzef
*/
此次abcde.txt文件内容为pqrswxyzef
运行结果:
1.打开文件,返回fd1,写入“pqrs”,此时光标在文件末尾
2.fd3打开这个文件,O_APPEND表明光标停留在文本文件的最后一个字符后面,并写入"jklmn"
3.dup函数,将文件描述符表fd1文件指向了fd2,简单理解就是fd2和fd1是一样的,所以可知fd2光标在pqrs后面
4.写fd2,此时写的"wxyz"会覆盖"jklm"
5.写fd3,因为fd3的光标在最末尾,所以ef写在了最后
本文参考链接:
1.https://blog.youkuaiyun.com/qq_44871442/article/details/103375768#_12
2.《计算机系统基础》第十章——系统级I/O