Linux系統編程-文件IO

目錄

1. 無處不在的系統調用

  • 但凡涉及與資源有關的操作、會影響其他進程的操作,都需要操作系統的介入支持,都需要通過系統調用來實現,其實系統調用從概念上來講也不難理解。
  • 由操作系統實現並提供給外部應用程序的編程接口(Application Programming Interface,API),是應用程序同系統之間數據交互的橋樑。

1.1 系統調用和庫函數的區別?

  • 系統調用是操作系統向上層提供的接口。
  • 庫函數是對系統調用的進一步封裝。
  • 應用程序大多是通過高級語言提供的庫函數,間接的進行系統調用。

1.2 調用的簡單過程

標庫函數和系統函數調用過程。

2. C標準庫的文件IO函數

  • fopen、fclose、fseek、fgets、fputs、fread、fwrite……
  • 在命令行,通過 man fopen…… 等可以查看系統定義的對應的標庫函數。

2.1 fopen 打開文件

  • r 只讀、r+讀寫、w只寫並截斷為0、w+讀寫並截斷為0、a追加只寫、a+追加讀寫。
  • 這些字符串參數 mode 值後面也可以添加b,可以通過 man-pages 看到。
函數 fopen 打開文件名為 path 指向的字符串的文件,將一個流與它關聯。

       參數 mode 指向一個字符串,以下列序列之一開始 (序列之後可以有附加的字符):

       r      打開文本文件,用於讀。流被定位於文件的開始。

       r+     打開文本文件,用於讀寫。流被定位於文件的開始。

       w      將文件長度截斷為零,或者創建文本文件,用於寫。流被定位於文件的開始。

       w+     打開文件,用於讀寫。如果文件不存在就創建它,否則將截斷它。流被定位於文件的開始。

       a      打開文件,用於追加 (在文件尾寫)。如果文件不存在就創建它。流被定位於文件的末尾。

       a+     打開文件,用於追加
              (在文件尾寫)。如果文件不存在就創建它。讀文件的初始位置是文件的開始,但是輸出總是被追加到文件
的末尾。

       字符串                       mode                      也可以包含字母                      ``b''
       作為最後一個字符,或者插入到上面提到的任何雙字符的字符串的兩個字符中間。這樣只是為了和      ANSI
       X3.159-1989  (``ANSI  C'')  標準嚴格保持兼容,沒有實際的效果;在所有的遵循 POSIX 的系統中,``b''
       都被忽略,包括        Linux。(其他系統可能將文本文件和二進制文件區別對待,如果在進行二進制文件的
       I/O,那麼添加 ``b'' 是個好主意,因為你的程序可能會被移植到非 Unix 環境中。)

2.2 按字符讀寫 fgetc、fputc

編譯運行看對應的輸出文件和控制台打印內容。

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
// 按照字符方式 fgetc(), fputc();
void test01()
{
  // 寫文件
  // 可讀可寫的方式打開文件,沒有就創建
  FILE *f_write = fopen("./test01.txt", "w+");
  if (f_write == NULL)
  {
    return;
  }
  char buf[] = "Read and write as characters";
  for (int i = 0; i < strlen(buf); i++)
  {
    fputc(buf[i], f_write);
  }

  // 關閉,會刷新緩衝區
  fclose(f_write);

  // 讀文件
  FILE *f_read = fopen("./test01.txt", "r");
  if (f_read == NULL)
  {
    return;
  }
  char ch;
  while ((ch = fgetc(f_read)) != EOF)
  {
    printf("%c", ch);
  }
  fclose(f_read);
}

int main(int argc, char *argv[])
{
  test01();
}

2.3 按行讀寫 fgets、fputs

編譯運行看對應的輸出文件和控制台打印內容。

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

void test02()
{
  // 寫入文件
  // 可寫的方式打開文件
  FILE *f_write = fopen("./test02.txt", "w");
  if (f_write == NULL)
  {
    return;
  }
  char *buf[] = {
      "hellow world\n",
      "hellow world1\n",
      "hellow world2\n"};
  int len = sizeof(buf) / sizeof(char *);
  for (int i = 0; i < len; i++)
  {
    fputs(buf[i], f_write);
  }
  fclose(f_write);

  // 讀取文件
  FILE *f_read = fopen("./test02.txt", "r");
  char *s = NULL;
  while (!feof(f_read))
  {
    char buf[1024] = {0};
    fgets(buf, 1024, f_read);
    printf("%s", buf);
  }
  fclose(f_read);
}

int main(int argc, char *argv[])
{
  test02();
}

2.4 按塊讀寫文件 fread、fwrite

  • 主要針對於自定義的數據類型,可以通過 二進制 的方式讀寫。
  • 編譯運行看對應的輸出文件和控制台打印內容。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

// 按照塊讀寫文件(自定義的數據類型,二進制):fread() fwrite();
void test03()
{
  // 寫文件
  FILE *f_write = fopen("./test03.txt", "wb");
  if (f_write == NULL)
  {
    return;
  }
  
  // 自定義結構體類型
  struct Person
  {
    char name[16];
    int age;
  };

  struct Person persons[5] =
      {
          {"zhangsan", 25},
          {"lisi", 25},
          {"wangwu", 25},
          {"zhuliu", 25},
          {"zhuoqi", 25},
      };
  int len = sizeof(persons) / sizeof(struct Person);
  for (int i = 0; i < 5; i++)
  {
    // 參數:數據地址、塊的大小、塊的個數、文件流
    fwrite(&persons, sizeof(struct Person), 5, f_write);
  }
  fclose(f_write);

  // 讀文件
  FILE *f_read = fopen("./test03.txt", "rb");
  if (f_read == NULL)
  {
    return;
  }
  struct Person temp[5];
  fread(&temp, sizeof(struct Person), len, f_read);
  for (int i = 0; i < len; i++)
  {
    printf("name: %s, age: %d \n", temp[i].name, temp[i].age);
  }
}

int main(int argc, char *argv[])
{
  test03();
}

2.5 按格式化讀寫文件 fprintf、fscanf

編譯運行看對應的輸出文件和控制台打印內容。

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

void test04()
{
  // 寫文件
  FILE *f_write = fopen("./test04.txt", "w");
  if (f_write == NULL)
  {
    return;
  }
  fprintf(f_write, "hello world %d year - %d - month %d - day", 2008, 8, 8);
  fclose(f_write);

  // 讀文件
  FILE *f_read = fopen("./test04.txt", "r");
  if (f_read == NULL)
  {
    return;
  }
  char buf[1024] = {0};

  while (!feof(f_read)) // 直到文件結束標識符,循環結束
  {
    fscanf(f_read, "%s", buf);
    printf("%s ", buf);
  }
  fclose(f_read);
}


int main(int argc, char *argv[])
{
  test04();
}

3. 系統open、close函數

3.1 通過man-pages查看函數

  • int open(const char *pathname, int flags);
  • int open(const char *pathname, int flags, mode_t mode);
  • int close(int fd);
  • 參數:文件路徑、讀寫方式、權限設置(一般O_CREAT,權限用8進制,如0664)

3.2 open 中 flags 參數說明

  • 頭文件:fcntl.h 中定義
  • O_RDONLY :只讀
  • O_WRONLY:只寫
  • O_RDWR:讀寫
  • O_APPEND: 追加
  • O_CREAT: 文件存在就使用,不存在就創建
  • O_EXCL:文件不存就創建,存在則返回錯誤信息
  • O_TRUNC: 文件截斷為0
  • O_NONBLOCK: 非阻塞的方式操作

3.3 open 中 mode 參數並不是文件真正權限

通過八進制創建文件的權限,在系統當中還要考慮umask。可以命令行運行 umask 進行查看。

標庫函數fopen的man-pages中也有關與這個 umask 的提及。

計算公式:新建真實文件權限 = mode & ~umask

如設置mode = 777,此時系統umask = 002,~umask取反得775,那麼真實創建出來的文件權限 777 & 775 = 775;

  // 理解過程如下
  文件真實權限
  ⬇
  mode & ~umask
  ⬇
  777 & ~(002)
  ⬇
  777 & 775s
  ⬇
  775

3.4 open常見錯誤

  • 打開文件不存在
  • 以寫方式打開只讀文件(打開文件沒有對應權限)
  • 以只寫方式打開目錄

3.5 系統open函數打開文件

編譯運行輸出

#include <unistd.h> // open close 引入的頭文件
#include <fcntl.h>
#include <stdio.h>
#include <errno.h> // errno 需要的頭文件

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

    // int fd = open("./dict.back", O_RDONLY | O_CREAT | O_TRUNC, 0777);

    int fd = open("./demo.txt", O_RDWR | O_TRUNC);

    printf("fd=%d \n", fd);
    
    // 這裡關閉,下面代碼中會產errno = 9;
    // close(fd);  
    
    if (fd != -1)
    {
        printf("open success");
    }
    else
    {
        // 出錯的時候會產生一個errno, 對應不同的錯誤細節。
        printf("errno=%d \n", errno);
        printf("open failure");
    }
    
    // close(fd);
    
    return 0;
}

4. PCB、文件描述符表、文件結構體

4.1 文件描述符表、文件結構體、PCB結構體之間的關係圖如下

4.2 task_struct 結構體

  • 控制台中可使用命令 locate /include/linux/sched.h,如果沒有locate 插件,可以根據系統提示命令行安裝。
  • 如定位文件目錄為:/usr/src/kernels/3.10.0-1160.11.1.el7.x86_64/include/linux/sched.h。
  • 打開文件可以看到,task_struct 中 保存了指向文件描述符表files指針。

4.3 文件描述符表

  • sched.h 頭文件中,PCB 結構體的成員變量 files_struct *file 指向文件描述符表。
  • 從應用程序使用角度,該指針可理解記憶成一個字符指針數組,通過下標 [0/1/2/3/4…] 找到對應的file結構體。
  • 本質是鍵值對, [0/1/2/3/4…] 分別對應具體file結構體地址。
  • 鍵值對使用的特性是自動映射的,系統會將自動找到使用下標的文件結構體。
  • 新打開文件,返迴文件描述符表中未使用的最小文件描述符,這個系統自動進行管理。
  • 三個文件鍵是系統是默認打開,如果要用,使用系統定義的宏。
    • 0->宏STDIN_FILENO 指向標準輸入文件。
    • 1->宏STDOUT_FILENO 指向標準輸出文件。
    • 2->宏STDERR_FILENO 指向標準錯誤文件。
  • files_struct 結構體中成員變量,fd_array 為 file描述符數組。
struct files_struct
{
    // 引用累加計數
  atomic_t count; 
    ...
    // 文件描述符數組
  struct file * fd_array[NR_OPEN_DEFAULT]; 
}

4.4 FILE結構體

  • file結構體主要包含文件描述符、文件讀寫位置、IO緩衝區三部分內容。
  • open一個文件,內核就維護一個結構體,用來操作文件。
  • 結構體文件可以命令行定位 locate /include/linux/fs.h。
  • vim /usr/src/kernels/3.10.0-1160.11.1.el7.x86_64/include/linux/fs.h。

舉例說明常用的成員變量

// 文件屬性操作函數指針
struct inode            *f_inode;       /* cached value */

// 文件內容操作函數指針
const struct file_operations    *f_op;

// 打開的文件數量
atomic_long_t           f_count;

// O_RDONLY、O_NONBLOCK、O_SYNC(文件的打開標誌)
unsigned int            f_flags;

// 文件的訪問權限
fmode_t                 f_mode;

// 文件的偏移量
loff_t                  f_pos;

// 文件所有者
struct fown_struct      f_owner;

...

4.5 最大打開文件數

單個進程默認打開文件的個數1024。命令查看unlimit -a 可看到open files 默認為1024。

可以改通過提示的 (-n) 修改當前 shell 進程打開最大文件數量 ,命令行 ulimit -n 4096。
但是只對當前運行進程生效,如果退出shell進程,再進入查看最大文件數變成原來的值1024

通過修改系統配置文件永久修改該值(不建議)。
vim /etc/security/limits.conf,按照格式要求修改。

cat /proc/sys/fs/file-max 可以查看該電腦最大可以打開的文件個數,受內存大小影響。

5. 系統read、write函數

5.1 通過man-pages查看函數

  • ssize_t read(int fd, void *buf, size_t count);
  • ssize_t write(int fd, const void *buf, size_t count);
  • read與write函數類似,但注意 read、write 函數的第三個參數有所區別。
int main(int argc, char *argv[]) {
    char buf[1024];
    int ret = 0;
    int fd = open("./dict.txt", O_RDWR);
    
    while(( ret = read(fd, buf, sizeof(buf)) ) != 0) {
        wirte(STDOUT_FILENO, buf, ret);
    }
    
    close(fd);
}

5.2 緩衝區的作用

  • 假設我們一次只讀一個位元組實現文件拷貝功能,使用read、write效率高,還是使用對應的標庫函數fgetc、fputc效率高?
  • 根據調用的順序,標庫函數-系統調用-底層設備。調用一次系統函數,有個權級切換,比較耗時。
  • 所以標庫函數理論上比系統調用的要快,通過下面兩個小節來說明一下。

5.2.1 標庫函數fgetc、fputc使用的標庫(用戶)緩衝區

  • 過程:fgetc –> 庫函數緩衝區 –> 系統調用write –> 磁盤
  • 標庫函數有自己的緩衝區4096位元組。
  • write(有用戶區切換到 kernel 區這樣的權級切換,一次刷4096位元組)。
  • 示例代碼如下通過fget、fputc 實現文件copy功能。
#include <stdio.h>
#include <stdlib.h>

int main(int argc, char *argv[]) {
    FILE *fp, *fp_out;
    int n;
    
    // 使用標庫函數打開
    // r:只讀,r+讀寫
    fp = fopen("./from.txt", "r");
    if (fp == NULL) {
        perror("fopen error");
        exit(1);
    }
    
    // w 只寫,並且截斷為0,w+ 讀寫,並且截斷為0
    fp_out = fopen("./to.txt", "w");
    if (fp == NULL) {
        perror("fopen error");
    }

    // 先存到庫函數去的緩存,4096位元組,滿了在調用系統函數寫入磁盤。
    while ((n = fgetc(fp)) != EOF) {
        fputc(n, fp_out);
    }

    fclose(fp);
    fclose(fp_out);

    return 0;
}

5.2.2 系統調用read、write使用系統緩衝區

  • 過程: 系統調用write –> 磁盤
  • 內核也有一個緩衝區,默認大小4096位元組。
  • 文件輸入,先到緩衝區,充滿再刷新到磁盤。
  • write(user區到kernel區權級切換,每次切換比較耗時。如果一次刷一個位元組,切換的次數會特別的多,比較慢)。
  • read、write函數也可以稱為 Unbuffered I/O,指的是無用戶級緩衝區。但不保證不使用內核緩衝區。
  • 示例代碼如下通過read、write 實現文件copy功能。
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <stdlib.h>
#include <errno.h>

// buf 緩存的大小。
// #define N 1024
#define N 1

int main(int argc, char *argv[]) {
    int fd, fd_out;
    int n;
    char buf[N];

    fd = open("from.txt", O_RDONLY);
    if (fd < 0) {
        perror("open from.txt error");
        exit(1);
    }

    fd_out = open("to.txt", O_WRONLY | O_CREAT | O_TRUNC, 0644);
    if (fd < 0) {
        perror("open to.txt error");
        exit(1);
    }

    while ((n = read(fd, buf, N))) {
        if (n < 0) {
            perror("read error");
            exit(1);
        }
        write(fd_out, buf, n);
    }

    close(fd);
    close(fd_out);

    return 0;
}

5.3 系統調用是否能被標庫函數完全替代?

  • 既然標庫函數減少了權級切換的次數,比系統調用快,但庫函數也不能完全可以替代系統調用。
  • 比如需要保持實時性的場景,即時通訊的QQ、微信等軟件。

5.4 預輸入緩輸出

  • 用戶區到內核區,權級切換比較耗時。所以通過緩存來提高讀寫效率。
  • 預輸入: 文件Input,如果客戶端需要100個位元組,系統內核先從磁盤讀滿緩衝區4096位元組(4KB),下一次讀取的時候,從緩衝區裏面讀取。
  • 緩輸出: 文件Output, 如果客戶端需要輸出100M位元組內容到磁盤,先存滿內核緩衝區4096位元組(4KB),再由系統內核一次次的刷新到磁盤中。

6. 系統錯誤處理函數

6.1 exit 函數

  • 頭文件:stdlib.h
  • 函數參數可以由開發人員約定,比如0表示正常退出,1表示異常退出。但是系統方法沒有強制要求。
...
if (fd < 0) {
  perror("open to.txt error");
  exit(1);  // 1表示異常,有開發人員相互協定
}

while ((n = read(fd, buf, N))) {
    if (n < 0) {
        perror("read error");
        exit(1);  // 1表示異常
    }
    write(fd_out, buf, n);
}

...

6.2 錯誤編號 errno

  • 對應不同類型錯誤編號和編號對應的描述。
  • 頭文件:errno.h
  • 頭文件位置: /usr/include/asm-generic/errno-base.h、/usr/include/asm-generic/errno.h

...
// 如打開文件不存在, 查看errno對應的編號,代碼如下
fd = open("test", O_RDONLY);
if (fd < 0)
{
    printf("errno = %d\n", errno);
    exit(1);
}
...

6.3 perror 函數

  • 會把上面errno對應的字符串描述一起拼接上,進行控制台打印。
  • void perror(const char *s)
...
// 以寫方式打開一個目錄
// fd = open("testdir", O_RDWR);
fd = open("testdir", O_WRONLY);
if (fd < 0)
{
    perror("open testdir error");
    exit(1);
}
...

6.4 strerror 函數

  • 返回錯誤編號對應的描述
  • 頭文件:string.h
  • char *strerror(int errnum);

printf ("open testdir error", strerror(errno));

6.5 錯誤處理的代碼示例

#include <unistd.h> //read write
#include <fcntl.h>  //open close O_WRONLY O_RDONLY O_CREAT O_RDWR
#include <stdlib.h> //exit
#include <errno.h>
#include <stdio.h> //perror
#include <string.h>

int main(void)
{
    int fd;
#if 0
    //打開文件不存在
    // fd = open("test", O_RDONLY | O_CREAT);
    fd = open("test", O_RDONLY);
    if (fd < 0)
    {
        printf("errno = %d\n", errno);
        printf("open test error: %s\n", strerror(errno));
        exit(1);
    }
    printf("open success");
#elif 0
    // 打開的文件沒有對應權限(以只寫方式打開一個只有讀權限的文件)
    if (fd < 0)
    {
        fd = open("test", O_WRONLY);
        // fd = open("test", O_RDWR);
        printf("errno = %d\n", errno);
        perror("open test error");
        exit(1);
    }
    printf("open success");

#endif
#if 1
    // 以寫方式打開一個目錄
    // fd = open("testdir", O_RDWR);
    fd = open("testdir", O_WRONLY);
    if (fd < 0)
    {
        perror("open testdir error");
        exit(1);
    }
#endif

    return 0;
}

7. 阻塞、非阻塞

7.1 阻塞和非阻塞概念

讀常規文件是不會阻塞的,不管讀多少位元組,read一定會在有限的時間內返回。從終端設備或網絡讀則不一定,如果從終端輸入的數據沒有換行符,調用read讀終端設備就會阻塞,如果網絡上沒有接收到數據包,調用read從網絡讀就會阻塞,至於會阻塞多長時間也是不確定的,如果一直沒有數據到達就一直阻塞在那裡。同樣,寫常規文件是不會阻塞的,而向終端設備或網絡寫則不一定。

現在明確一下阻塞(Block)這個概念。當進程調用一個阻塞的系統函數時,該進程被置於睡眠(Sleep)狀態,這時內核調度其它進程運行,直到該進程等待的事件發生了(比如網絡上接收到數據包,或者調用sleep指定的睡眠時間到了)它才有可能繼續運行。與睡眠狀態相對的是運行(Running)狀態,在Linux內核中,處於運行狀態的進程分為兩種情況:

正在被調度執行: CPU處於該進程的上下文環境中,程序計數器(eip)里保存着該進程的指令地址,通用寄存器里保存着該進程運算過程的中間結果,正在執行該進程的指令,正在讀寫該進程的地址空間。

就緒狀態: 該進程不需要等待什麼事件發生,隨時都可以執行,但CPU暫時還在執行另一個進程,所以該進程在一個就緒隊列中等待被內核調度。系統中可能同時有多個就緒的進程,那麼該調度誰執行呢?內核的調度算法是基於優先級和時間片的,而且會根據每個進程的運行情況動態調整它的優先級和時間片,讓每個進程都能比較公平地得到機會執行,同時要兼顧用戶體驗,不能讓和用戶交互的進程響應太慢。

7.2 終端設備

  • 文件描述符:STDIN_FILENO、STDOUT_FILE、STDERR_FILENO;
  • 上面三個文件描述符對應都是一個設備文件,/dev/tty。
  • 從控制台輸入內容到設備文件,這個過程就是阻塞的,對應STDIN_FILENO,會等待用戶輸入。
  • 阻塞與非阻塞是對於設備文件而言。

7.3 阻塞讀終端

...
   // 默認是用阻塞的方式
   fd = open("/dev/tty", O_RDONLY | O_NONBLOCK);

    if (fd < 0)
    {
        perror("open /dev/tty");
        exit(1);
    }
    else
    {
        printf("fd: %d", fd);
    }
... 

7.4 非阻塞讀終端(O_NONBLOCK)

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

#define MSG_TRY "try again\n"

int main(void)
{
    char buf[10];
    int fd, n;

    // 默認是阻塞的方式
    // fd = open("/dev/tty", O_RDONLY);

    // 使用 O_NONBLOCK 標誌,設置非阻塞讀終端
    fd = open("/dev/tty", O_RDONLY | O_NONBLOCK);

    if (fd < 0)
    {
        perror("open /dev/tty");
        exit(1);
    }
    else
    {
        printf("fd: %d", fd);
    }
tryagain:

    //-1 出錯  errno==EAGAIN 或者 EWOULDBLOCK
    n = read(fd, buf, 10);

    if (n < 0)
    {
        // 由於 open 時指定了 O_NONBLOCK 標誌,
        // 通過 read 讀設備,沒有數據到達返回-1,同時將 errno 設置為 EAGAIN 或 EWOULDBLOCK
        
        if (errno != EAGAIN)
        {
            perror("read /dev/tty");
            exit(1);
        }
        sleep(3);
        write(STDOUT_FILENO, MSG_TRY, strlen(MSG_TRY));
        goto tryagain;
    }
    write(STDOUT_FILENO, buf, n);
    close(fd);

    return 0;
}

7.5 非阻塞讀終端和等待超時

#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(int argc, char *argv[])
{
    char buf[10];
    int i;
    int fd;
    int n;
    
    //  使用 NONBLOCK 非阻塞
    fd = open("/dev/tty", O_RDONLY | O_NONBLOCK); 

    if (fd < 0)
    {
        perror("open /dev/tty");
        exit(1);
    }

    printf("open /dev/tty success ... %d \n", fd);

    // timeout
    for (i = 0; i < 5; ++i)
    {
        n = read(fd, buf, 10);
        if (n > 0)
        {
            // 讀到了東西,直接跳出循環
            break;
        }
        if (n != EAGAIN)
        {
            // EWOULDBLK
            perror("read /dev/tty");
            exit(1);
        }
        sleep(1);
        write(STDOUT_FILENO, MSG_TRY, strlen(MSG_TRY));
    }
    if (i == 5)
    {
        write(STDOUT_FILENO, MSG_TIMEOUT, strlen(MSG_TIMEOUT));
    }
    else
    {
        write(STDOUT_FILENO, buf, n);
    }
    close(fd);
    return 0;
}

7.6 read 函數返回值

7.6.1 返回 >0

實際讀取到的位元組數

7.6.2 返回 0

讀到文件末尾

7.6.3 返回 -1

  • errno != EAGAIN(或!= EWOULDBLOCK) read出錯
    • EAGAIN: enable again,Resource temporarily unavailable 表示資源短暫不可用,這個操作可能等下重試後可用。
    • EWOULDBLOCK:用於非阻塞模式,不需要重新讀或者寫
  • errno == EAGAIN (或== EWOULDBLOCK) read 正常,只不過沒有數據到達而已
    • 讀取了設備文件,設置了非阻塞讀,並且沒有數據到達。

8. lseek 函數

8.1 文件偏移

  • Linux中可使用系統函數lseek來修改文件偏移量(讀寫位置)。
  • 每個打開的文件都記錄著當前讀寫位置,打開文件時讀寫位置是0,表示文件開頭,通常讀寫多少個位元組就會將讀寫位置往後移多少個位元組。
  • 但是有一個例外,如果以O_APPEND方式打開,每次寫操作都會在文件末尾追加數據,然後將讀寫位置移到新的文件末尾。
  • lseek和標準I/O庫的fseek函數類似,可以移動當前讀寫位置(或者叫偏移量)。

8.2 標庫 fseek 函數

  • int fseek(FILE *stream, long offset, int whence)
  • fseek常用參數。 SEEK_SET、SEEK_CUR、SEEK_END
  • 成功返回0;失敗返回-1
  • PS:超出文件末尾位置返回0;往回超出文件頭位置,返回-1

8.3 系統 lseek 函數

  • lseek (int fd, off_t offset, int whence)
  • lseek常用參數。 SEEK_SET、SEEK_CUR、SEEK_END
  • 失敗返回 -1;成功返回 較文件起始位置向後的偏移量。
  • PS:lseek允許超過文件結尾設置偏移量,文件會因此被擴容。並且文件「讀」和「寫」使用同一偏移位置。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <fcntl.h>

int main(int argc, char *argv[])
{
    int fd;
    int n;
    int ret;

    char msg[] = "It's a test for lseek \n";
    char ch;

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

    if (fd < 0)
    {
        perror("open lseek.txt error");
        exit(1);
    }

    // 使用fd對打開的文件進行寫操作,寫完光標指針位於文件內容結尾處。
    write(fd, msg, strlen(msg));

    // 將文件內容指針,重置,設置從0開始,偏移12個位置。返回偏移量。
    ret = lseek(fd, 12, SEEK_SET);

    printf("offset len: %d \n", ret);

    while (n = read(fd, &ch, 1))
    {
        if (n < 0)
        {
            perror("read error");
            exit(1);
        }

        // 將文字內容按照位元組讀出,寫到屏幕
        write(STDOUT_FILENO, &ch, n);
    }

    close(fd);
    
    return 0;
}

8.4 lseek 常用操作

  • 文件的讀寫,使用一個光標指針,寫完文件,再去讀的話,需要重新設置指針目標。
  • PS: lseek函數返回的偏移量總是相對於文件頭而言。

8.4.1 使用lseek拓展文件

  • write操作才能實質性的拓展文件。
  • 單單lseek是不能進行拓展的,需要加一次實質性的IO操作。
  • 一般如write(fd, “c”, 1); 加一次實質性的IO操作。
  • 查看文件的16進制表示形式 od -tcx 文件名。
  • 查看文件的10進制表示形式 od -tcd 文件名。

8.4.2 標庫 truncate 函數

  • 截斷文件到具體specific長度,傳入通過文件路徑。
  • int truncate(const char *path, off_t length)。
  • 使用這個方法,文件必須可寫。
  • 成功返回0;失敗返回-1和設置errno。

8.4.3 系統 ftruncate 函數

  • 截斷文件到具體specific長度,傳入文件描述符。
  • 使用這個方法,文件必須open,且擁有可寫權限。
  • int ftruncate(int fd, off_t length)。
  • 成功返回0;失敗返回-1和設置errno。

8.4.4 通過lseek獲取文件的大小

int ret = lseek(fd, 0, SEEK_END);

8.4.5 綜合示例代碼如下

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

int main(int argc, char *argv[])
{
    int fd;
    int ret_len;
    int ret_truncate;

    fd = open("lseek.txt", O_RDWR | O_TRUNC | O_CREAT, 0664);
    if (fd < 0)
    {
        perror("open lseek.txt error");
        exit(1);
    }

    // 可以用來文件長度, 從末尾開始,偏移到頭。返回偏移量
    ret_len = lseek(fd, 0, SEEK_END);

    if (ret_len == -1)
    {
        perror("lseek error");
        exit(1);
    }

    printf("len of msg = %d\n", ret_len);

    // truncate(const char *path, off_t length) 截斷文件到具體長度,文件必須可寫, 成功返回0,失敗返回-1

    // ftruncate(int fd, off_t length) 截斷文件到具體長度,文件必須打開,成功返回0,失敗返回-1

    ret_truncate = ftruncate(fd, 1800);

    if (ret_truncate == -1)
    {
        perror("ftruncate error");
        exit(1);
    }

    printf("ftruncate file success, and ret_truncate is %d \n", ret_truncate);
#if 1

    ret_len = lseek(fd, 999, SEEK_SET);
    if (ret_len == -1)
    {
        perror("lseek seek_set error");
        exit(1);
    }

    int ret = write(fd, "a", 1);
    if (ret == -1)
    {
        perror("write error");
        exit(1);
    }

#endif

#if 0
    off_t cur = lseek(fd, -10, SEEK_SET);
    printf(" ****** %ld \n", cur);
    if (cur == -1) {
        perror("lseek error");
        exit(1);
    }
#endif
    close(fd);
    return 0;
}

9. fcntl 函數

  • 頭文件 fcntl.h
  • 文件控制 file control,改變一個已經打開的文件的訪問控制屬性。不需要重新open設置。
  • int fcntl(int fd, int cmd, … /* arg */ )
  • 兩個參數,F_GETFL 和 F_SETFL 重點需要掌握

9.1 F_GETFL(get file flags)

獲取文件描述符,對應文件的屬性信息

9.2 F_SETFL(set file flags)

設置文件描述符,對應文件的屬性信息

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

#define MSG_TRY "try again \n"

int main(int argc, char *argv[])
{
    char buf[10];
    int flags;
    int n;

    // 獲取stdin屬性信息
    flags = fcntl(STDIN_FILENO, F_GETFL);
    if (flags == -1)
    {
        perror("fcntl error");
        exit(1);
    }

    // 位或操作,加入非阻塞操作權限(這樣文件不用重新通過設置權限的方式打開)
    flags |= O_NONBLOCK;
    
    int ret = fcntl(STDIN_FILENO, F_SETFL, flags);
    if (ret == -1)
    {
        perror("fcntl error");
        exit(1);
    }

tryagain:
    n = read(STDIN_FILENO, buf, 10);
    if (n < 0)
    {
        if (errno != EAGAIN)
        {
            perror("read /dev/tty");
            exit(1);
        }
        sleep(3);
        write(STDOUT_FILENO, MSG_TRY, strlen(MSG_TRY));
        goto tryagain;
    }
    write(STDOUT_FILENO, buf, n);
    return 0;
}

10. ioctl函數

  • 頭文件:sys/ioctl.h,文件位置 locate sys/ioctl.h。
  • 主要應用於設備驅動程序中,對設備的I/O通道進行管理,控制設備特性。
  • 通常用來獲取文件的物理特性,不同文件類型所含有的特性值各不相同。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/ioctl.h>

int main(void) {
    // 定義一個包含窗口大小的結構體。
    struct winsize size;
    
    // isatty 如果是不是終端,返回0
    if (isatty(STDOUT_FILENO) == 0) {
        exit(1);
    }
    
    if (ioctl(STDOUT_FILENO, TIOCGWINSZ, &size) < 0) {
        perror("ioctl TIOCGWINSZ error");
        exit(1);
    }
    
    // 輸出控制台行和列
    printf("%d rows, %d colums \n", size.ws_row, size.ws_col);
    
    return 0;
}
Tags: