基礎IO(文件輸入輸出、標準IO接口、文件描述符和文件流指針)

 

目錄

基礎IO(文件的輸入輸出操作)

 FILE *fopen(char* filename,char* mode);(文件名稱,打開方式)

size_t fread(char* buf,size_t block_size,size_t block_count,FILE* fp);(緩衝區,塊大小,塊個數,文件流指針);

size_t fwrite(char*data,size_t block_size,size_t block_count,FILE* fp);(數據首地址,塊大小,塊個數,文件流指針);

int fseek(FILE* fp,long offset,int whence);(將文件的讀寫指針從whence位置偏移offset個字節)- - - - -跳轉文件讀寫位置

int fclose(FILE* fp);- - - - - 關閉文件流指針,釋放資源

標準的IO接口(都是庫函數,而庫函數就是對系統調用接口的一層封裝)

 int open(char* filename,int flag,mode_t mode);

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

ssize_t read(int fd,char* buf,size_t len);

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

int close(int fd);通過文件描述符關閉文件,釋放資源;

int dup2(int oldfd,int newfd);- - - - -描述重定向函數

在minishell中實現>/>>標準輸出重定向 

文件描述符與文件流指針的關係:



基礎IO(文件的輸入輸出操作)

例:fopen/fwrite/fread/fseek/fclose        stdin/stdout/stderr

 FILE *fopen(char* filename,char* mode);(文件名稱,打開方式)

打開方式:*r(只讀); r+(讀寫);w(只寫);w+(讀寫);a(追加寫);a+(追加讀寫);b(二進制操作)。

追加寫:每次寫入數據總是寫入到文件末尾

r+的讀寫和w+的讀寫的區別:r+讀寫打開文件,若文件不存在則報錯;w+讀寫打開文件,若不存在則創建,若存在則清空原有內容。

a:不僅是追加寫,並且文件不存在會創建新文件

b:默認清空如果不指定b,則認爲文件是文本操作,加上b則認爲是二進制操作;區別在於:有時候一個特殊字符,只是一個字符但是佔據兩個字節的內存(例:讀取一個100字節大小的文件,文本操作最終讀取出來的數據,不一定是100個字節)

返回值:返回實際一個FILE*的文件流指針作爲文件的操作句柄;失敗則返回NULL;

size_t fread(char* buf,size_t block_size,size_t block_count,FILE* fp);(緩衝區,塊大小,塊個數,文件流指針);

size_t fwrite(char*data,size_t block_size,size_t block_count,FILE* fp);(數據首地址,塊大小,塊個數,文件流指針);

  • 注意:fread/fwrite操作的數據實際大小=塊大小*塊個數;(例:塊大小爲10,塊個數爲2,則需要寫入/讀取20個字節的數據);
  • 返回值:返回實際操作的塊個數;(例:讀取一個文件size爲10,count爲2,如果文件大小足夠則返回2,但是若文件大小隻有16個字節,則返回1,因爲第二塊沒有讀滿;fread如果讀到了文件末尾則會返回0)
  • 如果讀取1000個字節,塊個數爲1,文件大小隻有512字節,雖然讀取了512個數據但是也會返回0

int fseek(FILE* fp,long offset,int whence);(將文件的讀寫指針從whence位置偏移offset個字節)- - - - -跳轉文件讀寫位置

int fclose(FILE* fp);- - - - - 關閉文件流指針,釋放資源

fread/fwrite的塊大小一般設定爲1,塊個數設爲想要操作的數據長度

fseek:文件沒有數據也可以跳轉到讀寫位置;

對文件數據進行字符串操作是時候要注意文件數據中\0這種數據

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

int main()
{
    FILE *fp = NULL;

    fp = fopen("./test.txt", "r+");
    if (fp == NULL) {
        perror("fopen error");//打印上一個系統調用接口的使用錯誤原因
        return -1;
    }

    //fseek跳轉讀寫位置 SEEK_SET-從文件起始偏移 SEEK_CUR-從當前讀寫位置開偏移
    //SEEK_END-從文件末尾偏移
    fseek(fp, 10, SEEK_END); //將文件的讀寫位置偏移到末尾

    //sizeof獲取的是一塊空間的大小 / strlen獲取的是字符串的長度遇到\0截止
    char buf[] = "It's a fine day\0 today~~\n";
    int ret = fwrite(buf, strlen(buf), 1, fp);//(數據,塊大小,塊個數,流指針);
    if (ret == 0) {
        perror("fwrite error");
        return -1;
    }
    printf("write ret:%d\n", ret);

    fseek(fp, 0, SEEK_SET);

    char tmp[1024] = {0};
    ret = fread(tmp, 1, 1023, fp);//在塊大小爲1情況下不會出現讀取到數據依然返回0的情況
    if (ret == 0) {
        printf("have no data or error\n");
    }
    printf("ret:%d-[%s]\n", ret, tmp);

    fclose(fp);
    return 0;
}

標準的IO接口(都是庫函數,而庫函數就是對系統調用接口的一層封裝)

系統調用IO接口的學習:open\read\write\seek\close

 int open(char* filename,int flag,mode_t mode);

filename:要打開的文件名稱;

flag:選項參數,文件的打開方式,必選項/可選項

  • 必選項(只能選擇其一):O_WRONLY(只寫);O_RDWR(讀寫);O_TRUNC(打開文件的同時清空原有內容);O_APPEND(追加寫,總是將數據寫入到文件末尾)。
  • mode:權限,如果使用了O_CREAT有可能創建新文件,就一定要指定文件權限,八進制數字形式。
  • 返回值:一個非負數,文件描述符,文件的操作句柄;失敗返回-1;

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

fd:open返回的文件描述符,文件操作句柄,通過這個fd指定要往那個文件寫入數據

buf:要寫入文件的數據的空間首地址

count:要寫入的數據大小

返回值:返回實際寫入文件的數據字節長度;失敗則返回-1

ssize_t read(int fd,char* buf,size_t len);

fd:open返回的文件描述符,文件的操作句柄;

buf:從文件中讀取數據放到哪個緩衝區中的首地址;

len:想要讀取的數據長度,注意這個len不能大於緩衝區的大小;

返回值:返回的是實際讀取到的數據字節長度,錯誤返回-1;

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

fd:open返回的文件描述符;

offset:偏移量;

whence:從哪開始偏移;SEEK_SET(文件起始位置),SEEK_CUR(文件當前讀寫位置),SEEK_END(文件末尾);

返回值:成功返回當前位置相對於起始位置的偏移量;失敗返回-1;

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

int main()
{
    int fd = -1;
    umask(0);//將當前進程的文件創建權限掩碼設置位0-僅當前進程有效
    fd = open("./test.txt", O_RDWR|O_CREAT, 0777);
    if (fd < 0) {
        perror("open error");
        return -1;
    }
    lseek(fd, 10, SEEK_END);// 目的讀寫位置跳轉到文件末尾
    char ptr[1024] = "hello world~\n";
    int ret = write(fd, ptr, strlen(ptr));
    if (ret < 0) {
        perror("write error");
        return -1;
    }
    printf("ret:%d\n", ret);

    lseek(fd, 0, SEEK_SET);// 跳轉到文件的起始位置
    char buf[1024] = {0};
    ret = read(fd, buf, 1024);
    if (ret < 0) {
        perror("read error");
        return -1;
    }
    printf("ret:%d-[%s]\n", ret, buf);

    close(fd);
    return 0;
}

int close(int fd);通過文件描述符關閉文件,釋放資源;

爲什麼打開一個文件,如果不操作一定要關閉,釋放資源?

答:因爲文件描述符實際上是有限的,若不關閉文件,文件描述符用光,則在進程中就打不開新文件了。

一個進程運行起來,進程會默認打開三個文件:標準輸入 0-stdin/標準輸出 1-stdout/標準錯誤 2-stderr;

文件描述符的分配原則:最小未使用;

print打印數據到標準輸出,close(1)是把標準輸出關閉了;打開新文件後,printf並沒有把數據打印出來,而是在刷新緩衝區之後,兩數據寫入到了文件中。

printf並非幀的一定要把數據寫入標準輸出文件,而是因爲printf函數中操作文件的時候操作的描述符是1。原本向1中寫入數據就是向標準輸出寫入,然後當1指向了新的文件後,這個printf就會將數據寫入到新的文件中重定向:將數據不再寫入原本的文件,而是寫入新的指定的文件中,實現方式就是替換這個描述符對應的文件描述信息。實際上是描述符的重定向,改變描述符所指向的文件,就是改變了數據的流向。

int dup2(int oldfd,int newfd);- - - - -描述重定向函數

功能:讓newfd這個描述符也指向oldfd所指向的文件,這時候oldfd和newfd都能夠操作oldfd所指向的文件。

**重定向實現原理**:每個文件描述符都是一個內核中文件描述信息數組的下標,對應有一個文件的描述信息用於操作文件,而重定向就是在不改變所操作的文件描述符的情況下,通過改變描述符對應的文件描述信息進而實現改變所操作的文件。

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

int main()
{
    //close(1);//關閉0號描述符-就是關閉了標準輸入
    /*
    umask(0);
    int fd = open("./test.txt", O_RDWR|O_CREAT, 0664);
    if (fd < 0) {
        perror("open error");
        return -1;
    }
    dup2(fd, 1);//將1重定向到test.txt這個文件
    printf("fd=%d\n", fd);
    fflush(stdout);//刷新標準輸出緩衝區
    close(fd);
    */
    FILE *fp = fopen("./test.txt", "r+");
    fp->_fileno = 1;//將文件流指針中的文件描述符改成標準輸出的描述符了
    fwrite("hello world\n", 1, 12, fp);
    fclose(fp);
}

在minishell中實現>/>>標準輸出重定向

 

>清空重定向open(O_CREAT|O_TRUNC);

>>追加重定向opend(O_CREAT|O_APPEND);

a.txt  fd = open(a.txt);
dup2(fd,1);   //子進程在運行指令的時候,ls本身要將數據寫入標準輸出
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>

int main()
{
    while(1) {
        //增加一個shell提示
        printf("[san@minishell]$ ");
        fflush(stdout);//刷新標準輸出緩衝區
        //1. 等待標準輸入
        char buf[1024] = {0};
        fgets(buf, 1023, stdin);
        buf[strlen(buf)-1] = '\0'; //buf[...]='\n'
        //1.5 解析重定向
        //ls -l  > a.txt
        char *ptr = buf;
        int redirect_flag = 0;
        char *redirect_file = NULL;
        while(*ptr != '\0') {
            if (*ptr == '>') {
                redirect_flag = 1;//這是清空重定向
                *ptr = '\0';//將>替換成結尾標誌,則命令的解析到此位置就完畢了
                ptr++;
                if (*ptr == '>') {//有第二個>則是追加重定向
                    redirect_flag = 2;
                    *ptr = '\0';
                    ptr++;
                }
                while(*ptr == ' ' && *ptr != '\0') ptr++;//將a.txt之前的空格走完
                redirect_file = ptr;//redirect_file這個指針指向了a.txt中a的位置
                while(*ptr != ' ' && *ptr != '\0') ptr++; // 將a.txt字符走完
                *ptr = '\0';
            }
            ptr++;
        }
        //2. 對輸入命令數據進行解析
        char *argv[32] = {NULL};
        int argc = 0;
        ptr = buf;
        // [    ls    -a    -l    ]
        while(*ptr != '\0') {
            if (*ptr != ' ') {
                argv[argc] = ptr;
                argc++;
                while(*ptr != ' ' && *ptr != '\0') {
                    ptr++;               
                }
                *ptr = '\0';
            }
            ptr++;
        }
        argv[argc] = NULL;//最後一個參數的下一個位置置NULL
        //3. 創建子進程 4. 在子進程中程序替換
        pid_t pid = fork();
        if (pid == 0) {
            if (redirect_flag == 1) {//清空重定向
                int fd = open(redirect_file, O_WRONLY|O_CREAT|O_TRUNC, 0664);
                dup2(fd, 1);//將標準輸入重定向到redirect_file;原本要打印的數據就會被寫入文件
            }else if (redirect_flag == 2){ // 追加重定向
                int fd = open(redirect_file, O_WRONLY|O_CREAT|O_APPEND, 0664);
                dup2(fd, 1);//將標準輸入重定向到redirect_file;原本要打印的數據就會被寫入文件
            }
            //execvp(char *file, char *argv[])  file--新程序名稱 
            execvp(argv[0], (char**)argv);//程序替換成功就去運行新程序了,32行以後就不會運行了
            //能夠走到第33行,那麼肯定程序替換失敗了
            perror("execvp error");//打印上一次系統調用接口使用的錯誤原因
            exit(0);
        }
        //5. 進程等待
        wait(NULL);
    }
    return 0;
}

文件描述符與文件流指針的關係:

文件描述符:是一個非負整數,系統調用的IO接口;在進程中每打開一個文件,都會創建有相應的文件描述信息struct file,這個描述信息被添加在pcb的struct files_struct中,以數組的形式進行管理,隨即向用戶返回數組的下標作爲文件描述符,用於操作文件。

文件流指針:FILE結構體,typedef struct _IO_FILE FILE - - - - -庫函數IO接口的操作句柄

通過文件流指針進行最終文件操作的時候,依然還要能夠找到文件對應的文件描述符纔可以,文件流指針是一個結構體,結構體中有很多的成員變量,其中有一個叫做_fileno- - -這就是文件描述符;

向文件寫入數據,並不會直接寫入文件,而是先寫入緩衝區中,刷新緩衝區的時候纔會寫入文件;(只有庫函數才存在這個緩衝區)

系統調用接口是直接將數據寫入文件的,系統調用接口是沒有這個緩衝區的。

exit( )退出會刷新緩衝區,而_exit( )退出時不會刷新緩衝區。

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章