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 & 775
⬇
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;
}