C语言中的文件读写方法有很多,这里就稍微说一下。忘记了的看我的C语言文件操作博客链接跳转.
我们打开一个文件进行读写操作,C语言中用的FILE结构体。
//写操作
#include
#include
int main()
{
FILE *fp = fopen("log.txt", "w");
//这里可以r w a 对应读、写、追加操作
if(!fp){
printf("fopen error!\n");
}
const char *msg = "hello bit!\n";
int count = 5;
while(count--){
fwrite(msg, strlen(msg), 1, fp);//二进制写入字符串
}
fclose(fp);
return 0;
}
//读操作
#include
#include
int main()
{
FILE *fp = fopen("log.txt", "r");
if(!fp){
printf("fopen error!\n");
}
char buf[1024];
const char *msg = "hello bit!\n";
while(1){
//注意返回值和参数,此处有坑,仔细查看man手册关于该函数的说明
ssize_t s = fread(buf, 1, strlen(msg), fp);
if(s > 0){
buf[s] = 0;
printf("%s", buf);
}
if(feof(fp)){
break;
}
}
fclose(fp);
return 0;
}
这里有需要注意的一个细节
这里读写的路径,是当前路径,这里提出一个问题,一个空文件占内存空间嘛?
答案:是肯定的。
每个进程,都有一个内置属性,所以文件 = 内容 + 属性
任何的C程序都会默认打开三个文件,分别叫做标准输入stdin,标准输出stdout,标准错误stderr,他们分别对应键盘文件、显示器文件、显示器文件,仔细观察发现,这三个流的类型都是FILE*, fopen返回值类型,文件指针。
提到这里,可能就有人会问了,为什么硬件也是文件?
1、其实所有的外设硬件,本质的核心操作都是read和write。
2、不同的硬件,对应的读写方式不一样
底层其实是通过一个双链表链接起来的,都有对应的读写操作,所以在Linux下,一切皆文件。
在C语言中,我们也可以直接对stdin,stdout,stderr进行读写。比如fprintf、fscanf。
操作文件,除了上述C接口(当然,C++也有接口,其他语言也有),我们还可以采用系统接口来进行文件访问。在此之前,我们先站在OS角度,来看进程和文件的关系。
我们先来了解如何使用系统调用接口。
man 2 open
#include
#include
#include
int open(const char *pathname, int flags);
int open(const char *pathname, int flags, mode_t mode);
pathname: 要打开或创建的目标文件
flags: 打开文件时,可以传入多个参数选项,用下面的一个或者多个常量进行“或”运算,构成flags。
参数:
O_RDONLY: 只读打开
O_WRONLY: 只写打开
O_RDWR : 读,写打开
这三个常量,必须指定一个且只能指定一个
O_CREAT : 若文件不存在,则创建它。需要使用mode选项,来指明新文件的访问权限
O_APPEND: 追加写
返回值:
成功:新打开的文件描述符
失败:-1
mode_t理解:文件权限
open 函数具体使用哪个,和具体应用场景相关,如目标文件不存在,需要open创建,则第三个参数表示创建文件的默认权限,否则,使用两个参数的open
写操作
1 #include <stdio.h>
2 #include <sys/types.h>
3 #include <sys/stat.h>
4 #include <fcntl.h>
5 #include <unistd.h>
6 #include <string.h>
7 int main()
8 {
9 umask(0);
10 int fd = open("myfile", O_WRONLY|O_CREAT, 0644);
11 if(fd < 0){
12 perror("open");
13 return 1;
14 }
15 int count = 5;
16 const char *msg = "hello bit!\n";
17 int len = strlen(msg);
18 while(count--){
19 write(fd, msg, len);//fd: 后面讲, msg:缓冲区首地址, len: 本次读取,期望写入多少个字节的数据。 返回值:实际写了多少字节数据
20 }
21 close(fd);
22 return 0;
23 }
1 #include <stdio.h>
2 #include <sys/types.h>
3 #include <sys/stat.h>
4 #include <fcntl.h>
5 #include <unistd.h>
6 #include <string.h>
7 int main()
8 {
9 int fd = open("myfile", O_RDONLY);
10 if(fd < 0){
11 perror("open");
12 return 1;
13 }
14 char buffer[1024];
15 const char* msg = "hello bit!\n";
16 while(1)
17 {
18 size_t s = read(fd, buffer, strlen(msg));
19 if(s > 0) printf("%s", buffer);
20 else break;
21 }
22 // int count = 5;
23 // const char *msg = "hello bit!\n";
24 // int len = strlen(msg);
25 // while(count--){
26 // write(fd, msg, len);//fd: 后面讲, msg:缓冲区首地址, len: 本次读取,期望写入多少个字节的数据。 返回值:实际写了多少字节数据
27 // }
28 close(fd);
29 return 0;
30 }
第二位的操作增加append即可
每个语言本身都有自己的语法规则,系统调用使用成本较高,而且不具备可移植性。
在认识返回值之前,先来认识一下两个概念: 系统调用 和 库函数
0 1 2 已经被占用了,所以新创建的普通文件对应的fd是3。
所有的文件,如果要被使用,首先必须被打开。一个进程可以打开多个文件,系统内被打开的文件,一定是有多个的。那么这些多个文件,也是需要被操作系统管理起来的。先描述,在组织。这里就是一个结构体struct file封装的,里面装了目标文件的基本操作与部分属性(文件名、创建时间、文件大小等等)。
而现在知道,文件描述符就是从0开始的小整数。当我们打开文件时,操作系统在内存中要创建相应的数据结构来描述目标文件。于是就有了file结构体。表示一个已经打开的文件对象。而进程执行open系统调用,所以必须让进程和文件关联起来。每个进程都有一个指针files,指向一张表files_struct*,该表最重要的部分就是包涵一个指针数组,每个元素都是一个指向打开文件的指针!所以,本质上,文件描述符就是该数组的下标。所以,只要拿着文件描述符,就可以找到对应的文件
发现结果是: fd: 0可见,文件描述符的分配规则:在files_struct数组当中,找到当前没有被使用的最小的一个下标,作为新的文件描述符
探究重定向的本质前,我们先来看一下重定向的现象
1 #include <stdio.h>
2 #include <sys/types.h>
3 #include <sys/stat.h>
4 #include <fcntl.h>
5 #include <stdlib.h>
6 int main()
7 {
E> 8 close(1);
9 int fd = open("myfile", O_WRONLY|O_CREAT, 00644);
10 if(fd < 0){
11 perror("open");
12 return 1;
13 }
14 printf("fd: %d\n", fd);
15 fflush(stdout);
E> 16 close(fd);
17 exit(0);
18 }
此时,我们发现,本来应该输出到显示器上的内容,输出到了文件 myfile 当中,其中,fd=1。这种现象叫做输出重定向。常见的重定向有:>, >>, <
那重定向的本质是什么?
还有一个问题:我们 printf 的时候,为什么要加fflush(stdout)强制刷新缓冲区?
函数原型如下:
#include
int dup2(int oldfd, int newfd);
我们通过man函数看一下使用规则
newfd是oldfd的一份拷贝,拷贝的不是fd,而是拷贝fd对应数组中的内容,所以oldfd指向新创建的普通文件,
输出重定向:
1 #include <stdio.h>
2 #include <string.h>
3 #include <unistd.h>
4 #include <fcntl.h>
5 int main()
6 {
7 int fd = open("log.txt", O_CREAT | O_RDWR);
8 if (fd < 0) {
9 perror("open");
10 return 1;
11 }
12 //close(1);
13 dup2(fd, 0);
14 const char* msg = "hello world\n";
15 ssize_t write_size = write(1, msg, strlen(msg));
16 //char buf[1024] = {0};
17 //ssize_t read_size = read(0, buf, sizeof(buf) - 1);
18 if (write_size < 0) {
19 perror("read");
E> 20 return;
21 }
22
23 //printf("%s", buf);
24 fflush(stdout);
25
26 return 0;
27 }
输入重定向:
1 #include <stdio.h>
2 #include <unistd.h>
3 #include <fcntl.h>
4 #include <string.h>
5 int main()
6 {
7 int fd = open("myfile.txt", O_CREAT | O_RDWR);
8 if (fd < 0) {
9 perror("open");
10 return 1;
11 }
12 dup2(fd, 1);
13 char buf[1024] = {0};
14 ssize_t read_size = read(0, buf, sizeof(buf) - 1);
15 if (read_size < 0) {
16 perror("read");
E> 17 return;
18 }
19 printf("%s", buf);
20 fflush(stdout);
21
22 return 0;
23 }
那么这里提一个问题,程序替换的时候,会不会影响重定向对应的数据几个和数据?
当然是不会的,任务控制块task_struct里面的指针分别指向进程地址空间和文件结构体struct files_struct,各执行各的流程,互不影响
刚刚上面重定向讲解了为什么需要fflush(stdout),因为printf是自带缓冲区的,fopen、fwrite等函数也是自带缓冲区的,所以FILE结构体里面也存在缓冲区。
struct FILE 内部包含:
1、底层对应的文件描述符下标
2、应用层,C语言提供的缓冲区数据
fflush(stdout)函数原型:int fflush(FILE *stream); FILE里面的stdout还存在,stdout->1。
关闭了1,此时指向普通文件,但是数据仍在应用层的缓冲区中待着,所以我们需要强制刷新缓冲区,把缓冲区的数据刷新到对应的普通文件中。
我们再来看一个细节
fprintf(stdout,"hello bit\n");
fprintf(stderr, "hello world");
都是往显示器上打印,可以打印出来,此时我们重定向一下,在来看看现象?
很神奇吧?为什么log.txt的内容只有hello bit ?
1指向的stdout,2指向的stderr,此时输出重定向1指向普通文件,改变的是1,当然 2 继续向显示器打印。
- 数据不是直接写入到磁盘
- 而是通过 fd->task_struct->files_struct->对应文件->系统缓冲区
- OS有自己的刷新策略和时机
- 为什么用户层不直接和内核打交道,代价太大(王婆介绍媳妇的故事),通过一些列机制访问缓冲区(就像磁盘有自己的磁盘驱动)
因为数据此时的刷新方式是行刷新,每行数据后面都有\n,所以就刷新出数据了,此时在fork也就没有了意义。
此时数据的刷新方式由行刷新变成了全缓冲,单独一个\n不能填满缓冲区,数据仍然留在缓冲区中,此时发生了写时拷贝,数据拷贝了一份,此时普通文件中也就两份数据了。
write是系统调用,没有缓冲区,数据不管在显示器打印还是输出重定向到普通文件中,都只存在一份。
这里结合了:缓冲区 + 写时拷贝 + 数据刷新策略 + 文件差异
定义:打开的文件,其属性与操作的方法就在struct file{},属于内存级文件,进程。普通未打开的文件,在磁盘上面,未被加载到内存,属于程序。打开的文件需要被管理,未打开的文件也需要被管理,这些文件都需要由OS文件系统进行管理。
先来了解文件系统?
我们知道文件是存在硬盘上面的,也就是所谓的磁盘,我们可以把磁盘看做一个线性的空间,有一个一个的分区(电脑上的C盘、D盘、E盘)。
我们来看看具体化的文件系统
从上图我们可以得知一个文件系统
我们可以通过stat命令来看一下一个文件存储在哪里的,下面是对应的文件系统的内容。
那么属性和数据分离到底是怎么工作的呢?我们先来创建一个文件。
[root@localhost linux]# touch abc
[root@localhost linux]# ls -i abc
263466 abc
我们看到,真正找到磁盘上文件的并不是文件名,而是inode。 其实在linux中可以让多个文件名对应于同一个inode。
我们通过ln 命令创建对应硬链接,在通过ll -i -a -l命令可以发现硬链接是对源文件的一份拷贝,iNode的数量+1
tmp.txt和hard_tmp链接状态完全相同,他们被称为指向文件的硬链接。内核记录了这个连接数,inode656043的硬连接数为2。
我们在删除文件时干了两件事情:1.在目录中将对应的记录删除,2.将硬连接数-1,如果为0,则将对应的磁盘释放。
硬链接是通过inode引用另外一个文件,软链接是通过名字引用另外一个文件,在shell中的做法。
总结:
1、硬链接相当于是一份拷贝,对应源文件的iNode数+1即可,删除源文件时,该文件不受到影响,对应iNode-1。
2、软连接是一个普通文件,有自己的iNode,内容存的对应源文件的路径,软链接相当于是快捷方式。
命令:
生成静态库
[root@localhost linux]# ar -rc libmymath.a add.o sub.o
ar是gnu归档工具,rc表示(replace and create)
查看静态库中的目录列表
[root@localhost linux]# ar -tv libmymath.a
rw-r--r-- 0/0 1240 Sep 15 16:53 2017 add.o
rw-r--r-- 0/0 1240 Sep 15 16:53 2017 sub.o
t:列出静态库中的文件
v:verbose 详细信息
库搜索路径
生成动态库
[root@localhost linux]# gcc -fPIC -c sub.c add.c [root@localhost linux]# gcc -shared -o libmymath.so *.o [root@localhost linux]# ls add.c add.h add.o libmymath.so main.c sub.c sub.h sub.o
使用动态库
运行动态库
库文件名称和引入库的名称
如:libc.so -> c库,去掉前缀lib,去掉后缀.so,.a
1、二者的不同点在于代码被载入的时刻不同。
2、静态库的代码在编译过程中已经被载入可执行程序,因此体积比较大。
3、动态库(共享库)的代码在可执行程序运行时才载入内存,在编译过程中仅简单的引用,因此代码体积比较小。
4、不同的应用程序如果调用相同的库,那么在内存中只需要有一份该动态库(共享库)的实例。
5、静态库和动态库的最大区别,静态情况下,把库直接加载到程序中,而动态库链接的时候,它只是保留接口,将动态库与程序代码独立,这样就可以提高代码的可复用度,和降低程序的耦合度。
6、静态库在程序编译时会被连接到目标代码中,程序运行时将不再需要该静态库。
7、动态库在程序编译时并不会被连接到目标代码中,而是在程序运行是才被载入,因此在程序运行时还需要动态库存在
这里我们知道区别以及如何封装一个静态库和动态库就可以了。