UNIX I/O
所有的I/O设备(例如网络、磁盘和终端)都被模型化为文件,``而所有的输入和输出都被当做相应文件的读和写来执行,所以内核可以利用称为 Unix I/O 的简单接口来处理输入输出,比如使用 open() 和 close() 来打开和关闭文件,使用 read() 和 write() 来读写文件,或者利用 lseek() 来设定读取的偏移量等等。
Linux shell创建的每个进程开始时都有三个文件:
0:标准输入
1:标准输出
2:标准错误
因此用户所打开的第一个文件的描述符为3,并且以后的所有文件的描述符都是从3开始的从小到大的整数。
文件分类
每个Linux文件都有一个类型(type)来表明它在系统中的角色:
- 普通文件(binary file):包含任意数据
- 目录(directory):相关一组文件的索引(可以简单理解为Windows里的快捷方式,但不全是)
- 套接字(socket):和另一台机器上的进程通信的类型
文件操作
文件操作有:open,close,read,write,stat,lseek,dup2
打开文件
进程通过调用open函数打开一个已存在的文件或创建一个新的文件。
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int open(char* filename,int flags, mode_t mode);
返回:若成功则为新的文件描述符,若出错则为-1
参数分析
(1)char* filename:文件名,指向一个字符串的指针!
(2)int flags:指明进程打算如何访问该文件
(3)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);
返回:若成功返回则0,若出错则为-1。
参数:想关闭文件的文件描述符
注意:关闭一个已关闭的描述符会出错
读写文件
应用程序是通过分别调用read和write函数来执行输入和输出的
#include <unistd.h>
ssize_t read(int fd,void *buf,size_t n);
返回:成功则为读取到的字节数,出错则为-1,若EOF(读完)则为0。
参数分析
(1)int fd:打开文件的文件描述符
(2)void *buf:读出来的结果存放的位置
(3)size_t n:最多读取的字节个数
read函数的作用:从描述符为fd的文件的当前位置复制最多n个字节到位置buf。
一个程序使用read和write函数调用一次一个字节地从标准输入复制到标准输出,如下所示。
/* $begin cpstdin */
#include "csapp.h"
int main(void)
{
char c;
while(Read(STDIN_FILENO, &c, 1) != 0)
Write(STDOUT_FILENO, &c, 1);
exit(0);
}
读取文件元数据
元数据:元数据就是用来描述数据的数据,由内核维护,可以通过stat和fstat函数来访问。
#include <unistd.h>
#include <sys/stat.h>
int stat(const char*filename,struct stat* buf);
int fstat(int fd,struct stat* buf);
stat函数以一个文件名为输入,并填写如下列所示的一个stat数据结构中的各个成员。fstat函数是相似的,只不过是以文件描述符而不是以文件名作为输入。
保存信息的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
}
共享文件
Linux文件可以用很多方式进行共享,内核通常用三个相关的数据结构来表示打开的文件:
-
描述符表: 每个进程都有独立的描述符表,表项是由进程打开的文件描述符来索引的。 每个描述符表项只想文件表中的一个表项。
-
文件表:
打开文件的集合是由一张文件表来表示的,所有进程共享。
它记录了当前文件的位置,当前指向该表项的描述符表项数(成为引用计数)和一个指向v-node表中对应表项的指针。 当引用计数为0是,内核会自动删除这个文件表表项。 -
v-node表:
所有进程共享,包含了stat结构中的大多数信息,包括st_mode和st_size成员。
示例:子进程继承父进程的打开文件
#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
运行结果
Parent: c1 = a, c2 = b
Child: c1 = a, c2 = c
过程分析
1.先打开了abcde.txt,返回文件描述符fd1,并读取文件里的第一个字符‘a’,光标停留在了ab之间;
2.进行了fork,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
运行结果
pqrswxyznef
过程分析
1.打开文件,返回fd1,写入“pqrs”,此时光标位于文件末尾
2.fd3打开这个文件,O_APPEND表明光标停留在文本文件的最后一个字符后面,并写入"jklmn"
3.dup函数,将文件描述符表fd1文件指向了fd2,简单理解就是fd2和fd1是一样的,所以可知fd2光标在pqrs后面
4.写fd2,此时写的"wxyz"会覆盖"jklm"
5.写fd3,fd3的光标在最末尾,将ef写在了最后