写在前面
现在我们要开始Linux的第二个大模块,基础IO.在这里你将真正的认识到文件究竟是什么,了解到文件系统,更可以解决我们之前一直避而不谈的缓冲区,我们一起来看看吧.
文件
在前面几个博客那里,我问了一个问题.什么是文件?或者说是文件包含什么?那里我们回答了文件包含两个部分,分别是文件得内容和文件得属性.这里得属性包含很多得内容,文件名,文件所有者 ,文件大小,文件所属组,新创建时间,修改时间…属性我会在文件系统那里会重点和和大家分析.
文件操作
我们今天谈的操作包括对文件内容和属性的操作,注意,我们主要是对文件得内容进行操作,而伴随这内容的操作往往属性会发生改变,例如我们修改文件内容,文件属性的中的文件大小,最新修改时间可能会发生改变,要知道属性也是数据,大家不要把他们割裂开来.
- 修改文件内容会修改文件属性
- 修改文件属性不一定会修改内容
回答一下第二个结论,如果我们仅仅是修改文件的权限,这里不会引起内容的改变.
打开文件
我们在C/C++中都学过文件操作,那么我们像问一下,我们打开文件究竟是做了什么?一旦我们打开文件,我们就要把文件的属性或者内容加载在内存中.那么是不是文件都处于被打开的状态?没有被打开的文件咋哪里?这个问题我们一想就可以知道,只有少数的文件处于被开的状态,那些没有打开的在磁盘上静静的等着.也就是文件分为两个模式,内存文件和磁盘文件.
再回答一个问题,文件是被谁打开的?在没有学Linux之前,我们很容易的可以说出是被运行起来的程序打开的,今天我们具体一点,是被进程打开的.记住一定不要说是被代码甚至编程程序 打开的,他们也是文件,没有运行前都在磁盘中静静的躺着,只有当程序运行起来的时候,才会真正的打开文件,我们才可以对文件进行操作.
准备工作
我们今天开始学习Linux下文件的操作.我们这里很疑惑,我们不是已经学习了C语言的文件的操作了吗?这里为何还要学Linux下的.这里给大家回答一下,因为它更加接近底层,也就是更容易我们接触到原理.是的,我们学习了C语言的,也不否认C语言用的额更加便捷,毕竟它是后面发展出来的.但是后面有几个内容C语言是解决不了的,我们必须使用Linux的,你知道文件描述符吗?开始的时候我也不知道,那么我们后面需要用该怎么办?这里必须从文件开始.
当我们写入文件时,最终是先磁盘写入文件.那么磁盘是硬件吗?谁有资格向硬件写入? 毫无疑问是操作系统OS,我们谁都绕不开OS.那么OS是如何被访问的. 要知道OS谁都不相信,那么这里我们就调用的系统函数,也就是我们语言封装了系统接口.这里就解释了C语言中那些函数和系统接口的关系.此时我们又出现了一个文题,既然存在了系统接口,我们为何又要耗费精力去封装他呢?这是因为原生应用陈本太高,而且只能在Linux平台上跑. 那么语言是如何解决跨平台问题?这里就是多态 (广义性)(和这个没有关系) 我更倾向于 条件编译(穷举所有的接口,后面编译).这个等会谈.
当前目录
这里开始回答一下我们C语言没有谈的一个内容,我们在打开文件时候说了如果我们不指定文件的路径,默认是在当前路劲下创建,那时我和大家说当前路径就是源代码所在的路径,这里是不准确的,只不过那里我们没有办法谈.先看现象.
int main()
{
FILE* fp = fopen("log.txt","w");
if(fp == NULL)
{
perror("fopen fail");
return 0;
}
const char* msg = "hello 104 ";
int cnt = 1;
while(cnt <= 5)
{
fprintf(fp, "%s: %d\n", msg, cnt++);
}
fclose(fp);
fp = NULL;
return 0;
}
现在我们就可以来看看默认的路劲究竟是什么了.先来写一个代码,把他写成死循环.
int main()
{
FILE* fp = fopen("log.txt","w");
if(fp == NULL)
{
perror("fopen fail");
return 0;
}
printf("pid : %d\n", getpid());
while(1)
{
sleep(1);
}
const char* msg = "hello 104 ";
int cnt = 1;
while(cnt <= 5)
{
fprintf(fp, "%s: %d\n", msg, cnt++);
}
fclose(fp);
fp = NULL;
return 0;
}
此时我们看一下上面截图,其中我们用红色框标记的东西就是进程的路劲,而我们之前所谓的单签路劲也是她,这里我们就明白了,如果我们不指定路劲的话,那么我们就会在进程所在的路劲下创建所谓的新文件.此时修改一下进程的路劲,试一下.
int main()
{
FILE* fp = fopen("log.txt","w");
if(fp == NULL)
{
perror("fopen fail");
return 0;
}
printf("pid : %d\n", getpid());
// 我们有意修改进程的工作路径
chdir("/home/bit/");
while(1)
{
sleep(1);
}
const char* msg = "hello 104 ";
int cnt = 1;
while(cnt <= 5)
{
fprintf(fp, "%s: %d\n", msg, cnt++);
}
fclose(fp);
fp = NULL;
return 0;
}
此时我们就会明白所谓的当前路径本质上就是进程的工作路劲,这里一点是需要我们了解的.
标记位
再谈这个之前,我们明白在C语言中如果我们用写的方式打开一个文件,那么这个文件如果不存在就创建,存在了那么就会把这个文件清空.这个我们已经谈了很多了,这里就不演示了.这里把追加的写一下,引出我们下面的话题.
int main()
{
FILE* fp = fopen("log.txt","a");
if(fp == NULL)
{
perror("fopen fail");
return 0;
}
const char* msg = "hello 104 ";
int cnt = 1;
while(cnt <= 5)
{
fprintf(fp, "%s: %d\n", msg, cnt++);
}
fclose(fp);
fp = NULL;
return 0;
}
这里我们就有点问题,编译器是如何分别出我们是追加还是清空的?这里我们先把原理和大家说一下.这个由于标志位的原因,我们不同比特位的比特位设置成1后者是0让后通过他们按位与来进行判断的.我们简单的实现一下他们的原理.
#define PRINT_A 0x01 // 0000 0001
#define PRINT_B 0x02 // 0000 0010
#define PRINT_C 0x04 // 0000 0100
#define PRINT_D 0x08 // 0000 1000
#define PRINT_DEL 0x00
void Show(int flags)
{
if (flags & PRINT_A)
printf("hello A\n");
if (flags & PRINT_B)
printf("hello B\n");
if (flags & PRINT_C)
printf("hello C\n");
if (flags & PRINT_D)
printf("hello D\n");
if (flags == PRINT_DEL)
printf("hello default\n");
}
int main()
{
Show(PRINT_DEL);
printf("\n\n");
Show(PRINT_A);
printf("\n\n");
Show(PRINT_B | PRINT_C | PRINT_D);
return 0;
}
这里我们就明白了,所谓的标志位就是位图结构,我们通过定义宏时,一般只有一个比特位是1,其余均是0,并且和其他的宏对应时不能重叠.
文件操作
现在我们开始一下Linux下文件的操作,这个是非常简单的.这里我们将会真正的了解文件操作的底层是什么.
打开/关闭文件
这里我们先学操作,后面再说它的原理.
open
先来看打开文件,我们看看内核提供的系统接口.我们只看第二个函数.这里分析一下参数.第一个就是文件名,和之前我们用的是一样的.第二个参数就是前面我们的标志位,在C语言中和以读或者以写的方式打开文件.第三个参数是我们打开文件的权限,关于权限问题我们已经谈过了.关于返回值这里我们先不谈,只需要知道它是一个int类型的,我们称之为文件描述符,如果打开失败,这里会返回值是-1.
[bit@Qkj 2022]$ man 2 open
现在我们开始谈第二个参数,我们说了是标志位,那么也就是内核会给我们提供几个宏供我们使用,实际上我们想的是正确的,这里我们重点看5个宏.
- O_CREAT 如果文件不存在,创建文件
- O_RDWR 只读文件,如果文件不存在,这里就会放回-1
- O_WRONLY 只写,如果文件不存在,这里就会放回-1
- O_APPEND 追加,如果文件不存在,这里就会放回-1
- O_TRUNC 文件已经存在,就清空它
上面5个宏都是各司其职,而且这里有个和我们之前不太一样,在C语言中如果我们以"w"的方式打开文件,那么这个文件如果不存在就会被创建,这里Linux就是一个功能,只负责写,而且写的时候必须保证文件存在.
int main()
{
umask(0);
int fd = open("log.txt", O_WRONLY, 0666);
if (fd < 0)
{
std::cout << "fd: " << fd << " " << strerror(errno) << std::endl;
return 1;
}
return 0;
}
除此之外,要知道如果以"w"的方式打开文件,如果文件存在内容,就会被清空,那么Linux不是的.这里我想在log.txt文件中添加一点内容,这里涉及一点文件的读写,大家先不用关心,后面我们都会学到的.
int main()
{
umask(0);
int fd = open("log.txt", O_WRONLY | O_CREAT, 0666);
const char *str = "abcdef";
int len = strlen(str);
write(fd, str, len); // 注意 带 '\0' 是C语言的风格 ,文件可是不做要求的
return 0;
}
等到我们把文件描述符谈完了,我们模拟实现一个C语言的fopen函数,这里会更加清楚这5个宏.
close
这个非常简单,就是一个函数.
[bit@Qkj 12_04]$ man 2 close
这里有一个问题,文件可以重复的关闭吗?我们测试一下.这里是可以的.
int main()
{
umask(0);
int fd = open("log.txt", O_WRONLY | O_CREAT, 0666);
close(fd);
close(fd);
return 0;
}
读写文件
这个很简单,我们在C语言里面谈的接口已经够多了,Linux下文件的读写接口是非常简单的.
write
我们先把函数给大家拿出来.
[bit@Qkj 12_04]$ man 2 write
我用语言说一下,我们向文件描述符为fd的文件中写入buf中的内容,其中我们写入count个字节.返回值先不谈.
int main()
{
umask(0);
int fd = open("log.txt", O_WRONLY | O_CREAT, 0666);
const char *str = "1234567";
int len = strlen(str);
write(fd, str, len);
return 0;
}
这里解决大家一个疑惑,这个函数是一个一个字节写入的,他把读取的数据转化成ASCII码写入在文件中.
int main()
{
umask(0);
int fd = open("log.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);
int val = 0x41424344;
char * msg = "abcd";
int len = strlen(msg);
write(fd,&val, 4);
close(fd);
return 0;
}
read
同样的,我们也先看看一下这个函数.
[bit@Qkj 12_04]$ man 2 read
这个函数是从文件描述符为fd的文件中读取数据,把读取的数据放到buf数组中,最多读取count个字节
int main()
{
umask(0);
int fd = open("file.txt", O_RDONLY, 0666);
if (fd < 0)
{
printf("%s\n", strerror(errno));
exit(1);
}
printf("fd %d\n", fd);
char buffer[128];
ssize_t s = read(fd, buffer, sizeof(buffer) - 1);
if (s > 0)
{
buffer[s] = '\0'; // 这个注意下标
printf("%s\n", buffer);
}
return 0;
}
上面我们一直规避两个问题,一个是C语言字符串末尾’\0’,另外一个是read函数的返回值.这里都给大家说一下.
这里先谈第二个,read函数的返回值.这个返回值类型就是一个无符号4个字节的整数,我们只是把他封装了一下吧了.read函数会返回实际读取到的字节数.
int main()
{
umask(0);
int fd = open("log.txt", O_RDONLY, 0666);
char buffer[128];
ssize_t s = read(fd, buffer, sizeof(buffer) - 1);
if (s > 0)
{
buffer[s] = '\0'; // 这个注意下标
printf("%d %s\n", s, buffer);
}
return 0;
}
再来回答我们的’\0’.注意,字符串后面以’\0’结尾这个是C语言标准的,有的语言是不遵守这个的.例如Java.文件作为一个通用的数据存储方式,写入的时候不需要’\0’,读取的时候更加没有’\0’.由于这里我们用的是C/C++,故读取的时候我们需要把他加上,写入的时候要去掉.
文件描述符
我们这里开始正式的谈一下open函数的返回值为何是一个int类型的整数.这个涉及到的内容有点多.先来回答个问题,C语言的打开文件的返回值是什么?这里很容易,一个FILE*.也就是我们每打开一个文件都会出现一个标志,这个标志是什么都不重要,它的代表意思才是最关键的.在Linux中open打开文件的标志是一个整数,这个仅此而已罢了
理解了上面的东西,我们先来看一个现象.
int main()
{
umask(0);
int fda = open("loga.txt", O_WRONLY|O_CREAT, 0666);
int fdb = open("logb.txt", O_WRONLY|O_CREAT, 0666);
int fdc = open("logc.txt", O_WRONLY|O_CREAT, 0666);
int fdd = open("logd.txt", O_WRONLY|O_CREAT, 0666);
int fde = open("loge.txt", O_WRONLY|O_CREAT, 0666);
printf("fda %d\n", fda);
printf("fdb %d\n", fdb);
printf("fdc %d\n", fdc);
printf("fdd %d\n", fdd);
printf("fde %d\n", fde);
return 0;
}
我们疑惑发现这个好像是一个连续的整数,那么012 去哪了?在C语言中我们说了0代表标准输入,1代表标准输出,2代表标准错误.听着是不是很熟悉.但是这我们可以很容易的推出消失的012也一定是文件描述符.那么请问这里两个说法哪个正确?
如果你能想到上面的问题,恭喜你,你已经开始有意识的吧C语言和Linux给联系起来了.我们之前说的0代表标准输入…是C语言的规则,现在谈的是Linux的.要知道C语言是对Linux的系统接口做了封装,也就是我们之前谈的FILE结构体里面必定会有一个属性和文件描述符一摸一样.我们先来证明一下这个观点,等会谈012.
int main()
{
umask(0);
FILE* pf1 = fopen("log1.txt","w");
FILE* pf2 = fopen("log2.txt","w");
FILE* pf3 = fopen("log3.txt","w");
printf("pf1 : %d\n", pf1->_fileno);
printf("pf2 : %d\n", pf2->_fileno);
printf("pf3 : %d\n", pf3->_fileno);
return 0;
}
这里确实证明了C语言中的FILE确实封装文件描述符,我们在很久之前已经说了,一个程序运行时会默认打开3个流,也就是标准输入,标准输出和标准错误,这也是我们文件描述符是从3开始的原因,因为012已经打开了.
int main()
{
umask(0);
printf("stdin : %d\n", stdin->_fileno);
printf("stdout : %d\n", stdout->_fileno);
printf("stderr : %d\n", stderr->_fileno);
return 0;
}
你只是给我们看了一个现象,只能证明C语言确实封装了文件描述符.这里感觉还是有点说服不了我,我们还需要用一下,证明Linux中0确实代表标准输入.如何证明呢? 不用再使用scanf了
int main()
{
umask(0);
char buffer[128];
// 从 fd = 0的是文件中去读,读到内容放在 buffer中
ssize_t s = read(0, buffer, sizeof(buffer) - 1);
if (s > 0)
{
buffer[s] = '\0';
printf("输出 %s", buffer);
}
return 0;
}
这个是往fd=1的文件中写入数据
int main()
{
umask(0);
const char *msg = "123456\n";
write(1, msg, strlen(msg));
return 0;
}
也可以对 fd=2写入数据,关于1和2的区别,我们等会谈
文件描述符
上面我们已经解决了012消失的问题,这里我们需要在谈一下文件描述符.你看01234…这些数据像什么?像不像数组的下标.好了这是我们的开端.我们需要简单谈一下内核的数据结构.
我想问一下,对于几个进程我们是不是可以打开多个文件?这是肯定的,我们上面也这样做了.也就是进程和文件的关系是1:n.反过来一个文件是不是可以被多个进程打开这个我们先不谈.对于打开的文件,OS会把它加载到内存中,也就是进程和文件都在内存中.那么请问进程对于这一大堆文件怎么办? 一个进程中可能存在许多文件,那么OS是不是要对文件进行管理呢?要的,那么我们是如何管理的.这里和进程一样,对文件 先描述,在组织.我们先来谈描述.这里简单的说一下.
struct file
{
//文件 属性 + 内容
struct file *prev;
struct file *next;
};
既然我们把文件给描述成一个结构体了,此时我们就要有相应的数据结构把打开的文件组识起来.对于被打开文件,肯定要创建打开文件的数据结构 ,并把它链接起来 ,注意底层不一定是这样做的,我们这么说只是为了理解.此时对文件的管理变成的对链表的增删改查.这个和进程是一样的.
那么进程如何管理这个链表呢? 我们打开一个文件,OS创建一个 strcut file结构体,用链表连起来,此时我们把这个结构体的地址放在一个数组中,至于如何放,遵循什么规则我们先不谈,然后返回数组的下标.这就是返回文件描述符的本质,fd本质就是数组下标.
这个时候我们需要数组所在的结构体用进程给管理起来,在task_struct中我们有一个指向改结构体的指针,OS会把它连接起来.此时就完成了进程对文件的管理.
这里我们就可以解释了我们read或者write的时候,我们只需要传入文件描述符就可以了,OS会找到当前进程,通过进程里里面的的指针struct files_struct* fs找到相应的结构体,在结构体里面找到该数组,并通过数组下标找到里面的元素,也就是文件的file的指针.通过里面的指针快速找到具体的结构体.这就是OS寻找文件的具体流程.
一切皆文件
上面好象有点问题你说012分别是标准输入输出和错误,要知道我们在之前谈了这三个执行流分别是对应的是键盘,显示器,显示器.要知道这些都是硬件啊,怎么可能用struck file?你是不是在骗我.首先我要说一下,我们上面没有说错,用的就是这一套.此时我们需要在理解一下一切皆文件.前面的博客我们说过了,不过没有细节谈 ,今天就要理解更加深入一点.
请问,如何,如何使用C语言设计类 (面向对象)?这个问题有很大的意思.首先我们要知道C++中的类里面既可以保存成员变量也可以保存成员函数,但是在C语言中结构体只能保存变量.所以我们这么做.
struct file
{
// 对象属性
//函数指针 这个是类型
void (*read)(struct file *filep, int fd....);
void (*write)(struct file *filep, int fd....);
}
这我们在实现一下read和write函数就可以了,当我们实例化结构体时候,我们把这个函数指针初始化一下就可以了.
void read(struct file *filep, int fd....)
{
// 逻辑代码
}
void write(struct file *filep, int fd....)
{
// 逻辑代码
}
注意你看到没,我们第一个参数是是一个文件结构体的指针,这个想不想this指针.C++ 可不是凭空出来的,是由于我们有需求才来的,此时我们就了解了如何使用C语言来形成多态.
可是我们说了这么多,还是美哟有谈硬件是如何参与进去的.此时我们来谈点东西.作为一哥硬件的制造厂商,在保证的质量的之前,他们首先要做的就是给这些硬件提供两个函数,一个是读函数,一个是写函数,或许他们的实现是不一样的,但是接口确实统一的.这个时候就有人疑惑了,键盘不就是一个写的接口,这个是不能读的.我们直接把读接口直接置为NULL不就行了吗.这个就是我们为何可以操作硬件的原因.
去看一下Linux的内核,你就会发现我们的想法是没有错误的,文件结构体file确实给我们嵌套了函数指针.
这里我们要下一个概念,所有(注意是所有)底层的差异可以添加一个软件层来消除,例如我们的进程地址空间, C++ 中基类的多态, 只要传基类的指针或者引用 ,继承本身就是软件层状结构.
分配规则
谈完了其他的,我们这里可以说一下文件描述符的分配规则了.下面的很容易理解为何是3了,这里是由于012被占据了.
int main()
{
umask(0);
int fd = open("log.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);
if(fd < 0)
{
printf("%s\n", strerror(errno));
exit(1);
}
printf("fd %d\n", fd);
return 0;
}
如果我们先关闭0,再重新打开一个文件,再测试一下.
int main()
{
umask(0);
close(0);
int fd = open("log.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);
if(fd < 0)
{
printf("%s\n", strerror(errno));
exit(1);
}
printf("fd %d\n", fd);
close(fd);
return 0;
}
这里就可以了下一个结论了,OS会遍历数组,找到一个最小的没有被使用fd的分配给文件.
理解重定向
上面我们已经理解了文件描述的,此时我们还需要看看下面的情况.我们关闭文件描述符1.
int main()
{
umask(0);
close(1);
int fd = open("log.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);
if (fd < 0)
{
printf("%s\n", strerror(errno));
exit(1);
}
printf("fd %d\n", fd);
close(fd);
return 0;
}
这里我们知道肯定结果肯定是1,为何没有打印出来,这是由于1代表的是标准输出,printf函数是打印到stdout的,或者说是打印到文件描述符1的,也就是1虽然不在指向显示器,但是指向了 log.txt.一定在这个文件中.
我们发现文件中没有,这个是由于printf有缓冲区,但是我们可以变出来.刷新缓冲区(先不谈)
int main()
{
umask(0);
close(1);
int fd = open("log.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);
if (fd < 0)
{
printf("%s\n", strerror(errno));
exit(1);
}
printf("fd %d\n", fd);
fflush(stdout);
close(fd);
return 0;
}
这里我们感觉有点不对,printf不是应该往显示器打印吗?怎么往文件里面打了?这是重定向.这是我们要谈的.
int main()
{
int fd = open("log.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);
if (fd < 0)
{
printf("%s\n", strerror(errno));
exit(1);
}
fprintf(stdout, "%s %d\n", "打开文件成功", fd);
close(fd);
return 0;
}
这里我们先来关闭fd为1的,在进行一次测试.
int main()
{
close(1);
int fd = open("log.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);
if (fd < 0)
{
printf("%s\n", strerror(errno));
exit(1);
}
fprintf(stdout, "%s %d\n", "打开文件成功", fd);
close(fd);
return 0;
}
我们谈了,上面文件中之所以没有,是因为我们没有涮新缓冲区.这里刷新一下.
int main()
{
close(1);
int fd = open("log.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);
if (fd < 0)
{
printf("%s\n", strerror(errno));
exit(1);
}
fprintf(stdout, "%s %d\n", "打开文件成功", fd);
fflush(stdout);
close(fd);
return 0;
}
先不解释,等到谈完缓冲区在来.先来解释printf应该往显示器打印吗?是的.但是这里为何往文件中打印了.这里和大家说一下,与其说是往显示器中打印,不如说是往1中.上面的现象就是重定向的原理.
重定向的本质就是我们打开一个文件,把这个文件得额fd覆盖原本的012,这样OS不会关心,也不想知道它往哪打印.例如fprintf是不知道的 ,他只知道往1中写就可以了,对于其他的动作不关心,也不知道.上层只认fd,不关心fd里面保存的元素是什么.我们可以通过改变fd指向的指针,就可以完成重定向.这就是重定向的本质.
dup2
上面我们已经知道原理,可是我们不会这么挫吧,还要一个一个关闭fd,感觉有点麻烦.注意上面都是操作系统内核做的,我们一个以一个关闭太麻烦 ,而且OS也是不想让我们这么做的,所以它必定会提供接口.这里是dup2接口,dup太简单,我们不学.
[bit@Qkj 12_05]$ man dup2
这个函数有很大的问题,准确说是有点反人类,我们先看文件的描述.
dup2() makes newfd be the copy of oldfd, closing newfd first if necessary, but note the following:
* If oldfd is not a valid file descriptor, then the call fails, and newfd is not closed.
* If oldfd is a valid file descriptor, and newfd has the same value as oldfd, then dup2() does nothing, and returns newfd.
读一读上面的,意思把一个老的fd拷贝到新的fd中.那么请问拷贝是什么,整数下标对应的元素,但是我们只需要传fd就可以了.
输出重定向
再来问问,谁older,假设我们要实现输出重定向,我们先想一想,我们要完成输出重定向,也就是把输出fd=1的数据输出到fd=3中,但是上层会认为自己仍旧会把数据打到fd=1中,也就是我们需要把fd=3的元素拷贝到fd=1中.那么oldfd就是新打开的文件的fd,新的就是fd=1
dup2(fd, 1); // 这么传
这里有点反反直觉 ,不过这么用就可以了
int main()
{
int fd = open("log.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);
if (fd < 0)
{
printf("%s\n", strerror(errno));
exit(1);
}
dup2(fd, 1); // 这么传
printf("打开文件成功 fd %d\n", fd);
return 0;
}
注意这个函数也是有返回值的.
On success, these system calls
return the new descriptor. On
error, -1 is returned, and errno
is set appropriately.
这里也测试一下.
int main()
{
int fd = open("log.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);
if (fd < 0)
{
printf("%s\n", strerror(errno));
exit(1);
}
int ret = dup2(fd, 1);
if (ret >= 0)
{
close(fd);
}
printf("ret %d\n", ret);
printf("打开文件成功 fd %d\n", fd);
return 0;
}
这里解释一下我们为何在重定向成功后关闭 fd,只要我们重定向f成功了,d就没有意义了.我们只想要1,销毁空间就是把指针给放弃掉就可以了,这里我们可以肯定的是文件结构体体中肯定会有一个引用计数来记录被打开的次数.
追加重定向
这个不是很简单的吗,我们在打开文件的时候标志位传入追加就可以了.
int main()
{
int fd = open("log.txt", O_WRONLY | O_CREAT | O_APPEND, 0666);
if (fd < 0)
{
printf("%s\n", strerror(errno));
exit(1);
}
int ret = dup2(fd, 1);
if (ret >= 0)
{
close(fd);
}
printf("ret %d\n", ret);
printf("打开文件成功 fd %d\n", fd);
return 0;
}
输入重定向
输入重定向本质是原本从键盘中读,此时你我们让他从文件中读取.这个我们也可以使用dup2.
int main()
{
int fd = open("log.txt", O_RDONLY | O_CREAT, 0666);
if (fd < 0)
{
printf("%s\n", strerror(errno));
exit(1);
}
int ret = dup2(fd, 0);
if (ret >= 0)
{
close(fd);
}
char buffer[64];
while (fgets(buffer, sizeof(buffer), stdin) != NULL)
{
printf("%s\n", buffer);
}
return 0;
}
缓冲区
我们在自己实现进度条的时候都见见识过缓冲区的现象了,哪个时候我们一直没有谈原理,只是和大家说了一种解决办法,这是由于我们那时没有办法说,此时我们将正式的认识缓冲区.
什么是缓冲区
缓冲区就是一块空间,我们把数据临时保存在那里.
为何要有缓冲区
我们举一个例子,假设你在云南农业大学,你的朋友在北京.你近期看了几本书,非常好.要把书给你同学送去.你有两个选择,一是你自己送 ,自己乘坐火车等交通工具,耗费时间长,耽误你的时间.而是你的学校里面有一个快递点,你把书给站点,快递点一拿到你的书就给你朋友送过去.等到了让朋友拿走就可以了,此时书到你朋友那里的时间是固定的,快递点最大的价值是解放你的时间.此时快递点就是缓冲区 .
- 解放进程的时间
- 多送几本,等到书多了,快递点同一配送,集中处理数据刷洗,减少时间IO
今天我们呢只关注第一点.
缓冲区在哪里
我们谈了恨多,但是你还是没有何和我们说缓冲区在那里?用户层有缓冲区,内核也有.那么我们谈的是哪一个 缓冲区?代码验证一下
int main()
{
printf("hello printf\n");
const char *msg = "hello write\n";
write(1, msg, strlen(msg));
sleep(5);
return 0;
}
大家在看一下下面的代码.此时你会发现不一样的结果.
int main()
{
printf("hello printf"); // stdout 中写
const char *msg = "hello write";
write(1, msg, strlen(msg));
sleep(5);
return 0;
}
这个等待 动图
你发现hello write立马出来,hello printf却不是.我们知道printf底层就是封装了write,也就是printf中一定会有缓冲区 ,不带\n是没有办法刷新缓冲区的.write是立即刷新的,也就是它没有缓冲区 (先不谈内核的缓冲器).这个时候我们就会明白我们之前谈的缓冲区不是内核级别的,只是语言级别的,是C语言提供的.也就是说我们在C语言中,我们每打开一个文件就会有一个FILE*指针返回,此时对于每一个FILE结构体,每一个都有一个独属于自己的缓冲区.
刷新策略
这里存在两个问题,什么时候刷新?如果在刷新之前关闭fd会怎么样?我们先来讨论第二个问题.
int main()
{
printf("hello printf"); // stdout 中写
const char *msg = "hello write";
write(1, msg, strlen(msg));
close(1);
sleep(5);
return 0;
}
这个时候我们在sleep之前把1给关掉了,此时当进程退出后,原本存在缓冲区的数据就会丢失.
现在我们可以说一下缓冲区的刷新策略,常见的一般分为三种
- 无缓冲 立即刷新
- 行缓冲 逐行刷新,遇到’\n’刷新
- 全缓冲 缓冲区满了刷新youc
还有几个比较特殊的
- 进程结束 有的语言会强制
- 用户强制 fflush 不关心
那么你说了这么多,有哪些情况符合上面的呢?一般而言,显示器是行缓冲,快设备(例如磁盘文件)是全缓冲.
int main()
{
const char *str1 = "hello printf\n";
const char *str2 = "hello fprintf\n";
const char *str3 = "hello fputs\n";
const char *str4 = "hello write\n";
printf("%s", str1);
fprintf(stdout, "%s", str2);
fputs(str3, stdout);
write(1, str4, strlen(str4));
fork();
return 0;
}
没有问题,我们这里这里重定向一下,你将会发现一个神奇的事情.
先来你解释为何是4条.这个时候我们就要明白了,显示器是行缓冲,在想显示器打印过之后,这个数据就直接清空了.这里就是为何是4条的原因.即使fork后没有数据可以被打印.
对于7条我这样解释.如果我们重定向了这里变成了磁盘文件,所以是全缓冲,也就是这里不会立即刷新到文件中,fork父子进程里面结束了,此时假设父进程先退出,缓冲区是FILE维护的,属于父进程内部的数据区域 刷新数据时子进程会发生写时拷贝,其中里面的数据也会拷贝一份,等到子进程退出后也会刷新到文件中,这里解释了为何是6条.此时我们再看,对于第七条你会发现write只有一条,我们可以这么理解,write是无缓冲,在fork前就刷新出去了,所以不会打印.
我们拷贝的是task_struct,里面保存的是一个地址,这个地址不会改变,也就不会发生写时拷贝,当父进程刷新时,此时fd位置元素指针指向的内容已经被清空了,这就是为何是1条的原因.
模拟实现
我们还是有点不太理解,这里我们手写一个缓冲区,注意,这是按照我自己想的写的,不一定和内核中的一样.
int main()
{
MyFILE *pf = my_fopen();
if (pf = NULL)
{
printf("my_fopen errno\n");
return 1;
}
char *s = "hello my file\n";
my_fwrite(pf, s, strlen(s));
my_fclose(pf);
return 0;
}
看下,这是我们需要实现的功能,等会里面加一个 刷新的函数,我们先来把结构体给定义出来.
#define NUM 1024
#define NONE_FLUSH 0x0 // 无缓冲
#define LINE_FLUSH 0x1 // 行缓冲
#define FULL_FLUSH 0x2 // 全缓冲
typedef struct MyFILE
{
int _fd;
char _buffer[NUM];
int _flags; // 刷新策略
int _end; // buffer的结尾
} MyFILE;
下面我们就可以实现它的函数了.
对于fopen函数,这里我们需要传入的是文件名和打开的方式,注意这个打开方式算是我上面说的给你们模拟实现的.这里我们默认这个文件是不存在的,同时默认的刷新方式是行缓冲.
MyFILE *my_fopen(char *filename, char *mode)
{
assert(filename);
assert(mode);
int flags = O_RDONLY;
if (strcmp(mode, "r") == 0)
{
}
else if (strcmp(mode, "r+") == 0)
{
}
else if (strcmp(mode, "w") == 0)
{
flags = O_WRONLY | O_CREAT | O_TRUNC;
}
else if (strcmp(mode, "w+") == 0)
{
}
else if (strcmp(mode, "a") == 0)
{
flags = O_WRONLY | O_CREAT | O_APPEND;
}
else if (strcmp(mode, "a+") == 0)
{
}
int fd = open(filename, flags, 0666);
if (fd < 0)
return NULL;
MyFILE *pf = (MyFILE *)malloc(sizeof(MyFILE));
if (pf == NULL)
return NULL;
memset(pf, 0, sizeof(MyFILE));
pf->_fd = fd;
pf->_end = 0;
pf->_flags = LINE_FLUSH;
return pf;
}
现在我们要做的就是写入.这个是非常简单的.你会发现我们这里面有一个函数你是不认识的, syncfs这个是因为write是写入到内核中,我们这个函数可以把内容打到硬盘中,这里不错解释,会用就行.
void my_fwrite(MyFILE *pf, char *buffer, int len)
{
assert(pf);
assert(buffer);
assert(len >= 0);
// 写入到缓冲区 认为 不会填满
strncpy(pf->_buffer + pf->_end, buffer, len); // 写入到 buffer中的_end的位置
pf->_end += len;
// 开始 刷新
if (pf->_flags & NONE_FLUSH)
{
}
else if (pf->_flags & LINE_FLUSH)
{
// 只要是行缓冲肯定是 '\n'
// abcd\n
if (pf->_end > 0 && pf->_buffer[pf->_end - 1] == '\n')
{
// 给我刷新 写入到内核中
write(pf->_fd, pf->_buffer, pf->_end);
pf->_end = 0;
syncfs(pf->_fd);
}
}
else if (pf->_flags & FULL_FLUSH)
{
}
}
好了现在我们就可以关闭文件了,不过我们在关闭文件之前需要刷新一下缓冲区i,此时就要一个强制刷新.
void my_fflush(MyFILE *pf)
{
assert(pf);
if (pf->_end > 0)
{
write(pf->_fd, pf->_buffer, pf->_end);
pf->_end = 0;
syncfs(pf->_fd);
}
}
void my_fclose(MyFILE *pf)
{
assert(pf);
my_fflush(pf);
close(pf->_fd);
free(pf);
}
我们这里就可以测试一下上面我们写的代码了.
int main()
{
MyFILE *pf = my_fopen("log.txt", "w");
if (pf == NULL)
{
printf("my_fopen errno\n");
return 1;
}
char *s = "hello my file\n";
my_fwrite(pf, s, strlen(s));
printf("消息已经刷新");
char *ss = "hello my file 1";
my_fwrite(pf, ss, strlen(ss));
printf("写了一个不满足刷新的字符串\n");
sleep(5);
char *sss = "hello my file 2";
my_fwrite(pf, sss, strlen(sss));
printf("写了一个不满足刷新的字符串\n");
sleep(5);
my_fclose(pf);
return 0;
}
此时我们就可以解决上面的问题了,我们模拟一下进程退出.这里解释为何是两次的原因,由于我们模拟的是进程的推处,也就是强制刷新,此时父子进程发生写时拷贝呢,故所以是两行.
int main()
{
MyFILE *pf = my_fopen("log.txt", "w");
char *s = "aaaaaaa";
my_fwrite(pf, s, strlen(s));
fork();
my_fclose(pf);
return 0;
}
那么下面为何又是一行了,这里我们是因为有’\n’,这里就以在父进程之前就刷新了.
int main()
{
MyFILE *pf = my_fopen("log.txt", "w");
char *s = "aaaaaaa\n";
my_fwrite(pf, s, strlen(s));
fork();
my_fclose(pf);
return 0;
}
关于shell扩展的,我们先不谈,等到后面的几个博客再说吧.
分析 cerr & cput
我们前面谈到,标准输出和标准错误都是打印在显示器上,那么这个所谓的显示器有什么区别?
int main()
{
// stdout
printf("hello printf 1\n"); // 1
fprintf(stdout, "hello fprintf 1\n");
fputs("hello fputs 1\n", stdout);
// stderr
fprintf(stderr, "hello fprintf 2\n");
fputs("hello fputs 2\n", stderr);
perror("hello perror 2");
// cout
cout << "hello cout 1" << endl;
// cerr
cerr << "hello cerr 2" << endl;
return 0;
}
但是如果我们使用一下输出重定向,此时你就会发现点有意思的,你会发现在显示器其中有关于标准错误的打印在屏幕中.
[bit@Qkj 11_15]$ ./a.out > stdout.txt
我们看看这个重定向的我文件.
[bit@Qkj 11_15]$ cat stdout.txt
这里我们就可以得到一个结论,虽然标准输出和标准错误都是显示器,是同一个设备 ,但是它们互不干扰.
如果我们想要把不同的输入到不同的文件中呢?这样就可以了
那么我们就有点问题,意义在哪里.这是由于日志信息的级别是不同的,有的是普通的打印,有的是致命的,.所以对我们来说我们需要把日志给区分开来. 但是如果我们就是不想把他们分开,我们该如何做呢?
那么OS是如何做到的,这里就是重定向的,也就是dup(fd,1),dup(fd,2),仅此而已.
模拟实现perror
上面我们perror的时候出现了一个Success
int main()
{
perror("hello perror 2");
return 0;
}
,这个是因为C语言有一个全局的变量,这个变量记录调用成功或者失败的原因.
我们模拟实现一下就可以了.
void my_perror(const char *p)
{
printf("%s: %s\n", p, strerror(errno));
}
int main()
{
int fd = open("log.txt", O_RDONLY); // 必然失败,没有这个函数
if (fd < 0)
{
// 失败后 errno会被设置
my_perror("open");
perror("open");
}
return 0;
}