文件的读写(标准库函数与系统调用函数),文件描述符的复制

文件描述符

  •  为了解决内核对象在可访问性与安全”性之间的矛盾,Unix系统通过所谓的文件描述符,将位于内核空间中的文件表项间接地提供给运行于用户空间中的程序代码。
  • 为了便于管理在系统中运行的各个进程,内核会维护一张存有各进程信息的列表,谓之进程表。系统中的每个进程在进程表中都占有一个表项。每个进程表项都包含了针对特定进程的描述信息,如进程ID、用户ID、组ID等,其中也包含了一个被称为文件描述符表的数据结构
  • 文件描述符表的每个表项都至少包含两个数据项--文件描述符标志和文件表项指针,而所谓文件描述符,其实就是文件描述符表项在文件描述符表中从0开始的下标
  • 作为文件描述符表项在文件描述符表中的下标,合法的文件描述符一定是个大于等于0的整数
  • 每次产生新的文件描述符表项,系统总是从下标0开始在文件描述符表中寻找最小的未使用项
  • 每关闭一个文件描述符,无论被其索引的文件表项和v节点是否被删除,与之对应的文件描述符表项一定会被标记为未使用,并在后续操作中为新的文件描述符所占用                             
  •  系统内核缺省为每个进程打开三个文件描述符,它们在unistd.h头文件中被定义为三个宏                            

                                                       #define STDIN_FILENO 0 //标准输入                                                                                               #define STDOUT_FILENO 1 // 标准输出        ​​​​​​​        ​​​​​​​        ​​​​​​​        ​​​​​​​        ​​​​​​​        ​​​​​​​        ​​​​​​​                               ​​​​​​​#define STDERR_FILENO 2 // 标准错误

 

文件的读写 

#include <unistd.h>
ssize_t write(int fd, void const* buf size t count);

->功能:向指定的文件写入数据
->参数:fd 文件描述符

            buf内存缓冲区,即要写入的效据
            count 期望写入的字节数
->返回值:成功返回实际写入的字节数,失败返回-1。

#include <unistd.h>
ssize_t read(int fd, void* buf, size t count);

->功能:从指定的文件中读取数据
->参数:fd        文件描述符
           buf       内存缓冲区,存读取到的数据
           count    期望读取的字节数

->返回值:成功返回实际读取的字节数,失败返回-1。 

代码实现 

write函数

//向文件写入数据
#include<stdio.h>
#include<fcntl.h>
#include<unistd.h>
#include<string.h>

int main(){
    //打开文件
    int fd = open("./shared.txt",O_WRONLY | O_CREAT | O_TRUNC,0664);
    if(fd == -1){
        perror("open");
        return -1;
    }
    //读取存储区内容写入文件
    char* buf = "hello world";
    ssize_t size = write(fd,buf,strlen(buf));
    if(size == -1){
        perror("write");
        return -1;
    }
    printf("实际写入文件%ld个字节\n",size);
    //关闭文件
    close(fd);
}

read函数 

//从文件读数据
#include<stdio.h>
#include<fcntl.h>
#include<unistd.h>
#include<stdlib.h>


int main(){
    //打开文件
    int fd = open("./shared.txt",O_RDONLY);
    if(fd == -1){
        perror("open");
        return -1;
    }
    //读数据
    char* buf = malloc(sizeof(char)*100);
    printf("buf = %zu\n",sizeof(buf));
    ssize_t size = read(fd,buf,99);
    if(size == -1){
        perror("read");
        return -1;
    }
    //打印数据
    printf("实际读取%ld个字节\n",size);
    printf("读取到的数据是:%s\n",buf);
    //关闭文件
    close(fd);
    return 0;
   
}

输出重定向 

代码实现

//输出重定向
#include<stdio.h>
#include<unistd.h>//close
#include<fcntl.h>//open
int main(){
    //修改1的指向 1---->out.txt
    close(STDOUT_FILENO);//关闭1 1空闲
    int fd = open("./out.txt",O_WRONLY | O_CREAT | O_TRUNC,0664);
    printf("fd = %d\n",fd);
    
    return 0;
}

        内容最后写入out.txt 文件中。

顺序与随机读写

  • 每个打开的文件都有一个与其相关的文件读写位置保存在文件表项中,用以记录从文件头开始计算的字节偏移文件读写位置通常是一个非负的整数,用off_t类型表示,在32位系统上被定义为long int,而在64位系统上则被定义为long long int
  • 打开一个文件时,除非指定了O_APPEND标志,否则文件读写位置一律被设为0,即文件首字节的位置
  • 每一次读写操作都从当前的文件读写位置开始,并根据所读写的字节数,同步增加文件读写位置,为下一次读写做好准备

  • 因为文件读写位置是保存在文件表项而不是v节点中的,因此通过多次打开同一个文件得到多个文件描述符,各自拥有自己的文件读写位置

修改文件读写位置

#include <unistd.h>
off_t lseek(int fd, off t offset, int whence)

->功能:人为调整文件读写位置
-> 参数:fd:文件描述符。
             offset: 文件读写位置偏移字节数
             whence:offset参数的偏移起点,可如下取值:
                                      SEEK_SET         -从文件头(首字节)开始
                                      SEEK_CUR        -从当前位置(最后被读写字节的下一个字节)开始                                            SEEK_END         -从文件尾(最后一个字节的下一个字节)开始

->返回值:成功返回调整后的文件读写位置,失败返回-1

  • lseek函数的功能仅仅是修改保存在文件表项中的文件读写位置,并不实际引发任何I/O动作
  • ->lseek (fd,-7,SEEK_CUR); // 从当前位置向文件头偏移7字节
  • ->lseek (fd, 0, SEEK_CUR); // 返回当前文件读写位置
  • ->lseek (fd,0,SEEK_END); //返回文件总字节数
  • 可以通过lseek函数将文件读写位置设到文件尾之后,,在超过文件尾的位置上写入数据,将在文件中形成空洞,位于文件中但没有被写过的字节都被设为0,文件空洞不占用磁盘空间,但被计算在文件大小之内

 代码实现

//修改文件读写位置
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include<string.h>

int main(){
    //打开文件
    int fd = open("./lseek.txt",O_WRONLY|O_CREAT|O_TRUNC,0664);
    if(fd == -1){
        perror("open");
        return -1;
    }
    //向文件写入hello world! 12
    char *str = "hello world!";
    if(write(fd,str,strlen(str)) == -1){
        perror("write");
        return -1;
    }
    //修改文件读写位置
    if(lseek(fd,6,SEEK_SET) == -1){
        perror("lseek");
        return -1;  
        
    }
    //再次向文件写入数据linux!
    char *str1 = "linux!";
    if(write(fd,str1,strlen(str1)) == -1){
        perror("write");
        return -1;
    }
    // 再次修改文件读写位置
    if(lseek(fd,8,SEEK_END) == -1){
        perror("lseek");
        return -1;
    }
    // 再次向文件写入数据
    char *str2 = "地三鲜";
    if(write(fd,str2,strlen(str2)) == -1){
        perror("write");
        return -1;  
    }
    //再次修改文件读写位置
    if(lseek(fd,-4,SEEK_SET) == -1){
        perror("lseek3");//error:Invalid argument
        return -1;
    }
    //再次向文件写入数据
    char *str3 = "123456";
    if(write(fd,str3,strlen(str3) == -1)){
        perror("write");
        return -1;  
    }
    //关闭文件
    close(fd);
    return 0;
}

系统IO与标准IO

当系统调用函数被执行时,需要在用户态和内核态之间来回切换,因此频繁执行系统调用函数会严重影响性能
标准库做了必要的优化,内部维护一个缓冲区,只在满足特定条件时才将缓冲区与系统内核同步,借此降低执行系统调用的频率,减少进程在用户态和内核态之间来回切换的次数,提高运行性能。

标准库函数与系统调用函数性能比较

标准库函数使用(执行100W次写入)

#include<stdio.h>

int main(){
    FILE *fp = fopen("./std.txt","w");
    if(fp == NULL){
        perror("fopen");
        return -1;
    }
    for(int i = 0;i < 1000000;i++){
        fwrite(&i,sizeof(int),1,fp);
    }
    fclose(fp);
    return 0;
}

系统调用函数使用(执行100W次写入)

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


int main(){
    //打开文件
    int fd = open("./sys.txt", O_WRONLY | O_CREAT |O_TRUNC, 0644);
    if(fd == -1){
        perror("open");
        return -1;
    }
    //写文件
    for(int i = 0; i < 1000000; i++){
        write(fd,&i,sizeof(int));
    }
    //关闭文件
    close(fd);
    return 0;
}

执行情况

day03$time ./std

real	0m0.021s
user	0m0.019s
sys	0m0.000s
day03$time ./sys

real	0m0.702s
user	0m0.161s
sys	0m0.539s
day03$
  • real:表示从开始到结束的实际时间(也称为“墙钟时间”)。这个时间包含了所有程序执行过程中消耗的时间,包括等待 I/O 操作、操作系统调度等。
  • user:表示程序在用户模式下花费的 CPU 时间。这是程序实际执行其指令所消耗的时间。
  • sys:表示程序在内核模式下花费的 CPU 时间。这部分时间主要用于处理系统调用,如文件 I/O、内存分配等。

        系统调用函数在执行大量I/O操时,由于频繁在内核态与用户态切换,降低了执行效率

 文件描述符的复制

#include <unistd.h>
int dup(int oldfd);

->功能:复制文件描述符表的特定条目到最小可用项
->参数:oldfd:源文件描述符
->返回值:成功返回目标文件描述符,失败返回-1
        dup函数将oldfd参数所对应的文作描述符表项复制到文件描述符表第一个空闲项中,同时返回该表项对应的文作描述符。dup函数返回的文件描述符一是调用进程当前未使用的最小文件描述符

        dup函数只复制文件描述符表项,不复制文件表项和v节点,因此该函数所返回的文件描述符可以看做是参数文件描述符oldfd的副本,它们标识同一个文件表项

  •  注意,当关闭文件时,即使是由dup函数产生的文件描述符副本,也应该通过close函数关闭,因为只有当关联于一个文件表项的所有文件描述符都被关闭了,该文件表项才会被销毁,类似地,也只有当关联于一个v节点的所有文件表项都被销毁了,v节点才会被从内存中删除,因此从资源合理利用的角度讲,凡是明确不再继续使用的文件描述符,都应该尽可能及时地用close函数关闭
  • dup函数究竟会把oldfd参数所对应的文件述符表项,复制到文件描述符表的什么位置,程序员是无法控制的,这完全由调用该函数时文件描述符表的使用情况决定,因此对该函数的返回值做任何约束性设都是不严谨的。由dup函数返回的文件描述符与作为参数传递给该函数的文件描述符标识的是同一个文件表项而文件读写位置是保存在文件表项而非文件描述符表项中的,因此通过这些文件描述符中的任何一个,对文件进行读写或随机访问,都会影响通过其它文件描述符操作的文件读写位置。这与多次通过open函数打开同一个文件不同。

代码实现 

//文件描述符的复制
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>

int main(){
    //打开文件,得到文件描述符oldfd
    int oldfd = open("./dup.txt", O_WRONLY | O_CREAT | O_TRUNC, 0664);
    if(oldfd == -1){
        perror("open");
        return -1;
    }
    printf("oldfd = %d\n", oldfd);
    //通过复制文件描述符oldfd,得到新的文件描述符newfd
    int newfd = dup(oldfd);
    if(newfd == -1){
        perror("dup");
        return -1;  
    }
    printf("newfd = %d\n", newfd);
    //通过oldfd向文件写入数据,hello world!
    if(write(oldfd, "hello world!", 12) == -1){
        perror("write");
        return -1;
    }
    //通过newfd修改文件读写位置lseek
    if(lseek(newfd, 6, SEEK_SET) == -1){
        perror("lseek");
        return -1;
    }
    //通过oldfd向文件写入数据,hello linux!
    char* buf = "linux!";
    if(write(oldfd, buf, 6) == -1){
        perror("write");
        return -1;
    }

    //关闭文件
    close(oldfd);
    close(newfd);



    return 0;
}

执行结果

oldfd = 3
newfd = 4
[1] + Done                       "/usr/bin/gdb" --interpreter=mi --tty=${DbgTerm} 0<"/tmp/Microsoft-MIEngine-In-kyfxvhcx.pyn" 1>"/tmp/Microsoft-MIEngine-Out-fcceg4ba.gyc"
day03$cat dup.txt
hello linux!day03$

  • 17
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值