一.回顾C语言中的文件接口
//写文件 fwrite
#include <stdio.h>
#include <string.h>
int main()
{
FILE *fp = fopen("txt", "w");
if(!fp)
{
printf("fopen error!\n");
}
const char *str = "hello,fwrite\n";
int count = 3;
while(count--)
{
fwrite(str, strlen(str), 1, fp);//strlen(msg)是一个元素占的字节,1是代表有几个元素,函数返回值是成功写入文件的元素个数,不会超过函数中的元素个数
}
fclose(fp);
return 0;
}
//读文件 fread
#include <stdio.h>
#include <string.h>
int main()
{
FILE *fp = fopen("txt", "r");
if(!fp)
{
printf("fopen error!\n");
}
char buf[1024];
const char *str = "hello ,fread!\n";
while(1)
{
ssize_t s = fread(buf, 1, strlen(str), fp);
if(s > 0)
{
buf[s] = 0;
printf("%s", buf);
}
if(feof(fp))
{
break;
}
fclose(fp);
return 0;
}
文件存在于磁盘,磁盘是一个硬件设备,所以程序里虽然调用的是fwrite和fread,但实际上还是由操作系统来真正的完成的读写操作。
二.Linux提供系统调用完成基本的文件操作
1.通过系统调用来完成基本的文件操作。
通过接口(可通过man手册来查看):
- open
- int open(const char *pathname, int flags);
//pathname是文件路径(绝对路径或者相对路径)。flags是打开方式,注意是一个整型。返回值表示文件描述符,是一个句柄(数字方式表示)。
O_RDONLY: 只读打开
O_WRONLY: 只写打开
O_RDWR : 读,写打开
//上面三个常量,必须指定一个且只能指定一个
O_CREAT : 若文件不存在,则创建它。需要使用mode选项,来指明新文件的访问权限
O_APPEND: 追加写- int open(const char *pathname, int flags, mode_t mode)//mode:表示文件的权限,八进制整数,与chmod设置规则相同。注意与上面函数不是重载,但是C语言中可以通过某些黑科技手段来实现,涉及到手写汇编。
- int creat(const char *pathname, mode_t mode);
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
int main()
{
int fd=open("./code.txt",O_RDONLY);//只读
printf("%d\n",fd);
return 0;
}
在没有创建这个文件的时候,ret会返回-1。在这里我随便创建了一个1.txt文件,再次编译执行后,fd的值等于3,那这个3怎么来的?
关于前面提到的进程控制块(PCB)中包含的内容,有一点没有做出详细介绍,那就是文件描述符表,这个里面包含了一个个的结构体指针:struct file*,(注意这里是小写的file*,是操作系统内核中定义的结构体指针,大写的FILE*是标准库中定义的结构体指针),每个指针指向一个文件结构体struct file(用来描述文件的信息)
所以fd返回值3是文件描述符表的下标(文件描述符),对应的就是1.txt的相关结构。
又问:为什么fd是从3开始?换句话说为什么我的1.txt对应fd=3?为什么不对应0,1,2?
因为进程启动时先会打开三个文件:标准输入(stdin)、标准输出(stdout)、标准错误(stderr),它们依次占用了0,1,2。后续打开的文件就会从3开始往后排。
通过代码来验证一下:
int main()
{
printf("stdin=%d\n",stdin->_fileno);//标准输入对应的文件描述符
printf("stdout=%d\n",stdout->_fileno);
printf("stderr=%d\n",stderr->_fileno);
int fd=open("./1.txt",O_RDONLY);//只读
printf("fd=%d\n",fd);
return 0;
}
打印结果:
- read
下面就开始来读一个文件,需要用到函数read
#include <unistd.h>
ssize_t read(int fd, void *buf, size_t count);
//fd--所读的文件符
//buf--文件从磁盘读入内存的位置
//count--缓冲区有多长。所读的范围不能超出缓冲区,超出就会导致内存访问越界
//ssize_t--返回值类型:有符号的长整形,表示read真实读到的长度
读我的1.txt文件,注意read的第三个参数要穿sizeof[buf]-1,因为当文件的大小大于buf的长度时,缓冲区会放满,而read这个函数最终读出来的是一个C风格的字符串,最后一位肯定是’\0’,这样的话就没有地方放‘\0’了,所以要给它留个位置。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
int main()
{
int fd=open("./1.txt",O_RDONLY);//只读
printf("fd=%d\n",fd);
if(fd<0)
{
perror("open");
return 1;
}
char buf[1024]={0};//缓冲区
ssize_t ret=read(fd,buf,sizeof(buf)-1);//C风格字符串,留一个位置放'\0'
buf[ret]='\0';
printf("%s\n",buf);
close(fd);
return 0;
}
执行结果如下,完美地打印出了文件的内容:
- write 写文件
#include <unistd.h>
ssize_t write(int fd, const void *buf, size_t count);
//fd--所读的文件符
//buf--字符串从内存写入磁盘的位置
//count--写入字符串的长度
//ssize_t--返回值类型:有符号的长整形,表示write真实写入的长度,当磁盘满的时候,写入长度会小于count
注意要将前面open中的参数变成读和写打开,如果只打开写,则写入的时候会覆盖文件原来的内容。
int main()
{
int fd=open("./1.txt",O_RDWR);//读写
printf("fd=%d\n",fd);
if(fd<0)
{
perror("open");
return 1;
}
char buf1[1024]="hello 1.txt";
ssize_t n=write(fd,buf1,strlen(buf1));
printf("%ld\n",n);
perror("write");
close(fd);
return 0;
}
执行结果:
再看看原来的文件内容:已经成功写入
三.文件描述符表的下标(文件描述符)分配规则
之前就已经试过,可以通过打印stdout->_fileno来打印标准输出对应的文件描述符,那么如果在这个前面加加一个close呢?我们来试一下。
#include <stdio.h>
#include <unistd.h>
int main()
{
close(1);
printf("%d\n",stdout->_fileno);
return 0;
}
结果是什么都打印不了,说明close(1)把stdout对应文件描述符强制关闭了,所以stdout就一定会失败,显示器上就不能输出任何东西。
好,那么现在在stdout失败的情况下:如果我要打印一个文件的文件描述符,应该怎么打印?答案是:借助标准错误,fprintf往标准错误上输出fd。
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
int main()
{
close(1);
int fd =open("1.txt",o_WRONLY);//只写
fprintf(stderr,"fd=%d\n",fd);//stderr是文件指针
return 0;
}
结果是fd=1,哪来的?为什么?这涉及到了文件描述符表的下标分配规则。
文件描述符表的下标分配规则:每次打开一个文件的时候,会从文件描述符表的开始位置一次往后找,找到第一个空闲的下标位置,就用这个下标表示新的文件。
这就可以解释之前的疑惑了:当close将stdout的文件描述符关掉了,所以此时在文件描述符表里,下标为1的位置已经空闲了,当打开一个文件时,系统就会将这个下标分配给打开的文件对应的文件描述符。所以打印出来的fd=1.
四.重定向
那么此时再利用printf进行输出会有什么效果?我在后面再加一行。
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
int main()
{
close(1);
int fd =open("1.txt",o_WRONLY);//只写
fprintf(stderr,"fd=%d\n",fd);//stderr是文件指针
printf("new print:%d\n",stdout->_fileno);
return 0;
}
结果是显示屏上什么都没有。但是在上面打开的文件里有了新内容,那么这个又是怎么发生的呢?
一下这两句话是等价的,虽然文件描述符1对应的文件不再是stdout,当文件描述符1的指向为1.txt文件时,stdout就会输出到文件描述符所对应的文件中。这个过程简而言之就是把本来要输出到显示器上的内容输出到了1.txt文件中,就个过程就是重定向。
printf("new print:%d\n",stdout->_fileno);
fprintf(stdout,"new print:%d\n",stdout->_fileno);
现在再来回顾一下刚刚整个重定向的实现过程,先通过close关闭某个文件描述符所指向的文件,再通过open函数来让这个文件描述符重新指向某个文件。这是重定向的第一种方法,这种方法比较依赖操作系统的底层行为,当操作系统版本不同时,重定向的过程也都会受到影响。
重定向的第二种方法
系统函数:dup
//oldfd–旧文件描述符
int dup(int oldfd);
int dup2(int oldfd, int newfd);//让新成为旧的一份拷贝,往新的里面写就相当于往旧的里面写
dup(1,3):1–old,3—new, 让文件描述符3的指向变成与文件描述符1的指向一模一样。
五.缓冲区的角色
下面再来看一段代码,这三条语句都是往显示屏输出一段字符串,
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <string.h>
int main()
{
printf("hello printf\n");
fprintf(stdout,"hello fprintf\n");
write(1,"I love my family\n",strlen("I love my family\n"));
return 0;
}
结果自然是输出没问题。
如果重定向改成输出到文件呢?
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <string.h>
int main()
{
printf("hello printf\n");
fprintf(stdout,"hello fprintf\n");
write(1,"I love my family\n",strlen("I love my family\n"));
return 0;
}
打印结果就变成了这样:
经过走访多篇博文,终于得出答案:输出到显示器和输出到文件时,两种情况对应的缓冲区的策略不一样,所以输出的顺序会不一样。
代码中的write方法没有缓冲区,所以调用write时,会将里面的内容直接写入文件。而虽然printf和fprintf是带有缓冲区的,虽然它们先执行,但是这两个函数会先将数据放入缓冲区,直到缓冲区满才会刷新。
常见的缓冲策略:
- 无缓冲:write等函数系统调用。
- 行缓冲:遇到\n就刷新,或者缓冲区满才刷新,或者手动刷新。(打印到显示器)
- 全缓冲:一直到缓冲区满才会刷新,或者手动刷新。(输出到文件)
加入我再加入一个fork(),结果会是怎样?
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <string.h>
int main()
{
printf("hello printf\n");
fprintf(stdout,"hello fprintf\n");
write(1,"I love my family\n",strlen("I love my family\n"));
fork();
return 0;
}
fork出来的子进程会拷贝一份父进程的PCB和虚拟地址空间(位于内存),printf和fprintf的输出的内容位于缓冲区(是内存上的一份空间),所以父子进程各有一份缓冲区的内容,当main函数退出,父子进程结束时,也会刷新缓冲区,就会打印两次printf和fprintf中的内容。
六.动态库和静态库
动态链接库:把一些.c或者.cpp文件编译生成的一种特殊的二进制程序,自身不能直接指向,但是可以被其他可执行程序调用。
应用场景:客户端更新的时候不必更新整个程序,而是更新其中的一部分模块,其中的模块就是以动态库的方式组织的。
静态链接库:把一些.o文件打包到一起,生成了一种特殊的二进制程序,自身不能直接指向,但是可以和其他的.c/。cpp文件编译生成一个新的可执行程序。那么这个新的可执行程序就可以单独发布。
应用场景:发布小程序时,可以使用静态库的方式编译生成一个单独的可执行程序且不依赖其他的库,发布较为方便。
生成一个静态库时:
- 注意要将所有的.c文件一起编译
- makefile命名规则:lib是前缀,.a 后缀(静态库),.so后缀(动态库)
ldd命令查看系统的动态库信息
LD_LIBRARY_PATH 设定环境变量体系系统去哪些目录中查找动态库