C檔案介面
寫檔案
#include<stdio.h>
#include<string.h>
int main(){
FILE *fp=fopen("myfile.txt","w");
if(!fp){
printf("open error!\n");
return 1;
}
const char *msg="Hello World!\n";
//這里不用 +1,具體查看man手冊對該函式得說明
fwrite(msg,strlen(msg),1,fp);
fclose(fp);
return 0;
}
讀檔案
#include<stdio.h>
#include<string.h>
int main(){
FILE * fp=fopen("myfile.txt","r");
if(!fp){
printf("open error!\n");
}
char buf[1024];
ssize_t s=fread(buf,sizeof(char),sizeof(buf),fp);
if(s>0){
buf[s]=0;
printf("%s\n",buf);
}
fclose(fp);
return 0;
}
輸出資訊到顯示幕
#include<stdio.h>
#include<string.h>
int main(){
const char* msg="hello fwrite!!\n";
//系統呼叫介面
fwrite(msg,sizeof(char),strlen(msg),stdout);
//直接向螢屏列印內容
printf("hello printf!!\n");
//C提供的庫函式,向顯示幕中列印
fprintf(stdout,"hello fprintf!!\n");
return 0;
}
stdin & stdout & stderr
C語言會默認打開三個輸入輸出流:
1.stdin(標準輸出),2.stdout(標準輸入),3.stderr(標準錯誤),
這三個輸入輸出流都是FILE * 型別,檔案指標型別,
打開檔案方式
r Open text file for reading. The stream is positioned at the beginning of the
file.
r+ Open for reading and writing. The stream is positioned at the beginning of
the file.
w Truncate file to zero length or create text file for writing. The stream is
positioned at the beginning of the file.
w+ Open for reading and writing. The file is created if it does not exist, oth‐
erwise it is truncated. The stream is positioned at the beginning of the
file.
a Open for appending (writing at end of file). The file is created if it does
not exist. The stream is positioned at the end of the file.
a+ Open for reading and appending (writing at end of file). The file is created
if it does not exist. The initial file position for reading is at the begin‐
ning of the file, but output is always appended to the end of the file.
系統檔案I/O
介面(open)介紹
#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: 打開檔案時,可以傳入多個引數選項,用下面的一個或者多個常量進行“或”運算,構成flags,
引數:
O_RDONLY: 只讀打開
O_WRONLY: 只寫打開
O_RDWR : 讀,寫打開
這三個常量,必須指定一個且只能指定一個
O_CREAT : 若檔案不存在,則創建它,需要使用mode選項,來指明新檔案的訪問權限
O_APPEND: 追加寫
回傳值:
成功:新打開的檔案描述符
失敗:-1
write read close lseek ,類比C檔案相關介面
系統呼叫和庫函式
上面的fopen、fclose、fread、fwrite都是C標準庫當中的函式,稱為
庫函式(libc),
open、close、read、write、lseek都屬于系統提供的介面,稱為系統呼叫,
看作業系統層次結構圖,我們就對系統呼叫介面和庫函式清楚明了,我們可以認為C標準庫當中的函式(f#系列)都是對系統呼叫介面的封裝,方便二次開發,

open回傳值
檔案描述符 fd
通過對open函式的學習,檔案描述符就是一個小整數,
0 & 1 & 2
Linux行程默認情況下會有3個預設打開的檔案描述符:標準輸出0,標準輸入1,標準錯誤2,
0,1,2對應的硬體設備一般是:鍵盤,顯示幕,顯示幕,

檔案描述符就是一個從0開始的小整數,當我們打開檔案時,OS在記憶體中要創建相應的資料結構來描述目標檔案,于是有了file結構體,表示一個已經打開的檔案物件,而行程執行open系統呼叫,所以必須讓行程和檔案關聯起來,每個行程都有一個指標*files,指向一張表files_struct,該表最重要的部分就是一個指標陣列,每個元素都有一個指向打開檔案的指標!所以,本質上,檔案描述符就是該陣列的下標,所以只要拿到檔案描述符,就可以找到對應的檔案,每個struct file中包含了打開檔案的相關內容和一些列的函式指標,這樣只需要讀寫相對應的函式指標就能夠對每個檔案進行讀寫操作,
檔案描述符的分配規則
檔案描述符的分配規則:在files_struct陣列當中,找到當前沒有被使用的最小的一個下標,作為新的檔案描述符,
重定向
#include<stdio.h>
#include<sys/stat.h>
#include<sys/wait.h>
#include<fcntl.h>
#include<string.h>
#include<unistd.h>
int main(){
close(1);
int fd=open("log.txt",O_CREAT | O_WRONLY ,0644);
if(fd<0){
printf("open error!!\n");
}
const char* str1="hello world!!: printf";
printf("%s\n",str1);
fflush(stdout);
close(fd);
return 0;
}
此時我們發現本來應該輸出到顯示幕的內容,輸出到了檔案log.txt當中,其中fd=1,這種現象叫做輸出重定向,常見的重定向有:>、>>、<
本質

我們知道對于標準輸出默認打開的是fd=1的檔案描述符,那么當我們關閉標準輸出時,當我們打開檔案log.txt,系統會為log.txt分配最小未使用的檔案描述符,就是我們剛剛關閉的檔案,所以給log.txt分配的檔案描述符就是1,所以這時當我們呼叫printf庫函式時,這時就會往檔案描述符為1的那個檔案里面寫內容,就是我們剛剛打開的那個檔案,這就是重定向,
本質
因為IO相關函式與系統呼叫介面對應,并且庫函式封裝系統呼叫,所以本質上,訪問檔案都是 通過fd來訪問的,
所以可以推斷,C庫函式當中的FILE結構體必定封裝了檔案描述符fd,
實驗代碼
#include<stdio.h>
#include<sys/stat.h>
#include<sys/wait.h>
#include<fcntl.h>
#include<string.h>
#include<unistd.h>
int main(){
close(1);
int fd=open("log.txt",O_CREAT | O_WRONLY ,0644);
if(fd<0){
printf("open error!!\n");
}
const char* msg="hello world!!\n";
const char* str1="hello world!!: printf";
const char* str2="hello world!!: fprintf";
write(fd,msg,strlen(msg));
printf("%s\n",str1);
fprintf(stdout,"%s\n",str2);
fork();
fflush(stdout);
close(fd);
return 0;
}
運行結果

我們發現 printf 和 fprintf (庫函式) 都列印了兩次,而write系統呼叫只列印了一次,這是為什么?這肯定和fork有關,
一般C庫函式寫
檔案時是全緩沖,而顯示幕是行緩沖,
printf和fprintf庫函式是自帶緩沖區的,當發生重定向時,由輸出資料到顯示幕變為輸出到檔案,所以緩沖方式由行緩沖變為了全緩沖,
而我們放在緩沖區的資料不會立即重繪,甚至fork之后,
但是當我們行程結束時,會統一重繪到檔案,
但是當fork的時候,父子行程會發生寫時拷貝,所以當父行程重繪資料的時候,子行程也有了相同的一份資料,子行程也會重繪,所以也就會產生兩份資料,
write沒有變化,所以沒有所謂的緩沖區,
綜上所述:printf和fprintf這些庫函式會自帶緩沖區,而write系統呼叫沒有帶緩沖區,我們這里所說的都是用戶級緩沖區,其實為了提升整體性能,OS也會提供相關內核級緩沖區,不過不再我們討論范圍之內,那這個緩沖區誰提供呢? printf fprintf 是庫函式, write 是系統呼叫,庫函式在系統呼叫的“上層”, 是對系統呼叫的“封裝”,但是 write 沒有緩沖區,而 printf fwrite 有,足以說明,該緩沖區是二次加上的,由C標準庫提供,
// 在/usr/include/libio.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; //封裝的檔案描述符,就是fd,對于stdin:_fileno=0,stdout:_fileno=1,stderr:_fileno=2
#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
};
使用dup2系統呼叫實作重定向
#include<unistd.h>
int dup2(int oldfd,int newfd);
//若成功則為非負的描述符,若出錯則為-1
dup2函式復制描述符表表項oldfd到描述符表表項newfd,覆寫描述符表表項newfd以前的內容,若newfd已經打開,dup2會在復制oldfd之前關閉newfd,
dup2(4,1)
呼叫dup2之前,fd=1(標準輸出)對應的檔案A,fd=4對應檔案B,A和B的參考計數都為1,呼叫之后,兩個檔案描述符都指向B;檔案A已經關閉,并且它的檔案表和v-node表表項已經洗掉,檔案B的計數參考已經增加,從此以后,任何寫到標準輸出的資料都被重定向到檔案B,

檔案系統
我們使用ls -l 的時候看到的除了檔案名,還看到了檔案元資料,
[root@VM-12-3-centos pengke]# ls -l
total 40
drwxrwxr-x 7 pengke pengke 4096 Mar 26 17:14 BitStudy
-rw-rw-r-- 1 pengke pengke 827 Mar 20 15:05 install.sh
drwxrwxr-x 4 pengke pengke 4096 Mar 30 12:20 Linux
-rwxrwxr-x 1 pengke pengke 21784 Mar 20 20:14 sort
drwxrwxr-x 3 pengke pengke 4096 Mar 26 17:12 test
ls -l 讀取存盤在磁盤上的檔案資訊,然后顯示出來,其實除了這種方式可以讀取出來還可以用命令stat命令讀取檔案的更多資訊,
[root@VM-12-3-centos Linux]# stat Mystring.h
File: ‘Mystring.h’
Size: 2493 Blocks: 8 IO Block: 4096 regular file
Device: fd01h/64769d Inode: 793845 Links: 1
Access: (0664/-rw-rw-r--) Uid: ( 1001/ pengke) Gid: ( 1001/ pengke)
Access: 2021-03-28 08:55:36.174026236 +0800
Modify: 2021-03-28 08:55:36.172026228 +0800
Change: 2021-03-28 08:55:36.172026228 +0800
Birth: -
inode
為了搞清楚inode我們先來看看檔案系統:

Linux ext2檔案系統,上圖是磁盤檔案系統圖(內核記憶體映像肯定有所不同),磁盤是典型的塊設備,硬碟磁區被劃分為一個個block,一個block的大小是由格式化的時候確定的,并且不可以更改的,例如mke2fs的-b選項可以設定block大小為1024、2048或4096位元組,而上圖中啟動塊(Boot Block)的大小是確定的,
Block Group:ext2檔案系統會根據磁區的大小劃分為數個Block Group,而每個Block Group都有著相同的結構組成,
超級塊(Super Block):存放檔案系統本身的結構資訊,記錄的資訊主要有:bolck 和 inode的總量,未使用的block和inode的數量,一個block和inode的大小,最近一次掛載的時間,最近一次寫入資料的時間,最近一次檢驗磁盤的時間等其他檔案系統的相關資訊,Super Block的資訊被破壞,可以說整個檔案系統結構就被破壞了,
GDT(Group Descriptor Table):塊組描述符,描述塊組屬性資訊,
塊位圖(Block Bitmap):Block Bitmap中記錄著Data Block中哪個資料塊已經被占用,哪個資料塊沒有被占用,
inode位圖(inode Bitmap):每個bit表示一個inode是否空閑可用,
節點表(inode Table):存放檔案屬性 如 檔案大小,所有者,最近修改時間等,
資料區(Data blocks):存放檔案資料,
當我們創建一個檔案是,屬性和資料是怎樣存盤的呢?
[root@localhost linux]# touch abc
[root@localhost linux]# ls -i abc
263466 abc
為了說明問題,我們將畫一個簡化圖:

由上圖可知:
1、目錄中的block中記錄的是目錄下以及子檔案和子目錄的檔案名和inode的對應,
2、檔案中的block中記錄的是檔案實際存盤的資料,
創建一個新檔案主要有以下四個操作:
1、存盤檔案:內核先找到一個空閑的inode節點(這里是263466),內核記錄檔案資訊,
2、存盤資料:該檔案需要存盤到三個磁盤塊,內核找到三個空閑塊:300、400、600,將內核快取區的資料依次復制到磁盤塊,
3、記錄分配情況:檔案內容按順序300,500,800存放,內核在inode上的磁盤分布區記錄了上述塊串列,
4、添加檔案名到目錄:新的檔案名abc,linux如何在當前的目錄中記錄這個檔案?內核將入口(263466,abc)添加到目錄檔案,檔案名和inode之間的對應關系將檔案名和檔案的內容及屬性連接起來,
軟硬鏈接
在Linux中快捷方式有兩種:
硬鏈接和軟鏈接,
硬鏈接
我們知道磁盤上的檔案資訊,是存盤在inode當中的,每個檔案的 inode 號都應該是不一樣的,inode 號就相當于檔案 ID,我們在查找檔案的時候,要先查找 inode 號,才能讀取到檔案的內容,創建硬鏈接就相當于多一個檔案名指向inode,硬鏈接不會建立自己的 inode 索引和 block(資料塊),而是直接指向源檔案的 inode 資訊和 block,所以硬鏈接和源檔案的 inode 號是一致的,
創建一個硬鏈接:
ln filename hardlinkName

硬鏈接特點
1、不論是修改源檔案(test 檔案),還是修改硬鏈接檔案(test-hard 檔案),另一個檔案中的資料都會發生改變,
2、不論是洗掉源檔案,還是洗掉硬鏈接檔案,只要還有一個檔案存在,這個檔案都可以被訪問,
3、硬鏈接不會建立新的 inode 資訊,也不會更改 inode 的總數,
4、硬鏈接不能跨檔案系統(磁區)建立,因為在不同的檔案系統中,inode 號是重新計算的,
5、硬鏈接不能鏈接目錄,因為如果給目錄建立硬鏈接,那么不僅目錄本身需要重新建立,目錄下所有的子檔案,包括子目錄中的所有子檔案都需要建立硬鏈接,這對當前的 Linux 來講過于復雜,
軟鏈接
軟鏈接會真正建立自己的 inode 索引和 block,所以軟鏈接和源檔案的 inode 號是不一致的,而且在軟鏈接的 block 中,寫的不是真正的資料,是源檔案的檔案名及 inode 號,
創建一個軟鏈接:
ln -s filename softlinkName

軟鏈接特點
1、不論是修改源檔案,還是修改軟鏈接檔案,另一個檔案中的資料都會發生改變,
2、 洗掉軟鏈接檔案,源檔案不受影響,而洗掉原檔案,軟鏈接檔案將找不到實際的資料,從而顯示檔案不存在,
3、軟鏈接會新建自己的 inode 資訊和 block,只是在 block 中不存盤實際檔案資料,而存盤的是源檔案的檔案名及 inode 號,
4、軟鏈接可以鏈接目錄、跨磁區,
動靜態庫
1、靜態庫(.a):程式在編譯鏈接的時候把庫的代碼鏈接到可執行檔案中,程式運行的時候將不再需\要靜態庫,可移植性強于動態庫,體積較動態庫大,
2、動態庫(.so):程式在運行的時候才去鏈接動態庫的代碼,多個程式共享使用庫的代碼,
3、一個與動態庫鏈接的可執行檔案僅僅包含它用到的函式入口地址的一個表,而不是外部函式所在目標檔案的整個機器碼,
4、在可執行檔案開始運行以前,外部函式的機器碼由作業系統從磁盤上的該動態庫中復制到記憶體中,這個程序稱為動態鏈接(dynamic linking),
5、動態庫可以在多個程式間共享,所以動態鏈接使得可執行檔案更小,節省了磁盤空間,作業系統采用虛擬記憶體機制允許物理記憶體中的一份動態庫被要用到該庫的所有行程共用,節省了記憶體和磁盤空間,
測驗代碼
//myadd.h
#pragma once
#include<stdio.h>
int Myadd(int x,int y);
//myadd.c
#include"myadd.h"
int Myadd(int x,int y)
{
return x+y;
}
//mysub.h
#pragma once
#include<stdio.h>
int Mysub(int x,int y);
//mysub.c
#include"mysub.h"
int Mysub(int x,int y)
{
return x-y;
}
//mytest.c
#include<stdio.h>
#include"myadd.h"
#include"mysub.h"
int main(){
int x=20,y=10;
printf("x + y = %d\n",Myadd(x,y));
printf("x - y = %d\n",Mysub(x,y));
printf("Hello World\n");
return 0;
}
生成靜態庫
#生成目標檔案
[root@localhost linux]# gcc -c myadd.c -o myadd.o
[root@localhost linux]# gcc -c mysub.c -o mysub.o
#生成靜態庫
[root@localhost linux]# ar -rc libmymath.a myadd.o mysub.o
# ar是gnu歸檔工具,rc表示(replace and create)
#查看靜態庫中的目錄串列
[root@localhost linux]# ar -tv libmymath.a
rw-r--r-- 0/0 1240 Sep 15 16:53 2017 myadd.o
rw-r--r-- 0/0 1240 Sep 15 16:53 2017 mysub.o
# t:列出靜態庫中的檔案
# v:verbose 詳細資訊
將上面的檔案按照這樣存放:

撰寫Makefile檔案:
path=$(shell pwd)
mytest:mytest.c
gcc $^ -o $@ -I$(path)/mylib/include -L$(path)/mylib/lib -lmymath -static
.PHONY:clean
clean:
rm -f mytest
# -I 頭檔案
# -L 指定庫路徑
# -l 指定庫名
#測驗目標檔案生成后,靜態庫刪掉,程式照樣可以運行
可以直接將庫檔案和頭檔案放到系統的頭檔案和庫檔案池(不建議會污染系統的頭檔案和庫檔案池):
# 目標檔案:myadd.h mysub.h ---> /usr/include/stdio.h
# 庫檔案:libmymath.a ---> /usr/lib64
生成動態庫
測驗代碼
//myadd.h
#pragma once
#include<stdio.h>
int Myadd(int x,int y);
//myadd.c
#include"myadd.h"
int Myadd(int x,int y)
{
return x+y;
}
//mysub.h
#pragma once
#include<stdio.h>
int Mysub(int x,int y);
//mysub.c
#include"mysub.h"
int Mysub(int x,int y)
{
return x-y;
}
生成動態庫:
-shared: 表示生成共享庫格式
-fPIC:產生位置無關碼(position independent code)
-庫名規則:libxxx.so
撰寫生成動態庫Makefile檔案:
libmymath.so:myadd.o mysub.o
gcc -shared $^ -o $@
# 生成的.o檔案要與位置無關
myadd.o:myadd.c
gcc -fPIC -c $<
mysub.o:mysub.c
gcc -fPIC -c $<
#清除命令
.PHONY:clean
clean:
rm *.o output libmymath.so
#發布命令
.PHONY:output
output:
mkdir -p mylib/include
mkdir -p mylib/lib
mv *.h mylib/include
mv *.so mylib/lib
將上面的檔案按照這樣存放:

撰寫測驗mytest.c檔案
#include<stdio.h>
#include<myadd.h>
#include<mysub.h>
int main(){
int x=20,y=10;
printf("x + y = %d\n",myadd(x,y));
printf("x - y = %d\n",mysub(x,y));
printf("Hello World\n");
return 0;
}
撰寫測驗Makefile檔案
path=$(shell pwd)
#-I:動態庫目錄,-L動態庫頭檔案目錄,-l:動態庫的庫名(去掉lib以及版本號和后最綴名:libmymath.so-->mymath)
mytest:mytest.c
gcc $^ -o $@ -I$(path)/mylib/include -L$(path)/mylib/lib -lmymath
.PHONY:clean
clean:
rm -f mytest
告訴作業系統運行時到哪里尋找動態庫:
方法一:
拷貝.so檔案到系統共享庫路徑下, 一般指/usr/lib
方法二:
匯入環境變數LD_LIBRARY_PATH(例如:export LD_LIBRARY_PATH=/home/pengke/Linux/lesson_15/mylibso/tmp/mylib/lib)
轉載請註明出處,本文鏈接:https://www.uj5u.com/qita/272596.html
標籤:其他
上一篇:2021年全網最細 VirtualBox 虛擬機安裝 Ubuntu 20.04.2.0 LTS及Ubuntu的相關配置
下一篇:AD9528資料
