一切皆文件。
(本篇中的例子若非特殊说明,使用的都是abcde.txt,文件内容就是abdce)
- Linux shell创建的每个进程开始时都有三个打开的文件(在头文件<unisted.h>中定义):
0:标准输入
1:标准输出
2:标准错误 - 文件操作有:open,read,write,stat,dup2,close,lseek;
- 每个Linux文件都有一个类型:普通文件,目录,套接字等。
那我们开始看看实践的题目叭!
打开和关闭文件
进程通过open函数来打开一个已经存在的文件或者创建一个新文件
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int open(char* filename,int flags, mode_t mode);
open函数会返回一个文件描述符,是在当前进程中没有打开的最小描述符
根据我们上面讲到的,shell创建的进程中首先会打开三个文件,所以用户打开的文件的最小进程号应该是3.
open函数带有的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:其他人(任何人)能够执行这个文件
这里的mode用一个三位的八进制数进行标识,也就意味着一共九位01二进制来表示是否可读,可写,可执行。
关闭文件
#include <unistd.h>
int close(int fd);
如果尝试关闭已经关闭的文件的话,就会报错。
读写文件
#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。三种返回值:-1出错,0表示EOF,或者是读取到的字节的个数。
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);
}
STDIN_FILENO就是标准输入,STDOUT_FILENO就是标准输出,SITERR_FILENO就是标准错误
运行结果:
我们看到这其实就是一个循环读入,每次从标准输入就是键盘当中输入一个字符的时候,就自动读取一个字符到缓冲区中去,直到读到一个\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);
stat函数以一个文件名作为输入,可以查看到文件的各种数据。我们可以用man stat 看一下Linux里关于它的一些信息。
保存信息的stat结构体的结构
然后让我们来试着看一下这个代码
/* $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)是否为网络套接字
这里的st_mode成员就是我们上面所说的那个无符号八进制数,让它和S_IRUSR做与运算,就能得到它是否可读。
共享文件
内核用三个相关的数据结构来表示打开的文件
- 描述符表
每个进程都有独立的描述符表,表项是由进程打开的文件描述符来索引的。每个描述符表项只想文件表中的一个表项。 - 文件表
打开文件的集合是由一张文件表来表示的,所有进程共享。它记录了当前文件的位置,当前指向该表项的描述符表项数(成为引用计数)和一个指向v-node表中对应表项的指针。当引用计数为0是,内核会自动删除这个文件表表项。 - v-node表。所有进程共享,包含了stat结构中的大多数信息。
这里就是打开了两个不同的文件,fd1和fd4通过不同的文件表表项A和B来引用两个不同的文件,这里没有共享文件,并且每个描述符对应一个不同的文件。
上面这一个就是两次打开了同一个文件。关键思想就是每个描述符都有他自己的文件位置,所以对不同描述符的读操作可以从文件的不同位置获取数据。简单点想就是只要是打开文件不进行后续的dup或者dup2,文件描述符都对应不同的文件表表项。
然后回到我们之前讲的fork,fork一次,生成了一个新的子进程,子进程复制了所有父进程的环境。那么如果父进程中打开了文件的话,就有如下的情况:
我们先来看看这个程序
#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;
}
运行结果:
我们看到了:
1.先打开了abcde.txt,并且读取了一个字符,现在光标停留在了ab之间
2.进行了fork,子进程复制了父进程的环境。
3.然后两个进程都遇到了sleep,接着先继续执行子进程,它读了一个字符,因为先前光标停留在ab之间,所以此时c2读到的是b
4.child执行完后回到父进程,父进程和子进程的文件表表项相同,所以光标在b后,往后读一个,读到了c
所以最后的输出结果就是如图所示辣。
讲完这个,我们来讲
I/O重定向
I/O重定向能帮助用户将磁盘文件和标准输入输出联系起来。
举个栗子
linux>ls>foo.txt
使得shell加载和执行ls程序,将标准输出重定向到磁盘文件foo.txt
那么如何重定向呢?
一种考的是dup2函数
#include <unistd.h>
int dup2(int oldfd,int newfd);
若成功则为非负的描述符,若出错则为-1
dup2函数做的其实就是用oldfd的文件表表项替换掉newfd的文件表表项,此外如果newfd是打开的状态的话,会需要先关闭掉newfd。示意图如下:
这里的操作就是dup2(4,1),这个例子里面内核会自动销毁文件A的文件表和v-node表项,然后B的引用计数会加一。
看个例子:
#include "csapp.h"
int main(int argc, char *argv[])
{
int fd1, fd2, fd3;
char c1, c2, c3;
char *fname = argv[1];
fd1 = Open(fname, O_RDONLY, 0);
fd2 = Open(fname, O_RDONLY, 0);
fd3 = Open(fname, O_RDONLY, 0);
dup2(fd2, fd3);
Read(fd1, &c1, 1);
Read(fd2, &c2, 1);
Read(fd3, &c3, 1);
printf("c1 = %c, c2 = %c, c3 = %c\n", c1, c2, c3);
Close(fd1);
Close(fd2);
Close(fd3);
return 0;
}
运行结果:
解释一下,首先fd1,fd2,fd3拥有不同的文件表表项,但指向同一个v-node。接着,dup2了一下.此时,fd2和fd3指向了相同的文件表表项都是fd2的文件表表项。然后读入c1,fd1中的光标位标记为ab之间,fd2的仍然是a前。然后读入c2,此时fd2的光标也在ab之间了,接着读入c3的时候,fd2和fd3是同一个文件表表项,所以c3读走了b。所以结果如图所示。
接着我们看看dup。(呜呜,要是早点写就好了。期末碰到了这一题,但是好像也写对了,嘿嘿)
#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
*/
这里我们看到了一个新的函数dup。
int dup(int oldfd);
dup和dup2的区别,其实简单讲就是dup2把前一个文件项覆盖了后一个文件项,而dup就是直接再建一个文件,文件的文件项就是oldfd。
来看看这道题的意思:
首先打开给的文件,如果没有就造一个,如果有就清空它。
然后在这个文件里写"pqrs"四个字符
紧接着fd3也打开了这个文件,打开的方式是光标停留在文本文件的最后一个字符后面。然后写了"jklmn"五个字符。所以现在文件里的样子是"pqrsjklmn"
紧接着dup了一下,所以现在fd2中应该是"pqrsjklmn",光标停留在pqrs后面,因为fd1的光标也在那里。
然后写fd2,此时写的"wxyz"显然会覆盖"jklm"
最后又写fd3,注意到fd3的光标在最末尾,所以ef写在了最后
所以文件最后就变成了这样:
本博客部分照片来自于csapp课本上解说的图片。