不同作業系統所暴露出的介面是不同的,因此Linux下的一些系統呼叫介面是無法移植到Windows下的,
文章目錄
- 一、C語言中的檔案介面
- 二、系統檔案I/O
- 2.1.系統呼叫介面
- open
- 2.2.檔案描述符fd(file descriptor)
- 2.3.補充內容--函式指標訪問硬體
- 2.4.重定向的實作原理
- 三、FILE
- 四、dup 重定向
- 4.1.使用dup2 完成重定向
- 4.2.重定向恢復
- 4.3.在my_shell中添加重定向功能
- 總結
一、C語言中的檔案介面
在C語言檔案操作時學過檔案介面C語言中的檔案介面
這里補充一些內容:
檔案=內容+屬性
stdin:標準輸入(鍵盤)
stdout:標準輸出(顯示幕)
stderr:標準錯誤(顯示幕)
Linux下一切皆檔案,所以stdin,stdout,stderr也是可以當做檔案看待,被fopen打開,
這是因為語言是人和計算機互動的載體,所以任何一門語言都需要標準輸入、輸出和錯誤
之所以能直接使用printf和scanf這種輸出到顯示幕和從鍵盤輸入的函式,是因為C語言默認幫助我們打開了這三個標準輸入和輸出的設備,
用下面的代碼驗證:



可以看到普通檔案和顯示幕檔案在代碼層面上是沒有差別的,
二、系統檔案I/O
2.1.系統呼叫介面
操作檔案,除了上述C介面(當然,C++也有介面,其他語言也有),我們還可以采用系統介面來進行檔案訪問,使用man手冊可以查看他們:




fd:要打開檔案的描述符
buf:寫入/讀入的字串指標
count:寫入/讀入字符的個數(位元組數)
實際上,我們上面用的C語言檔案介面,在底層用的就是系統呼叫的介面:

系統呼叫的介面只有一套,和系統有關,而庫函式可以有多個(C語言檔案介面,C++檔案介面等),語言層的庫函式檔案介面都是基于系統呼叫介面進行的封裝,
由于顯示幕是硬體,硬碟也是硬體,所以任何語言的檔案操作最終都要通過作業系統進行對硬體的寫入,
open
通過mani手冊可以查看用法:
要包含的頭檔案:
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
打開的檔案存在,使用下面的介面:
int open(const char *pathname, int flags);
打開的檔案不存在,使用下面的介面:
int open(const char *pathname, int flags, mode_t mode);
引數:
pathname:要打開檔案的名字
flags:打開檔案的方式
O_RDONLY :只讀方式
O_WRONLY :只寫方式
O_RDWR :讀寫方式
可選項:
O_TRUNC :截斷檔案(清空檔案內容)
O_CREAT :檔案不存在則創建檔案
O_APPEND :追加的方式
O_EXCL | O_CREAT :如果檔案存在,則打開檔案失敗
mode:創建檔案時的權限(八進制,比如0664等)
回傳值:
成功:新打開的檔案描述符
失敗:-1
可選項的原理:

所有選項對應的數在轉成二進制后只有一個位元位為1且為1的二進制位是不同的,所以傳多個選項的時候實際上是對傳入的數進行按位或處理,最后傳給flags的是一個數,這個數有一個或多個位元位為1,
以下面的代碼為例:


2.2.檔案描述符fd(file descriptor)
可以看到打開成功后各自的回傳值是3 4 5,,,
打開失敗回傳值則為-1
這些回傳值叫做檔案描述符,在系統層面是一個整數,從0開始,
其中:0為標準輸入,1為標準輸出,2為標準錯誤,這三個默認被打開
檔案描述符的本質是陣列下標,作業系統為每一個行程維護了一個檔案描述符表,該表的索引值都從從0開始的,索引值都有一個指標指向對應的檔案:


一個行程如何通過檔案描述符找到檔案:當行程執行write(4,"hello",5)時,行程先找到自己的PCB,PCB中包含檔案描述符表(struct files_struct),然后通過索引下標4找到對應的檔案,
所以如果將檔案描述符為0(標準輸入)的關掉,則分配的下標為0:


可以看出檔案描述符的分配規則為:在files_struct陣列當中,找到當前沒有被使用的最小的一個下標,作為新的檔案描述符,
另外,要記得關閉檔案描述符,否則會造成檔案描述符泄漏,因為檔案描述符表(陣列)是有上限的,
2.3.補充內容–函式指標訪問硬體
不同的硬體訪問方式是不一樣的,對于外設訪問的方式一般是讀和寫,但是實作代碼是不一樣的,此時行程就可以通過函式指標的方式來指向它們各自的實作方法:


2.4.重定向的實作原理
重定向有兩個運算子分別是>(格式化重定向)和>>(追加重定向),其實作原理就是作業系統內核把標準輸出(1)關掉,然后打開對應的檔案,此時該檔案的下標就是1,應用層默認輸入到下標為1的檔案中,所以就輸入到該檔案中了,至于追加重定向則是在打開的時候加上選項O_APPEND,



三、FILE
檔案指標中那個FILE本質是一個結構體,這個結構體中一定是包含檔案描述符(fd)的,因為訪問檔案都是通過fd訪問的,
對于下面的代碼:
#include<stdio.h>
#include<string.h>
#include<sys/stat.h>
#include<sys/types.h>
#include<fcntl.h>
#include<unistd.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;
}

如果對結果重定向,printf和 fwrite(庫函式)都輸出了2次,而write只輸出了一次(系統呼叫),
這是因為重定向影響了緩沖方式:
顯示幕采用的是行緩沖(遇到\n就重繪)
檔案采用的則是全緩沖(緩沖區資料寫滿才重繪)
沒有重定向之前是往顯示幕寫,而重定向則是往檔案中寫,
- 一般C庫函式寫入檔案時是全緩沖的,而寫入顯示幕是行緩沖,
- printf、fwrite庫函式會自帶緩沖區,當發生重定向到普通檔案時,資料的緩沖方式由行緩沖變成了全緩沖,
- 全緩沖行程退出之后,會統一重繪,寫入檔案當中,
- fork的時候,父子資料會發生寫時拷貝,資料被暫存在緩沖區中,因此子行程也就有了同樣的一份資料,即產生兩份資料,
- 由于父子行程的緩沖區中有兩份一樣的資料,所以會重繪兩份,
- write 沒有重繪兩份,說明沒有所謂的緩沖區,
我們這里所說的緩沖區,都是用戶級緩沖區,printf和fwrite是庫函式, write是系統呼叫,庫函式在系統呼叫的“上層”, 是對系統呼叫的“封裝”,但是write沒有緩沖區,而 printf和fwrite有,足以說明,該緩沖區是二次加上的,由C標準庫提供,
另外fflush函式也在C標準庫中,將用戶級的緩沖區往系統中重繪:

下面是FILE的代碼:
typedef struct _IO_FILE FILE; 在/usr/include/stdio.h
struct _IO_FILE {
int _flags; /* High-order word is _IO_MAGIC; rest is flags. */
#define _IO_file_flags _flags
//緩沖區相關
/* The following pointers correspond to the C++ streambuf protocol. */
/* Note: Tk uses the _IO_read_ptr and _IO_read_end fields directly. */
char* _IO_read_ptr; /* Current read pointer */
char* _IO_read_end; /* End of get area. */
char* _IO_read_base; /* Start of putback+get area. */
char* _IO_write_base; /* Start of put area. */
char* _IO_write_ptr; /* Current put pointer. */
char* _IO_write_end; /* End of put area. */
char* _IO_buf_base; /* Start of reserve area. */
char* _IO_buf_end; /* End of reserve area. */
/* The following fields are used to support backing up and undo. */
char *_IO_save_base; /* Pointer to start of non-current get area. */
char *_IO_backup_base; /* Pointer to first valid character of backup area */
char *_IO_save_end; /* Pointer to end of non-current get area. */
struct _IO_marker *_markers;
struct _IO_FILE *_chain;
int _fileno; //封裝的檔案描述符
#if 0
int _blksize;
#else
int _flags2;
#endif
_IO_off_t _old_offset; /* This used to be _offset but it's too small. */
#define __HAVE_COLUMN /* temporary */
/* 1+column number of pbase(); 0 is unknown. */
unsigned short _cur_column;
signed char _vtable_offset;
char _shortbuf[1];
/* char* _save_gptr; char* _save_egptr; */
_IO_lock_t *_lock;
#ifdef _IO_USE_OLD_IO_FILE
};
四、dup 重定向
上面的重定向是將標準輸出的fd關掉,然后打開重定向的檔案,
而使用dup2函式就不需要關掉標準輸出:

dup函式的作用是,回傳一個新的檔案描述符(可用檔案描述符的最小值)newfd,并且新的檔案描述符newfd指向oldfd所指向的檔案表項,
比如:


檔案描述符為1的檔案通過dup重定向到檔案描述符1上,那么也就相當于檔案描述符3對應的檔案也是顯示幕檔案,那么向檔案描述符3進行write,最終結果也會列印在顯示幕上,
4.1.使用dup2 完成重定向
dup2有兩個引數oldfd和newfd,newfd是oldfd的拷貝,將newfd重定向到oldfd,當整個函式呼叫成功后,會將newfd關掉,然后讓newfd指向oldfd,總之,dup2函式的作用就是讓newfd重定向到oldfd所指的檔案表項上,如果出錯就回傳-1,否則回傳的就是newfd,所以如果想讓原本輸出到顯示幕上的資料重定向到檔案中,可以這樣寫:


4.2.重定向恢復
在進行重定向后,如果想要恢復到重定向之前的狀態,可以在重定向之前用dup函式保留該檔案描述符對應的檔案表項,然后在需要恢復重定向的時候使用dup2重定向到原來的檔案表項,以重定向后恢復標準輸出為例,如下所示:


4.3.在my_shell中添加重定向功能
其實作原理是在子行程進行程式替換之前將標準輸出(1)重定向到打開的檔案中,
#include<stdio.h>
#include<string.h>
#include<unistd.h>
#include<stdlib.h>
#include<ctype.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#define SIZE 256
#define NUM 16 //命令列引數的個數
void redirect(char* cmd)
{
int fd=-1;
int redirect_count=0;//記錄>的個數
char*file=NULL;
char*ptr=cmd;
while(*ptr)
{
if(*ptr=='>')
{
*ptr++='\0';
redirect_count++;
if(*ptr=='>')
{
*ptr++='\0';
redirect_count++;
}
while(*ptr!='\0'&&isspace(*ptr))//跳過空格
{
ptr++;
}
file=ptr;//找到檔案名
while(*ptr!='\0'&&!isspace(*ptr))//清空檔案名后面的空格
{
ptr++;
}
*ptr='\0';
if(redirect_count==1){
//>
fd=open(file,O_CREAT|O_TRUNC|O_WRONLY,0644);
}
else if(redirect_count==2){
//>>
fd=open(file,O_CREAT|O_APPEND|O_WRONLY,0644);
}
else{
//do nothing!
}
//檔案已經打開,用dup2重定向
dup2(fd,1);
close(fd);
}//end if
else if(*ptr=='<')
{
//和重定向>類似
}
ptr++;
}
}
int main()
{
char cmd[SIZE];
const char* cmd_line="[my_shell@VM-0-16-centos ~]# ";
while(1)
{
cmd[0]=0;
printf("%s",cmd_line);
fgets(cmd,SIZE,stdin);
cmd[strlen(cmd)-1]='\0';
pid_t id=fork();
if(id<0)
{
perror("fork error!\n");
continue;
}
if(id==0)//子行程
{
redirect(cmd);
char*args[NUM];//將命令字串分割
args[0]=strtok(cmd," ");
int i=1;
do
{
args[i]=strtok(NULL," ");
if(args[i]==NULL)
{
break;
}
i++;
}while(1);
execvp(args[0],args);
exit(1);
}
int status=0;
pid_t ret=waitpid(id,&status,0);
if(ret>0)
{
printf("status code:%d\n",(status>>8)&0xFF);
}
}
return 0;
}

總結
- 作業系統提供系統呼叫介面(open,close,read,write),C語言對系統呼叫介面進行封裝
- FILE*是個結構體指標,FILE是個結構體,有兩個比較重要的方面:檔案描述符和緩沖區(C語言提供)
- fd是系統呼叫,fd是檔案描述符表(陣列)的下標,里面的內容是檔案指標指向檔案,系統默認打開0 1 2(標準輸入,標準輸出,標準錯誤)
- 重定向的底層是將fd的指向改變
轉載請註明出處,本文鏈接:https://www.uj5u.com/qita/321239.html
標籤:其他
上一篇:資料結構初階:演算法復雜度
