基礎IO(2)
理解“?切皆?件”
?先,在windows中是?件的東西,它們在linux中也是?件;其次?些在windows中不是?件的東西,?如進程、磁盤、顯?器、鍵盤這樣硬件設備也被抽象成了?件,你可以使?訪問?件的?法訪問它們獲得信息;甚?管道,也是?件;將來我們要學習?絡編程中的socket(套接字)這樣的東西,使?的接?跟?件接?也是?致的。
這樣做最明顯的好處是,開發者僅需要使??套 API 和開發?具,即可調取 Linux 系統中絕?部分的資源。舉個簡單的例?,Linux 中?乎所有讀(讀?件,讀系統狀態,讀PIPE)的操作都可以?read 函數來進?;?乎所有更改(更改?件,更改系統參數,寫 PIPE)的操作都可以? write 函數來進?。
之前我們講過,當打開?個?件時,操作系統為了管理所打開的?件,都會為這個?件創建?個file結構體,該結構體定義在 /usr/src/kernels/3.10.0-1160.71.1.el7.x86_64/include/linux/fs.h 下,以下展?了該結構部分我們關系的內容:
struct file {
...
struct inode *f_inode; /* cached value */
const struct file_operations *f_op;
...
atomic_long_t f_count; // 表?打開?件的引?計數,如果有多個?件指針指向
它,就會增加f_count的值。
unsigned int f_flags; // 表?打開?件的權限
fmode_t f_mode; // 設置對?件的訪問模式,例如:只讀,只寫等。所有
的標志在頭?件<fcntl.h> 中定義
loff_t f_pos; // 表?當前讀寫?件的位置
...
} __attribute__((aligned(4))); /* lest something weird decides that 2 is OK */
值得關注的是 struct file 中的 f_op 指針指向了?個 file_operations 結構體,這個結構體中的成員除了struct module* owner 其余都是函數指針。該結構和 struct file 都在fs.h下。
struct file_operations {
struct module *owner;
//指向擁有該模塊的指針;
loff_t (*llseek) (struct file *, loff_t, int);
//llseek ?法?作改變?件中的當前讀/寫位置, 并且新位置作為(正的)返回值.
ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);
//?來從設備中獲取數據. 在這個位置的?個空指針導致 read 系統調?以 -
EINVAL("Invalid argument") 失敗. ?個?負返回值代表了成功讀取的字節數( 返回值是?個
"signed size" 類型, 常常是?標平臺本地的整數類型).
ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);
//發送數據給設備. 如果 NULL, -EINVAL 返回給調? write 系統調?的程序. 如果?負, 返
回值代表成功寫的字節數.
ssize_t (*aio_read) (struct kiocb *, const struct iovec *, unsigned long,
loff_t);
//初始化?個異步讀 -- 可能在函數返回前不結束的讀操作.
ssize_t (*aio_write) (struct kiocb *, const struct iovec *, unsigned long,
loff_t);
//初始化設備上的?個異步寫.
int (*readdir) (struct file *, void *, filldir_t);
//對于設備?件這個成員應當為 NULL; 它?來讀取?錄, 并且僅對**?件系統**有?.
unsigned int (*poll) (struct file *, struct poll_table_struct *);
int (*ioctl) (struct inode *, struct file *, unsigned int, unsigned long);
long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);
long (*compat_ioctl) (struct file *, unsigned int, unsigned long);
int (*mmap) (struct file *, struct vm_area_struct *);
//mmap ?來請求將設備內存映射到進程的地址空間. 如果這個?法是 NULL, mmap 系統調?返
回 -ENODEV.
int (*open) (struct inode *, struct file *);
//打開?個?件
int (*flush) (struct file *, fl_owner_t id);
//flush 操作在進程關閉它的設備?件描述符的拷?時調?;
int (*release) (struct inode *, struct file *);
//在?件結構被釋放時引?這個操作. 如同 open, release 可以為 NULL.
int (*fsync) (struct file *, struct dentry *, int datasync);
//??調?來刷新任何掛著的數據.
int (*aio_fsync) (struct kiocb *, int datasync);
int (*fasync) (int, struct file *, int);
int (*lock) (struct file *, int, struct file_lock *);
//lock ?法?來實現?件加鎖; 加鎖對常規?件是必不可少的特性, 但是設備驅動?乎從不實現
它.
ssize_t (*sendpage) (struct file *, struct page *, int, size_t, loff_t *,
int);
unsigned long (*get_unmapped_area)(struct file *, unsigned long, unsigned
long, unsigned long, unsigned long);
int (*check_flags)(int);
int (*flock) (struct file *, int, struct file_lock *);
ssize_t (*splice_write)(struct pipe_inode_info *, struct file *, loff_t *,
size_t, unsigned int);
ssize_t (*splice_read)(struct file *, loff_t *, struct pipe_inode_info *,
size_t, unsigned int);
int (*setlease)(struct file *, long, struct file_lock **);
};
file_operation 就是把系統調?和驅動程序關聯起來的關鍵數據結構,這個結構的每?個成員都對應著?個系統調?。讀取 file_operation 中相應的函數指針,接著把控制權轉交給函數,從?完成了Linux設備驅動程序的?作。
介紹完相關代碼,?張圖總結:
上圖中的外設,每個設備都可以有??的read、write,但?定是對應著不同的操作?法!!但通過struct file 下 file_operation 中的各種函數回調,讓我們開發者只?file便可調取 Linux 系統中絕?部分的資源!!這便是“linux下?切皆?件”的核?理解。
緩沖區
什么是緩沖區
緩沖區是內存空間的?部分。也就是說,在內存空間中預留了?定的存儲空間,這些存儲空間?來緩沖輸?或輸出的數據,這部分預留的空間就叫做緩沖區。緩沖區根據其對應的是輸?設備還是輸出設備,分為輸?緩沖區和輸出緩沖區。
為什么要引?緩沖區機制
讀寫?件時,如果不會開辟對?件操作的緩沖區,直接通過系統調?對磁盤進?操作(讀、寫等),那么每次對?件進??次讀寫操作時,都需要使?讀寫系統調?來處理此操作,即需要執??次系統調?,執??次系統調?將涉及到CPU狀態的切換,即從??空間切換到內核空間,實現進程上下?的切換,這將損耗?定的CPU時間,頻繁的磁盤訪問對程序的執?效率造成很?的影響。
為了減少使?系統調?的次數,提?效率,我們就可以采?緩沖機制。?如我們從磁盤?取信息,可以在磁盤?件進?操作時,可以?次從?件中讀出?量的數據到緩沖區中,以后對這部分的訪問就不需要再使?系統調?了,等緩沖區的數據取完后再去磁盤中讀取,這樣就可以減少磁盤的讀寫次數,再加上計算機對緩沖區的操作? 快于對磁盤的操作,故應?緩沖區可? 提?計算機的運?速度。
??如,我們使?打印機打印?檔,由于打印機的打印速度相對較慢,我們先把?檔輸出到打印機相應的緩沖區,打印機再??逐步打印,這時我們的CPU可以處理別的事情。可以看出,緩沖區就是?塊內存區,它?在輸?輸出設備和CPU之間,?來緩存數據。它使得低速的輸?輸出設備和?速的CPU能夠協調?作,避免低速的輸?輸出設備占?CPU,解放出CPU,使其能夠?效率?作。
緩沖類型
標準I/O提供了3種類型的緩沖區。
? 全緩沖區:這種緩沖?式要求填滿整個緩沖區后才進?I/O系統調?操作。對于磁盤?件的操作通常使?全緩沖的?式訪問。
? ?緩沖區:在?緩沖情況下,當在輸?和輸出中遇到換?符時,標準I/O庫函數將會執?系統調?操作。當所操作的流涉及?個終端時(例如標準輸?和標準輸出),使??緩沖?式。因為標準I/O庫每?的緩沖區?度是固定的,所以只要填滿了緩沖區,即使還沒有遇到換?符,也會執?I/O系統調?操作,默認?緩沖區的??為1024。
? ?緩沖區:?緩沖區是指標準I/O庫不對字符進?緩存,直接調?系統調?。標準出錯流stderr通常是不帶緩沖區的,這使得出錯信息能夠盡快地顯?出來。
除了上述列舉的默認刷新?式,下列特殊情況也會引發緩沖區的刷新:
-
緩沖區滿時;
-
執?flush語句;
?例如下:
#include <stdio.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
int main() {
close(1);
int fd = open("log.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);
if (fd < 0) {
perror("open");
return 0;
}
printf("hello world: %d\n", fd);
close(fd);
return 0;
}
我們本來想使?重定向思維,讓本應該打印在顯?器上的內容寫到“log.txt”?件中,但我們發現,程序運?結束后,?件中并沒有被寫?內容:
[hyb@VM-8-12-centos buffer]$ ./myfile
[hyb@VM-8-12-centos buffer]$ ls
log.txt makefile myfile myfile.c
[hyb@VM-8-12-centos buffer]$ cat log.txt
[hyb@VM-8-12-centos buffer]$
這是由于我們將1號描述符重定向到磁盤?件后,緩沖區的刷新?式成為了全緩沖。?我們寫?的內容并沒有填滿整個緩沖區,導致并不會將緩沖區的內容刷新到磁盤?件中。怎么辦呢?可以使?fflush強制刷新下緩沖區。
#include <stdio.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
int main() {
close(1);
int fd = open("log.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);
if (fd < 0) {
perror("open");
return 0;
}
printf("hello world: %d\n", fd);
fflush(stdout);
close(fd);
return 0;
}
還有?種解決?法,剛好可以驗證?下stderr是不帶緩沖區的,代碼如下:
#include <stdio.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
int main() {
close(2);
int fd = open("log.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);
if (fd < 0) {
perror("open");
return 0;
}
perror("hello world");
close(fd);
return 0;
}
這種?式便可以將2號?件描述符重定向??件,由于stderr沒有緩沖區,“hello world”不?fflush就可以寫??件:
[hyb@VM-8-12-centos buffer]$ ./myfile
[hyb@VM-8-12-centos buffer]$ cat log.txt
hello world: Success
FILE
? 因為IO相關函數與系統調?接?對應,并且庫函數封裝系統調?,所以本質上,訪問?件都是通過fd訪問的。
? 所以C庫當中的FILE結構體內部,必定封裝了fd
#include <stdio.h>
#include <string.h>
int main()
{
const char *msg0="hello printf\n";
const char *msg1="hello fwrite\n";
const char *msg2="hello write\n";
printf("%s", msg0);
fwrite(msg1, strlen(msg0), 1, stdout);
write(1, msg2, strlen(msg2));
fork();
return 0;
}
運?出結果:
hello printf
hello fwrite
hello write
但如果對進程實現輸出重定向呢? ./hello > file , 我們發現結果變成了:
hello write
hello printf
hello fwrite
hello printf
hello fwrite
我們發現 printf 和 fwrite (庫函數)都輸出了2次,? write 只輸出了?次(系統調?)。為什么呢?肯定和fork有關!
? ?般C庫函數寫??件時是全緩沖的,?寫?顯?器是?緩沖。
? printf fwrite 庫函數+會?帶緩沖區(進度條例?就可以說明),當發?重定向到普通?件時,數據的緩沖?式由?緩沖變成了全緩沖。
? ?我們放在緩沖區中的數據,就不會被?即刷新,甚?fork之后
? 但是進程退出之后,會統?刷新,寫??件當中。
? 但是fork的時候,??數據會發?寫時拷?,所以當你?進程準備刷新的時候,?進程也就有了同樣的?份數據,隨即產?兩份數據。
? write 沒有變化,說明沒有所謂的緩沖。
綜上: printf fwrite 庫函數會?帶緩沖區,? write 系統調?沒有帶緩沖區。另外,我們這?所說的緩沖區,都是??級緩沖區。其實為了提升整機性能,OS也會提供相關內核級緩沖區,不過不再我們討論范圍之內。
那這個緩沖區誰提供呢? printf fwrite 是庫函數, write 是系統調?,庫函數在系統調?的“上層”, 是對系統調?的“封裝”,但是 write 沒有緩沖區,? printf fwrite 有,?以說明,該緩沖區是?次加上的,?因為是C,所以由C標準庫提供。
簡單設計?下libc庫
my_stdio.h
$ cat my_stdio.h
#pragma once
#define SIZE 1024
#define FLUSH_NONE 0
#define FLUSH_LINE 1
#define FLUSH_FULL 2
struct IO_FILE
{
int flag; // 刷新?式
int fileno; // ?件描述符
char outbuffer[SIZE];
int cap;
int size;
// TODO
};
typedef struct IO_FILE mFILE;
mFILE *mfopen(const char *filename, const char *mode);
int mfwrite(const void *ptr, int num, mFILE *stream);
void mfflush(mFILE *stream);
void mfclose(mFILE *stream);
my_stdio.c
$ cat my_stdio.c
#include "my_stdio.h"
#include <string.h>
#include <stdlib.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <fcntl.h>
#include <unistd.h>
mFILE *mfopen(const char *filename, const char *mode)
{
int fd = -1;
if(strcmp(mode, "r") == 0)
{
fd = open(filename, O_RDONLY);
}
else if(strcmp(mode, "w")== 0)
{
fd = open(filename, O_CREAT|O_WRONLY|O_TRUNC, 0666);
}
else if(strcmp(mode, "a") == 0)
{
fd = open(filename, O_CREAT|O_WRONLY|O_APPEND, 0666);
}
if(fd < 0) return NULL;
mFILE *mf = (mFILE*)malloc(sizeof(mFILE));
if(!mf)
{
close(fd);
return NULL;
}
mf->fileno = fd;
mf->flag = FLUSH_LINE;
mf->size = 0;
mf->cap = SIZE;
return mf;
}
void mfflush(mFILE *stream)
{
if(stream->size > 0)
{
// 寫到內核?件的?件緩沖區中!
write(stream->fileno, stream->outbuffer, stream->size);
// 刷新到外設
fsync(stream->fileno);
stream->size = 0;
}
}
int mfwrite(const void *ptr, int num, mFILE *stream)
{
// 1. 拷?
memcpy(stream->outbuffer+stream->size, ptr, num);
stream->size += num;
// 2. 檢測是否要刷新
if(stream->flag == FLUSH_LINE && stream->size > 0 && stream-
>outbuffer[stream->size-1]== '\n')
{
mfflush(stream);
}
return num;
}
void mfclose(mFILE *stream)
{
if(stream->size > 0)
{
mfflush(stream);
}
close(stream->fileno);
}
main.c
$ cat main.c
#include "my_stdio.h"
#include <stdio.h>
#include <string.h>
#include <unistd.h>
int main()
{
mFILE *fp = mfopen("./log.txt", "a");
if(fp == NULL)
{
return 1;
}
int cnt = 10;
while(cnt)
{
printf("write %d\n", cnt);
char buffer[64];
snprintf(buffer, sizeof(buffer),"hello message, number is : %d", cnt);
cnt--;
mfwrite(buffer, strlen(buffer), fp);
mfflush(fp);
sleep(1);
}
mfclose(fp);
}
#include <stdio.h>
#include <string.h>
#include <unistd.h>
int main()
{
mFILE *fp = mfopen(“./log.txt”, “a”);
if(fp == NULL)
{
return 1;
}
int cnt = 10;
while(cnt)
{
printf(“write %d\n”, cnt);
char buffer[64];
snprintf(buffer, sizeof(buffer),“hello message, number is : %d”, cnt);
cnt–;
mfwrite(buffer, strlen(buffer), fp);
mfflush(fp);
sleep(1);
}
mfclose(fp);
}