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;
}