Linux系统编程---文件IO

一、系统调用

由操作系统实现并提供给外部应用程序的编程接口(Application Programming Interface,API),用户程序可以通过这个特殊接口来获得操作系统内核提供的服务

系统调用和库函数的区别:

系统调用(系统函数)     内核提供的函数

库调用                         程序库中的函数

错误处理函数

errno用于记录系统的最后一次错误代码,返回一个int值(错误码),在errno.h中定义,不同的错误码表示不同的含义,新建errno.c如下:

#include<stdio.h>
#include<errno.h>
#include<string.h>

int main(void)
{
        FILE *fp = fopen("txt","r");//打开一个不存在的文件

        if (NULL == fp)
        {
                printf("fopen failed\n");
                printf("errno:%d\n",errno);//打印errno返回的错误码
                printf("fopen:%s\n",strerror(errno));//使用strerror函数来解释错误码

                return 1;
        }

        return 0;
}

编译再执行可得如下结果:

fopen failed
errno:2
fopen:No such file or directory

虚拟地址空间:

文件描述符

  • 当我们打开文件或者新建文件时,系统会返回一个文件描述符用来指定已打开的文件,这个文件描述符相当于这个已打开文件的标号,操作这个文件描述符就相当于操作这个描述符所指定的文件;
  • 程序运行起来后每个进程都有一张文件描述符的表,标准输入、输出,标准错误输出,对应的文件描述符0、1、2就记录在表中,程序运行起来后这三个文件描述符是默认打开的;

文件描述符是指向一个文件结构体的指针

进程控制块(PCB):本质---结构体

FILE结构体:主要包含文件描述符、文件读写位置、IO缓冲区三部分内容

最大打开文件数:一个进程默认打开文件的个数1024

命令查看:ulimit -a 查看open files 对应值。默认为1024   

可以使用ulimit -n 4096 修改

cat /proc/sys/fs/file-max可以查看该电脑最大可以打开的文件个数。受内存大小影响。

二、常用文件IO函数

1.open函数

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

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

参数:

        pathname: 欲打开的文件路径名

        flags:文件打开方式: #include <fcntl.h>

                O_RDONLY|O_WRONLY|O_RDWR

                O_CREAT|O_APPEND|O_TRUNC|O_EXCL|O_NONBLOCK ....

        mode:这个参数只有在文件不存在时有效,指新建文件时指定文件的权限

                取值8进制数,用来描述文件的访问权限。 rwx 0664

                创建文件最终权限 = mode & ~umask

返回值:

        成功: 打开文件所得到对应的 文件描述符(整数)

        失败: -1, 设置errno

flags必选项:

O_RDONLY        以只读的方式打开    

O_WRONLY       以只写的方式的打开

O_RDWR           以可读、可写的方式打开

 可选项,和必选项进行位或(|)

O_CREAT                文件不存在则创建文件,使用此选项时需使用mode说明文件的权限
O_EXCL                   如果同时指定了O_CREAT,且文件已经存在,则出错
O_TRUNC                如果文件存在,则清空文件内容
O_APPEND              写文件时,数据添加到文件末尾
O_NONBLOCK         对于设备文件,以O_NONBLOCK方式打开可以做非阻塞I/O

打开dict.cp,如果不存在则创建,并添加权限,如果存在则清空内容:

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

int main(int argc,char *argv[])
{
        int fd;
        open("./dict.cp",O_RDONLY | O_CREAT | O_TRUNC,0644);//rw-r--r--
                
        printf("fd = %d\n",fd);

        close(fd);

        return 0;
}

 对于存在mydir目录以及不存在mydir目录执行以下程序:

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

int main(int argc,char *argv[])
{
        int fd;
        open("mydir",O_WRONLY);

        printf("fd = %d,errno = %d:%s\n",fd,errno,strerror(errno));

        close(fd);

        return 0;
}

mydir目录存在输出为:

fd = 21991,errno = 21:Is a directory

mydir目录不存在输出为:

fd = 22002,errno = 2:No such file or directory

2.close函数

#include <unistd.h>

int close(int fd);
功能:
        关闭已打开的文件
参数:
        fd:文件描述符,open()的返回值
返回值:
        成功:0
        失败:-1,并设置errno

3.write函数

#include <unistd.h>


ssize_t write(int fd, const void *buf, size_t count);
功能:
        把指定数目的数据写到文件(fd)
参数:
        fd:文件描述符
        buf:数据首地址
        count:写入数据的长度(字节)
返回值:

        成功:实际写入数据的字节个数
        失败:-1

4.read函数

#include <unistd.h>

ssize_t read(int fd, void *buf, size_t count);
功能:
        把指定数目的数据读到内存(缓冲区)
参数:
        fd:文件描述符
        buf:内存首地址
        count:读取的字节个数
返回值:
        成功:实际读取到的字节个数
        失败:-1

 用read和write实现一个copy函数:

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

int main(int argc,char *argv[])
{
        char buf[1024];
         
        int n = 0;

        int fd1 = open(argv[1],O_RDONLY);//read
        if (fd1 == -1){
                perror("open argv1 error");
                exit(1);
        }

        int fd2 =open(argv[2],O_RDWR|O_CREAT|O_TRUNC,0644);
        if (fd2 == -1){
                perror("open argv2 error");
                exit(1);
        }


        while((n = read(fd1,buf,1024)) != 0){
                if (n < 0){
                        perror("read error");
                        break;  
                }
                write(fd2,buf,n);
        }

        close(fd1);
        close(fd2);

        return 0;
}

5.lseek函数

#include <sys/types.h>#include cunistd.h>


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

功能:
       改变文件的偏移量(读写位置)

参数:
        fd:文件描述符

        offset:根据whence来移动的位移数(偏移量),可以是正数,也可以负数,如果正数,则相对于whence往右移动,如果是负数,则相对于whence往左移动。如果向前移动的字节数超过了文件开头则出错返回,如果向后移动的字节数超过了文件末尾,再次写入时将增大文件尺寸。


        whence:其取值如下:
                SEEK_SET:从文件开头移动offset个字节

                SEEK_CUR:从当前位置移动offset个字节            

                SEEK__END:从文件未尾移动offset个字节

返回值:
        若lseek成功执行,则返回新的偏移量

        如果失败,返回-1

lseek允许超过文件结尾设置偏移量,文件会因此被拓展。

使用lseek获取文件大小:

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

int main(int argc,char *argv[])
{
        int fd = open(argv[1],O_RDWR);
        if (fd == -1)
        {
                perror("read error");
                exit(1);
        }

        int lenth = lseek(fd,0,SEEK_END);//获取文件大小
        printf("file size:%d\n",lenth);

        close(fd);

        return 0;
}

make之后执行 ./lseek_test dict.c 可得到dict.c的文件大小

out:

file size:48

使用lseek扩展文件大小:

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

int main(int argc,char *argv[])
{
        int fd = open(argv[1],O_RDWR);
        if (fd == -1)
        {
                perror("read error");
                exit(1);
        }
 
        int lenth = lseek(fd,52,SEEK_END);//扩展文件大小
        printf("file size:%d\n",lenth);

        write(fd,"a",1);

        close(fd);

        return 0;
}

make之后执行 ./lseek_test dict.c 可得到dict.c的文件大小,再执行ls -l dict.c得到扩展之后的文件大小,输出分别为:

file size:100

-rw-rw-r-- 1 *** *** 101 *** ** *** dict.c

应用场景:

1. 文件的“读”、“写”使用同一偏移位置。

2. 使用lseek获取文件大小(返回值接收)

3. 使用lseek拓展文件大小:要想使文件大小真正拓展,必须【引起IO操作】(即write)

使用 truncate 函数,直接拓展文件。

int ret =truncate("dict.cp”,250):

#include<stdio.h>
#include<unistd.h>
#include<string.h>
#include<stdlib.h>
#include<pthread.h>

int main(int argc,char *argv[])
{
        //open/lseek(fd,249.SEED_END)/write(fd,"\0",1);
        int ret = truncate("dict.cp",200);
        printf("ret = %d\n",ret);

        return 0;
}

起始cidt.cp文件大小为0,经过以上代码扩展之后,cidt.cp文件大小为200

三、系统调用和库函数比较---预读入缓输出

fputc/fgetc实现:

int fputc(int c, FILE *stream);

fputc 函数用于将一个字符写入到指定的文件流中

参数
  • c:要写入的字符。虽然参数类型为 int,但只有字符的低8位会被写入文件。
  • stream:目标文件流的指针,该文件流应已经用 fopen 或类似函数打开且具有写权限。
返回值
  • 成功:返回写入的字符。
  • 失败:返回 EOF,通常是一个负值,表示写入失败,可能是因为文件结束或发生错误

int fgetc(FILE *stream);

fgetc 函数用于从指定的文件流中读取下一个字符

参数
  • stream:要读取的文件流的指针,该文件流应已经用 fopen 或类似函数打开且具有读权限。
返回值
  • 成功:返回读取到的字符,虽然返回类型为 int,但实际上是一个无符号字符(0到255)。这样可以区分所有可能的字符和 EOF
  • 失败:返回 EOF,表示已经到达文件末尾或发生读取错误。

fputcfgetc 来复制文件的内容: 

#include <stdio.h>
#include <stdlib.h>

int main(void){
    FILE *fp,*fp_out;
    int n = 0;

    fp = fopen("hello.c", "r");
    if(fp == NULL)
    {
        perror("fopen error");
        exit(1);
    }

    fp_out = fopen ("hello.txt", "w" );
    if(fp_out =NULL)
    {
        perror("fopen error");
        exit(1);
    }

    while((n = fgetc(fp))!= EOF)
    {
        fputc(n, fp_out) ;
    }

    fclose(fp);
    fclose(fp_out);
    return 0;
}

代码解释:

在这个示例中,程序打开两个文件:一个用于读取,另一个用于写入。通过循环使用 fgetc 读取源文件的每个字符并用 fputc 写入到目标文件中,实现文件的复制。如果打开文件失败,将输出错误信息并退出程序。

read/write实现:

int main( int argc, char *argv[])
{
    char buf[1];

    int n = 0;

    int fd1 = open(argv[1],0_RDONLY);
    int fd2 = open(argv[2],O_RDWR|0_CREAT|0_TRUNC,0664);

    while((n = read (fd1,buf,1)) != 0)
    {
        write(fd2, buf, n);
    }

    close(fd1);
    close(fd2);
    return 0;
}

结果表明:read/write速度慢

原因分析:

  • read/write这块,每次写一个字节,会疯狂进行内核态和用户态的切换,所以非常耗时。
  • fgetc/fputc,有个缓冲区,4096,所以它并不是一个字节一个字节地写,内核和用户切换就比较少

预读入,缓输出机制。所以系统函数并不是一定比库函数牛逼,能使用库函数的地方就使用库函数。

  • 标准IO函数自带用户缓冲区,系统调用无用户级缓冲。系统缓冲区是都有的。

四、阻塞和非阻塞

阻塞操作是指调用者启动一个操作并且必须等待这个操作完成后才能继续执行。在阻塞操作期间,当前线程或进程会被挂起(不占用CPU时间),直到操作完成。这意味着在等待如文件读取或网络数据接收完成期间,程序的执行完全停止在那个点,不做任何工作,直到I/O操作完成。

非阻塞操作是指调用者启动一个操作后不需要等待其完成就可以继续执行其他任务。非阻塞操作允许多个操作并行进行而不相互阻塞。如果操作因为某些原因(如数据未准备好)不能立即完成,调用就会立即返回一个错误码,通常是 EWOULDBLOCKEAGAIN

阻塞为文件的属性,而不是read、write的属性。

产生阻塞的场景为读设备文件。读网络文件的属性。(读常规文件无阻塞概念)

/dev/tty -- 终端文件。

open("/dev/tty", O_RDWR | O_NONBLOCK) --- 设置 /dev/tty 非阻塞状态。(默认为阻塞状态)

更改非阻塞读取终端——超时设置

#include <unistd.h>  
#include <fcntl.h>  
#include <stdlib.h>  
#include <stdio.h>  
#include <errno.h>  
#include <string.h>  
  
#define MSG_TRY "try again\n"  
#define MSG_TIMEOUT "time out\n"  
  
int main(void)  
{  
    //打开文件
    int fd, n, i;  
    fd = open("/dev/tty", O_RDONLY | O_NONBLOCK);  
    if(fd < 0){  
        perror("open /dev/tty");  
        exit(1);  
    }  
    printf("open /dev/tty ok... %d\n", fd);  
  	
    //轮询读取
    char buf[10];  
    for (i = 0; i < 5; i++){  
        n = read(fd, buf, 10);  
        if (n > 0) {                    //说明读到了东西  
            break;  
        }  
        if (errno != EAGAIN) {          //EWOULDBLOCK    
            perror("read /dev/tty");  
            exit(1);  
        } else {  
            write(STDOUT_FILENO, MSG_TRY, strlen(MSG_TRY));  
            sleep(2);  
        }  
    }  
  	//超时判断
    if (i == 5) {  
        write(STDOUT_FILENO, MSG_TIMEOUT, strlen(MSG_TIMEOUT));  
    } else {  
        write(STDOUT_FILENO, buf, n);  
    }  
  
    //关闭文件
    close(fd);  
    return 0;  
}  

五、传入传出参数

传入参数:

        1. 指针作为函数参数。

        2. 通常有const关键字修饰。

        3. 指针指向有效区域, 在函数内部做读操作。

传出参数:

        1. 指针作为函数参数。

        2. 在函数调用之前,指针指向的空间可以无意义,但必须有效。

        3. 在函数内部,做写操作。

        4. 函数调用结束后,充当函数返回值。

传入传出参数:

        1. 指针作为函数参数。

        2. 在函数调用之前,指针指向的空间有实际意义。

        3. 在函数内部,先做读操作,后做写操作。

        4. 函数调用结束后,充当函数返回值。

  • 41
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值