不仅仅是程序本身——基础IO

关键字:C语言文件接口,系统调用文件接口,文件描述符,重定向,静态库与动态库,软链接与硬链接。

目录

基础IO

C语言文件接口

系统调用文件接口

open

write

read

lseek

close

文件描述符

从内核角度理解文件描述符

 文件描述符与文件流指针

重定向

 重定向的文件接口

从内核角度理解重定向

 dup2函数的一个小要点

 动态库&静态库

什么是动态库?什么是静态库?

动态库

 静态库

 软链接&硬链接


基础IO

通过程序来对文件进行简单的读写操作是我们走向项目开发的不可或缺的一步。首先,我们先来复习/了解一下C语言库函数中的一些简单的IO操作函数。

C语言文件接口

#include <stdio.h>

FILE* fopen(const char* path, const char* mode);

//path:带有路径的文件名称,如果不带路径,则会在当前文件夹寻找文件
//mode:打开文件的方式
//返回值:成功,返回文件流指针FILE*,失败,返回NULL。
mode含义文件流情况
"r"以只读方式打开,文件必须存在文件起始位置
"w"写方式打开,文件存在,则清空内容,文件不存在,则创建一个新文件。文件起始位置
"a"追加写,文件不存在,则创建文件。文件末尾
"r+"以可读可写方式打开。文件必须存在。文件起始位置
"w+"以可读可写方式打开。如果文件存在,则清空内容,如果文件不存在,则创建一个新文件。文件起始位置
"a+"追加写,也可以读。文件不存在,则创建文件。

追加在文件末尾

读从文件头开始

#include <stdio.h>

int fclose(FILE* fp);

//关闭文件流。并冲刷缓冲区。
//fp:文件流指针
//返回值:关闭成功,则返回0,关闭失败,则返回EOF
#include <stdio.h>

size_t fwrite(const void* ptr, size_t size, size_t nmemb, FILE* stream);

//ptr:要写入文件的内容
//size:向文件中写的每个块的大小,单位为字节
//nmemb:向文件中写入的块的个数
//stream:指向FILE对象的指针,该FILE对象指定了一个输入流。
//返回值:成功写入文件中的块个数。
#include <stdio.h>

size_t fread(const void* ptr, size_t size, size_t nmemb, FILE* stream);

//ptr:将文件中读到的内容保存到ptr指向的内存空间中
//size:从文件中读的时候,每块的大小,单位为字节。
//nmemb:希望读取多少块。
//stream:文件流指针。
//返回值:成功从文件中读取的块的个数。
#include <stdio.h>

int fseek(FILE* stream, long offset, int whence);

//stream:文件流指针
//offset:相对whence的偏移量。
//whence:将文件流指针移动到什么位置,一般使用C语言库中提供的三个常量。
//返回值:成功返回0,失败返回-1
常量描述
SEEK_SET文件开头

SEEK_CUR

文件流指针当前位置
SEEK_END文件末尾

举个小例子吧:

#include <stdio.h>
#include <string.h>
int main() {
    FILE* e_fp = fopen("1.txt", "r");
    if (e_fp == NULL) {
        perror("error example fopen:");
    }

    char str[] = "you share rose and get fun";
    char tmp[128] = {0};
    FILE* fp = fopen("1.text", "w+");

    int w_len = fwrite(str, sizeof(char), sizeof(str), fp);

    fseek(fp, 4, SEEK_SET);

    int len = fread(tmp, sizeof(char), 128, fp);

    fclose(fp);

    printf("write_len:%d, read_len: %d\n",w_len, len);
    printf("%s\n", tmp);
}

 以上知识C语言文件接口的一些简单介绍,至于其更精细的使用并不在本篇的讨论范畴,下面是我们在C语言文件接口之上所需要重点学习的Linux操作系统所提供给我们的系统调用文件接口:

C语言系统调用
fopenopen
fcloseclose
fwritewrite
freadread
fseeklseek

系统调用文件接口

open

#include <sys/type.h>
#include <sys/stat.h>
#include <fcntl.h>

int open(const char* pathname, inr flags);
int open(const char* pathname, inr flags, mode_t mode);

//pathname:要打开或创建的文件
//flags:打开文件时,可以传入多个参数选项,用下面一个或多个常量进行“或”运算。
//mode:当创建一个文件的时候,指定新创建文件的权限,传入一个8进制的数字。
//    即,权限,0664。
//返回值:成功返回新打开的文件描述符,失败返回-1。
flags含义其他
O_RDONLY只读打开必须指定且只能指定一个
O_WRONLY只写打开
O_RDWR可读可写
O_CREAT文件不存在,则创建,需要使用mode来指定文件操作权限可选,也可以都选
O_APPEND追加写

write

#include <sys/type.h>
#include <sys/stat.h>
#include <fcntl.h>

ssize_t write(int fd, const void* buf, size_t count);

//fd:文件描述符
//buf:将buf指向的内容写入到文件中
//count:期望写入的字节数
//返回值:返回成功写入的字节数。

read

#include <sys/type.h>
#include <sys/stat.h>
#include <fcntl.h>

ssize_t read(int fd, const void* buf, size_t count);

//fd:文件描述符
//buf:将文件中的内容读到buf指向的内存中。
//count:期望读取的字节数
//返回值:返回成功读取的字节数。

lseek

#include <sys/type.h>
#include <sys/stat.h>
#include <fcntl.h>

off_t lseek(int fd, off_t offset, int whence);

//fd:文件描述符
//offset:偏移量,单位为字节
//whence:偏移的位置
//返回值:成功返回偏移的位置,单位为字节;失败返回-1
常量描述
SEEK_SET文件开头

SEEK_CUR

文件流指针当前位置
SEEK_END文件末尾

close

#include <sys/type.h>
#include <sys/stat.h>
#include <fcntl.h>

int close(int fd);

//关闭文件描述符
//fd:文件描述符

文件描述符

文件描述符是一个非负整数。内核通过文件描述符来访问文件。

查看文件描述符的方式

1.在程序中打印观察文件描述符的值。

2.查看/proc/[pid]/fd文件夹下的文件描述符信息。

#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>

int main() {
    int fd = open("1.txt", O_RDWR | O_CREAT, 0644);

    if (fd < 0) {
        perror("open:");
    }
    while (1) {
        sleep(1);
    }
}

 上图中文件描述符1,2,3是进程创建时默认打开的,其中

0——标准输入

1——标准输出

2——标准错误

而最后的3,则是程序中open函数打开文件而产生的文件描述符。

#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>

int main() {
    int fd0 = open("2.txt", O_RDWR | O_CREAT, 0644);
    close(2);
    printf("waiting...\n");
    getchar();
    printf("loading...\n");

    int fd = open("1.txt", O_RDWR | O_CREAT, 0644);

    if (fd < 0) {
        perror("open:");
    }
    while (1) {
        sleep(1);
    }
}

 结合程序代码即贴图,可以看出,在文件描述符2关闭后,使用open产生新的文件描述符,而这个新的文件描述符的值为2。

文件描述符不是固定进行分配的,它是按照最小未使用原则进行分配的。

从内核角度理解文件描述符

通过之前的学习我们已经清楚了在Linux内核中,进程的相关信息是存储在task_struct结构体当中的,通过查阅Linux系统文件中关于task_struct结构体的具体内容,我们可以在其中找到一个与晚间相关的结构体:

struct file_struct* file

通过对该结构体的按图索骥,我们可以在其具体代码中找到另一个有趣的结构体:

struct file __rcu * fd_array[NR_OPEN_DEFAULT]
//__ruc是Linux操作系统内部的一种同步机制,全称为Read-Copy Update
//fd_array是一个长度为NR_OPEN_DEFAULT的结构体指针

这便是我们的重头戏了,文件描述符就是内核中fd_array数组的下标。而struct_file结构体中则记录了文件的信息,文件的所有者,文件的大小,文件的创建时间以及文件在磁盘中的保存位置,与权限等等。

在此处我们可以拓展一下,一个进程最多可以打开多少个文件描述符?

根据刚刚描述过的文件描述符在内核中的表现,相信很多人此时都会脱口而出,文件描述符最多可以打开NR_OPEN_DEFAULT个

通过查阅一些资料,我们可以得到,在64位操作系统当中,NR_OPEN_DEFAULT的大小为64,这显然是不可能的,以现在很多软件的体量,同时打开64个文件的进程肯定不在少数。

那么,真实的答案是多少呢?

不知道。这是一个很严肃的回答,在Linux操作系统中我们可以通过

ulimit -a

 来查询当前操作系统中规定的,一个进程最多可以打开多少文件描述符。使用该命令得到的一系列数据当中,open files的值即为我们想要查看的单个进程打开文件数量的上限。

 如上图,我所使用的Linux操作系统中,一个进程最多可以打开1024个文件描述符。当然这只是一个理论值,一切软件都是建立在硬件的基础上的,如果硬件系统跟不上,可能打开512个文件都比较费劲。并且,这个值是可以更改的。

我们可以通过:

ulimit -n [数量]

来人为地规定这个值。

那么操作系统是怎么突破NR_OPEN_DEFAULT的大关的?

根据搜索来的资料总结,是这样的:

在64位操作系统当中,如果一个进程打开的文件对象超过64个,内核将分配一个新数组,并将task_struct->files_struct->fdtable->fd指针指向它。所以对适当数量的文件对象的访问会执行的很快,因为它是对静态数组的操作;如果一个进程打开的文件数量过多,那么内核就会建立新数组。

 文件描述符与文件流指针

文件描述符刚才已经讲解过了,但细心的同志们应该都注意到了,文件描述符fd是Linux系统调用使用或返回的,而C语言库函数给我们提供的文件接口所使用的是与之完全不同的FEIL*类型的变量,也即我们所谓的文件流指针

通过在Linux操作系统中查看stdio.h头文件,

首先我们确定了,FILE的本质是一个struct _IO_FILE结构体。 而很遗憾的是,struct _IO_FILE的定义似乎并不在stdio.h中。通过进一步搜索,我们在libio.h中找到了struct _IO_FILE结构体的具体定义。

以下是结构体内容的部分截图

 经过查阅资料及相关测试证明,

#include <stdio.h>
#include <string.h>
int main() {
    char str[] = "you share rose and get fun";
    char tmp[128] = {0};
    FILE* fp = fopen("1.text", "w+");
    printf("_fileno: %d\n", fp->_fileno);
    int w_len = fwrite(str, sizeof(char), sizeof(str), fp);

    fseek(fp, 4, SEEK_SET);

    int len = fread(tmp, sizeof(char), 128, fp);
    while(1);
    fclose(fp);
    return 0;
}

 _IO_FILE结构体中变量_fileno中保存的即是文件描述符的数值

 值得一提的是,我在之前关于进程控制的文章中所提到exit与_exit函数的差别,其中的一条是:

与_exit()函数相比,身为C语言库函数的exit()函数会冲刷C标准库所定义的缓冲区。

 而上方截图中_IO_FILE结构体内的:

char* _IO_read_ptr;   /* Current read pointer */
char* _IO_read_end;   /* End of get area. */
char* _IO_read_base;  /* Start of putback+get area. */
char* _IO_write_base; /* Start of put area. */
char* _IO_write_ptr;  /* Current put pointer. */
char* _IO_write_end;  /* End of put area. */

即是读/写缓冲区的相关变量。

至于缓冲区的具体实现,在此不表。


 文件流指针对应的结构体struct _IO_FILE这个结构体内部的成员变量int _fileno保存了对应文件描述符的数值。


重定向

重定向:指的是重新指定设备代替原来的[输入/输出]设备作为新的[输入/输出]设备。

文件描述符
标准输入0
标准输出1
标准错误2

 重定向的文件接口

#include <unistd.h>

int dup2(int oldfd, int newfd);

//作用:将newfd的值重定向oldfd,即newfd拷贝oldfd。
//参数:newfd和oldfd都是文件描述符。
//返回值:成功返回newfd,关闭newfd原来指向的文件,
//        让newfd指向oldfd对应的task_struct结构体。
//    失败返回-1,并设置errno。

 关于其用法我们可以举一个简单的例子:

#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>

int main() {
    int fd = open("1.txt", O_RDWR | O_CREAT, 0644);
    
    int i = dup2(fd, 1);

    printf("i am in 1.txt\n");

    printf("new_fd: %d\n", i);
    sleep(20);
    return 0;
}

 

 经过之前的内容,我们都知道,一个进程创建时往往会默认打开三个文件,它们文件标识符分别为0、1、2,分别代表着标准输入,标准输出以及标准错误。

在这个例子中,我们将标准输出重定向到了文件1.txt,这也就表示,我们在该进程重定向操作之后的所有输出,都会输出到文件1.txt之中。而最终结果也即截图所显示的,与我们预期一样,并且显然它是以清除写的方式在文件中写入的。

从内核角度理解重定向

当我们调用dup2函数,将文件描述符1指向文件描述符3所指向的文件后,即将标准输出重定向到文件描述符3对应的文件。

 在此操作过程中,系统会将文件描述符1对应的结构体指针指向文件描述符3对应结构体指针所指的文件。此时文件描述符1与文件描述符3是共用一个struct file结构体

 dup2函数的一个小要点

对于以下程序,它会有怎样的输出?

#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>

int main() {
    int fd = open("1.txt", O_RDWR | O_CREAT, 0644);
    
    int i = dup2(fd, 1);

    printf("i am in 1.txt\n");
    
    char buf[] = "the simple struct file\n";
    write(fd, buf, strlen(buf));

    printf("new_fd: %d\n", i);
    close(fd);
    return 0;
}

如果知道我在另一篇博客中写过的,\n可以冲刷缓冲区,一定会以为结果是这样的吧?

然而真的将它运行之后,结果却不是如此,反而是

为什么"the simple struct file"明明在第一句printf之后,明明第一句printf的句尾有\n,但结果却是 "the simple struct file"输出在了"i am in 1.txt"之前?

因为对于标准输出的重定向改变了格式化IO,即printf等的缓冲方式

所有我们需要将程序改为:

#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>

int main() {
    int fd = open("1.txt", O_RDWR | O_CREAT, 0644);
    
    int i = dup2(fd, 1);
    
    setvbuf(stdout, NULL, _IONBF, 0);
    printf("i am in 1.txt\n");
    
    char buf[] = "the simple struct file\n";
    write(fd, buf, strlen(buf));

    printf("new_fd: %d\n", i);

    close(fd);
    return 0;
}

即可完成预期的目的。

至于造成这个现象的原因,我只在一些资料中找到了这样一句话:

当且仅当标准输入和标准输出并不指向交互式设备时,它们才是全缓冲的。 

 我们可以通过setvbuf函数来设置格式化IO的缓冲方式。

 动态库&静态库

本质上说,库是一种可执行代码的二进制形式,可以被操作系统载入内存执行。

库是写好的,成熟的,可复用的代码。

一般情况下,为了方便将程序提供给第三方使用,会将程序编写成为库文件提供给第三方(用户)使用。使用者不必关心函数具体是如何实现的,只需要关注函数的使用方法即可。

由于库文件是二进制文件,所以库文件有着极好的保密性。

库有两种,分别是动态库与静态库。

什么是动态库?什么是静态库?

什么是动态库?

动态库在编译的时候,并没有将所有的内容都编译进目标文件中,只是将一些重定位与符号表信息编了进去,这些信息可以协助完成链接过程。由于整合进去的只是一些信息,所以编译后的可执行文件比较小,但需要外部函数库的支持

什么是静态库?

静态库在编译时会将整个函数库的所有信息整合进目标代码中,所以编译后的可执行文件比较大,但不需要外部函数库的支持

动态库

动态库在WIN与Linux操作系统中的特征:

操作系统前缀后缀
WIN(Windows).dll
Linuxlib.so

生成方法:

使用gcc或g++编译器,添加命令行参数

        -fPIC

        -shared

注意:生成动态库的代码当中不需要包括main()函数

使用方法:

在编译可执行程序时,如果需要链接动态库:

        -L [path] :指定动态库所在路径

        -l[去掉前缀与后缀后的动态库名称] :指定所依赖的动态库 

 例如我们在上级目录有一个名为 libmysort.so 的动态库,在我们的程序中需要使用这个动态库,除了在程序中包含动态库所对应的头文件外,还应该在编译时:

gcc func.c -L .. -lprint -o func

这样就可以编译出名为 func 的可执行程序了。

//print.c
#include "print.h"

void print() {
    printf("you should see it");
}

//func.c
#include "../print.h"

int main() {
    print();
    return 0;
}

 然而真当我们按照上述来操作时,最终生成的可执行文件的链接情况中,并没有找到libprint.so。

这是因为我们手动生成的动态链接库文件并不在操作系统默认的库文件搜索路径中。对于这样的情况,通常有以下三种解决方案:

1.将库文件移动到系统默认的库文件搜索路径下。

2.将库文件与可执行程序放到同一路径下。

3.配置动态库环境变量:LD_LIBRARY_PATH

 静态库

静态库在WIN与Linux操作系统中的特征:

操作系统前缀后缀
WIN(Windows).lib
Linuxlib.a

生成方法:

1.使用gcc/g++将源代码编译成目标程序(.o文件)

2.使用ar -rc命令编译目标程序为静态库。

ar -rc [静态库文件名] [目标程序]

在不确定自己生成该静态库时都使用了哪些目标程序时,还可以通过:

ar -tv [静态库文件名]

来查看。演示如下:

编译可执行程序时,如果使用到了静态库,那么静态库会被直接编译到可执行程序中,并不存在如动态库一样的依赖。

 

 软链接&硬链接

软链接:又称为符号链接,这个文件包含了另一个文件的路径名。可以是任意文件或目录,可以链接不同文件系统的文件。

说的简单点就是快捷方式。 

硬链接:就是一个文件的一个或多个文件名。所谓链接无非是把文件名和计算机文件系统使用的节点号链接起来。因此我们可以用多个文件名与同一个文件进行链接,这些文件名可以在同一目录或不同目录。

软链接生成方式:

ln -s [源文件] [软链接文件]

 

 可见软链接文件的文件类型为 l

注意事项:

1.修改软链接文件,源文件也会被修改,反之同理。(快捷方式不都这样吗)

2.如果源文件被删除,软链接文件还在,修改软链接文件,会重新生成源文件(空的),重新建立链接关系(起死回生)。所以在删除源文件的时候,要将软链接文件一起删除掉(斩草除根)。

 硬链接生成方式:

ln [源文件] [硬链接文件名]

在Linux操作系统中每个文件都有一个名为inode结点的东西,而inode结点中有一个连接计数用来计算该文件的硬链接数量,每新建立一个硬链接,计数加一;删除一个,计数减一。

只有当硬链接数量减为0时,才可以将文件删除掉。

硬链接不能跨文件系统创建,并且只能对文件创建,不能对目录创建硬链接。 

 

  • 5
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

云雷屯176

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值