一、C庫IO函數工作流程示意圖:
FILE 類型的指針,是特殊結構體類型,包含文件描述符、讀寫指針位置、內存地址等信息,用於文件讀寫操作。
I/O緩衝區用於利用內存減少硬盤操作。在右側三種情況下刷新緩衝區,存到硬盤上。
二、進程控制塊PCB和文件描述符
文件描述符是int類型的。而且每個PCB的文件描述符表中的前三個都是固定的:
標準輸入 fd爲0
標準輸出 fd爲1
標準錯誤 fd爲2
實際上,文件描述表是是個結構體指針數組:
三、虛擬地址空間
程序啓動後,在磁盤上分配4G空間供進程使用,最多4G,用多少分多少。
0-3G在用戶區,程序員可操作;3-4G爲內核區,程序員不可操作。受保護的地址(0-4K)也不許用戶訪問,如NULL在此區域。程序從main函數開始執行,即從代碼段執行,然後根據代碼中變量類型等將元素分配到各個空間中。
四、庫函數與系統函數的關係
由此可見,由於標準C庫函數內部有一個緩衝區,所以可以等緩衝區滿了以後再調用系統IO函數,所以效率提高了。
五、Linux系統IO函數
1、open函數
man 2 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);
參數:
flags設置:
O_RDONLY 以只讀方式打開文件
O_WRONLY 以只寫的方式打開文件
O_RDWR 以讀寫的方式打開
O_CREAT 如果文件不存在則創建文件
O_EXCL 如果文件存在,則強制 open() 操作失敗
O_TRUNC 如果文件存在,將文件清零
O_APPEND 把文件添加內容的指針設到文件的結束處
mode 設置:
文件權限 = 給定對的文件權限 & 本地掩碼(取反)
例如:
設定權限 0777
如果umask
出來的本地掩碼是 0002
777 ----------------------------二進制 111 111 111
002 ----------------------------二進制 00 000 010 取反後得 111 111 101
111 111 111&111 111 101
實際權限 111 111 101
即實際權限爲 0775
返回值:
若成功返回文件描述符(fd);若出錯,返回-1
2、read函數
函數原型:
#include <uinstd.h>
ssize_t read(int fd, void *buf, size_t nbytes);
(讀常規文件是不會阻塞的,不管讀多少字節,read一定會在有限的時間內返回。從終端設備或網絡讀則不一定,如果從終端輸入的數據沒有換行符,調用read讀終端設備就會阻塞,如果網絡上沒有接收到數據包,調用read從網絡讀就會阻塞,至於會阻塞多長時間也是不確定的,如果一直沒有數據到達就一直阻塞在那裏。同樣,寫常規文件是不會阻塞的,而向終端設備或網絡寫則不一定。)
返回值:
-1
:error會被置爲相應的值。
error:爲EAGAIN,表示在非阻塞下,此時無數據到達,立即返回。
error:爲EINTR,表示被信號中斷了。
0
:對端已關閉,本端也需要close 該套接字。
>0
:實際讀取的數據長度。
3、write函數
函數原型:
#include <unistd.h>
ssize_t write(int fd, const void *buf, size_t count);
返回值:
若成功,返回已寫的字節數;若出錯 返回 -1 ;
open,read,write 函數的運用:從一個文件彙總讀取內容後,寫入另一個文件中,自己動手寫了個例子如下:
先寫一個read_write.c
4、lseek函數
函數原型:
#include <sys/types.h>
#include <unistd.h>
off_t lseek(int fd, off_t offset, int whence);
作用: 設置文件偏移量。
若文件的偏移量大於當前文件的長度,在這種情況下,對該文件的下一次寫將加長該文件,並在文件中構成一個空洞。位於文件中沒有寫過的字節都被讀爲0.
文件中的空洞並不要求在磁盤上佔用存儲區。
參數:
whence的取值:
SEEK_SET 文件的偏移位置設置爲距開始位置 offset 個字節
SEEK_CUR 文件的偏移位置設置爲當前值 + offset ,offset 的值可正可負
SEEK_END 文件的偏移位置設置爲文件長度 +offset ,offset 的值只能W爲正的,只能向後拓展,不能向前拓展
返回值:
若成功返回從文件頭部開始的偏移量,以字節爲單位(即文件大小);若出錯,返回-1
#include<unistd.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<stdlib.h>
#include<stdio.h>
int main()
{
//打開一個已有的文件
int fd = open("./bb.txt",O_RDWR);
if( fd == -1)
{
perror("open bb.txt:");
exit(1);
}
int ret = lseek(fd,0,SEEK_END);
printf("file length = %d\n",ret);
// 文件擴展
ret = lseek(fd,2000,SEEK_END);
printf("return value = %d\n",ret);
// 實現文件擴展,需要最後一次寫操作
write(fd,"a",1);
close(fd);
return 0;
}
5.獲取文件屬性—stat、lstat、fstat
函數原型
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
int stat(const char *path, struct stat *buf); //struct stat *buf 是函數外創建的,然後扔到函數內去賦值
int fstat(int fd, struct stat *buf);
int lstat(const char *path, struct stat *buf);
參數
1.path :文件名或者目錄名
2.fd : 文件描述符
3.
struct stat {
dev_t st_dev; //文件的設備編號
ino_t st_ino; //節點
mode_t st_mode; //文件的類型和存取的權限
nlink_t st_nlink; //連到該文件的硬連接數目,剛建立的文件值爲1
uid_t st_uid; //用戶ID
gid_t st_gid; //組ID
dev_t st_rdev; //(設備類型)若此文件爲設備文件,則爲其設備編號
off_t st_size; //文件字節數(文件大小)
blksize_t st_blksize; //塊大小(文件系統的I/O 緩衝區大小)
blkcnt_t st_blocks; //塊數
time_t st_atime; //最後一次訪問時間
time_t st_mtime; //最後一次修改時間
time_t st_ctime; //最後一次改變時間(指屬性)
};
st_mode:下圖中的是8進制下的數據
返回值:
若成功獲取文件屬性,返回0;若失敗,返回 -1;
例子:
使用 stat() 函數實現一個簡單的 ls -l 命令:
vi ls-l.c
#include<unistd.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<pwd.h> // 所有者信息
#include<grp.h> // 所屬組信息
#include<time.h>
#include<string.h>
#include<stdlib.h>
#include<stdio.h>
int main(int argc,char *argv[])
{
if( argc<2 )
{
perror("參數不夠");
exit(1);
}
struct stat st; //創建一個stat結構體的對象,供系統函數stat使用
int i;
for( i = 1; i<argc; i++)
{
int ret = stat(argv[i],&st); // 獲取文件或者目錄的所有信息存儲於 st 結構體中
if( ret == -1 )
{
perror("stat");
exit(1);
}
// 存儲文件類型和訪問權限
char perms[11] = {0};
// 判斷文件類型
switch( st.st_mode & S_IFMT )
{
case S_IFSOCK: // 套接字文件
perms[0] = 's';
break;
case S_IFLNK: // 軟連接文件
perms[0] = 'l';
break;
case S_IFREG: // 普通文件
perms[0] = '-';
break;
case S_IFBLK: // 塊設備文件
perms[0] = 'b';
break;
case S_IFDIR: // 目錄文件
perms[0] = 'd';
break;
case S_IFCHR: // 字符設備文件
perms[0] = 'c';
break;
case S_IFIFO: // 管道文件
perms[0] = 'p';
break;
default:
break;
}
// 判斷文件的訪問權限
// 文件的所有者
perms[1] = (st.st_mode & S_IRUSR) ? 'r':'-';
perms[2] = (st.st_mode & S_IWUSR) ? 'w':'-';
perms[3] = (st.st_mode & S_IXUSR) ? 'x':'-';
// 文件的所屬組
perms[4] = (st.st_mode & S_IRGRP) ? 'r':'-';
perms[5] = (st.st_mode & S_IWGRP) ? 'w':'-';
perms[6] = (st.st_mode & S_IXGRP) ? 'x':'-';
// 文件的其他用戶
perms[7] = (st.st_mode & S_IROTH) ? 'r':'-';
perms[8] = (st.st_mode & S_IWOTH) ? 'w':'-';
perms[9] = (st.st_mode & S_IXOTH) ? 'x':'-';
// 硬鏈接計數
int nums = st.st_nlink;
// 文件所有者
char *fileuser = getpwuid(st.st_uid)->pw_name;
// 文件所屬組
char *filegroup = getgrgid(st.st_gid)->gr_name;
// 文件大小
int size = (int)st.st_size;
// 文件修改時間
char *time = ctime(&st.st_mtime);
char mtime[512]="";
strncpy(mtime,time,strlen(time)-1);
// 保存輸出信息格式
char buf[1024]={0};
// 把對應信息按格式輸出到 buf 中
sprintf(buf,"%s %d %s %s %d %s %s",perms,nums,fileuser,filegroup,size,mtime,argv[i]);
// 打印 buf
printf("%s\n",buf);
}
return 0;
}
stat、lstat、fstat之間的區別:
1.fstat 函數:形參是”文件描述符”,而另外兩個形參是“文件路徑”。文件描述符是我們用 open 系統調用後得到的,而文件路徑直接寫就可以了。
2.stat 函數與 lstat 函數的區別: 當一個文件是軟鏈接時,lstat 函數返回的是該軟鏈接本身的信息 (不穿透);而 stat 函數返回的是該軟鏈接指向文件的信息 (穿透)。
stat與 lstat 對比的例子:
vi ls-l.c
gcc ./ls-l.c -o ls-l
ln -s main.c main.soft
接下來我們要使用剛生成的可執行文件來觀察main.c和其軟鏈接main.soft的大小。
ls -l
./ls-l main.c main.soft
驗證了確實stat具有穿透性,stat 函數返回的是該軟鏈接指向文件的信息。
接下來我們把ls-l.c中的stat改成lstat,重新編譯後重復執行上面步驟後再來觀察:
驗證了lstat不具有穿透性。
6.unlink
例:使用unlink的特性創建臨時文件。
vi unlink.c
gcc unlink.c -o unlink
./unlink
7.其他常見的文件操作的系統IO
接下來看目錄操作的系統IO
8.chdir
man 2 chdir
1.作用:修改當前進程的路徑
2.函數原型:
#include <unistd.h>
int chdir(const char *path);
9.getcwd
1.作用:獲取當前進程的工作目錄
2.函數原型:
#include <unistd.h>
char *getcwd(char *buf, size_t size);
char *getwd(char *buf);
例子:chdir 函數和 getcwd 函數的運用:
#include<unistd.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<stdlib.h>
#include<stdio.h>
int main(int argc, char *argv[] )
{
if( argc<2 )
{
perror("參數不夠");
exit(1);
}
printf(" agrv[1] = %s\n",argv[1]);
// 修改當前的路徑
int ret =chdir(argv[1]);
if( ret == -1 )
{
perror("chdir");
exit(1);
}
// 在這裏通過在改變後的目錄下創建一個新的文件,來證明目錄已經改變
int fd = open("chdir.txt",O_CREAT|O_RDWR,0644);
if( fd == -1 )
{
perror("open");
exit(1);
}
close(fd);
// 獲取改變目錄後的目錄名
char buf[100]={0};
getcwd(buf,sizeof(buf));
printf("current dir: %s\n",buf);
return 0;
}
10.rmdir
1.作用:刪除一個目錄
2.函數原型:
#include <unistd.h>
int rmdir(const char *pathname);
11.mkdir
1.作用:創建一個目錄
2.函數原型:
#include <sys/stat.h>
#include <sys/types.h>
int mkdir(const char *pathname, mode_t mode);
mode就是權限
12.opendir
1.作用:打開一個目錄
2.函數原型
#include <sys/types.h>
#include <dirent.h>
DIR *opendir(const char *name);
DIR *fdopendir(int fd);
13.readdir
1.作用:讀目錄
2.函數原型:
#include <dirent.h>
struct dirent *readdir(DIR *dirp);
3.返回值:返回一個記錄項(即一個結構體對象)
14.closedir
1.作用:關閉一個目錄
2.函數原型:
#include <sys/types.h>
#include <dirent.h>
int closedir(DIR *dirp);
3.返回值:
若函數執行成功,返回0;若失敗,返回 -1.
例子:遞歸讀目錄獲取普通文件的個數
vi file_count.c
#include<unistd.h>
#include<dirent.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<stdlib.h>
#include<stdio.h>
#include<string.h>
// 獲取 root 目錄下的文件個數
int get_file_count(char *root)
{
// open dir
DIR * dir = NULL;
dir = opendir(root);
if( NULL == dir )
{
perror("opendir");
exit(1);
}
// 遍歷當前打開的目錄
struct dirent* ptr = NULL;
char path[1024]={0};
int total = 0;
while( (ptr = readdir(dir) )!= NULL)
{
// 過濾掉 . 和 ..
if( strcmp(ptr->d_name,".") == 0 || strcmp(ptr->d_name,"..") == 0 )
{
continue;
}
// 如果是目錄,遞歸讀目錄
if(ptr->d_type == DT_DIR)
{
sprintf(path,"%s/%s",root,ptr->d_name);
total += get_file_count(path);
}
// 如果是普通文件
if( ptr->d_type == DT_REG )
{
total++;
}
}
// 關閉目錄
closedir(dir);
return total;
}
int main(int argc,char *argv[])
{
if( argc<2 )
{
perror("參數不夠");
exit(1);
}
// 獲取指定目錄下普通文件的個數
int count = get_file_count(argv[1]);
printf("%s has file numbers : %d\n",argv[1],count);
return 0;
}
15.dup和dup2(重定向文件描述符)
1.作用:複製現有的文件描述符
2.函數原型:
#include <unistd.h>
int dup(int oldfd);
int dup2(int oldfd, int newfd);
3.返回值:
(1).dup返回的是文件描述符中沒有被佔用的,然後就可以做到dup返回的文件描述符和oldfd都指向同一個文件。
(2).dup2 分兩種情況討論下:
(a).如果oldfd和newfd不相同,那麼在拷貝前會先關掉newfd對應的文件,然後newfd被重定向,這樣oldfd和newfd就都指向同一個文件了。
(b).如果oldfd和newfd是同一個文件描述符,不會關掉newfd , 直接返回oldfd,,這樣顯然oldfd和newfd也是指向同一個文件。
dup的例子如下:
vi dup.c
vi a.txt
先在a.txt裏隨便寫一句話:
#include<unistd.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
int main(void)
{
int fd =open("a.txt",O_RDWR);//打開一個已有的文件
if( fd == -1 )
{
perror("open");
exit(1);
}
printf("file open fd = %d\n",fd);
// 找到進程文件描述符表中第一個可用的文件描述符
// 將參數指定的文件複製到該描述後,返回這個描述符
int ret = dup(fd);
if( fd == -1 )
{
perror("dup");
exit(1);
}
printf(" dup fd = %d\n",ret);
char *buf = "你是猴子請來的救兵嗎??\n";
char *buf1 = "你大爺的,我是程序猿!!!\n";
lseek(fd,0,SEEK_END);//注意,如果不想原有內容被覆蓋,就要移動文件指針到末尾
write(fd,buf,strlen(buf));
write(ret,buf1,strlen(buf1));
close(fd);
return 0;
}
gcc ./dup.c -o ./dup
./dup
vi a.txt
dup2的例子如下:
#include<unistd.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
int main(void)
{
int fd =open("english.txt",O_RDWR);
if( fd == -1 )
{
perror("open");
exit(1);
}
int fd1 =open("a.txt",O_RDWR);
if( fd1 == -1 )
{
perror("open");
exit(1);
}
printf("fd = %d\n",fd);
printf("fd1 = %d\n",fd1);
int ret = dup2(fd1, fd);//關閉fd也就是english.txt,然後fd被重定向,即fd也指向了a.txt
if( ret == -1 )
{
perror("dup2");
exit(1);
}
printf(" current fd = %d\n",ret);
char *buf = "主要看氣質\n";
lseek(fd,0,SEEK_END);//注意,如果不想原有內容被覆蓋,就要移動文件指針到末尾
write(fd,buf,strlen(buf));
write(fd1,"hello world!",12);
//以上兩句都是對a.txt進行操作
close(fd);
close(fd1);
return 0;
}
16.fnctl(網絡編程會用到)
fnctl的例子如下:
#include<unistd.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
int main(void)
{
int flag;
int fd;
// 測試字符串
char *p = "我們是一個由中國特色社會主義的國家";
char *q ="社會主義好哇";
// 以只寫方式打開文件
fd = open("test.txt",O_WRONLY);
if( fd == -1 )
{
perror("open");
exit(1);
}
// 輸入新的內容,該內容會覆蓋原來的內容
if( write(fd,p,strlen(p)) == -1 )
{
perror("write");
exit(1);
}
//使用 F_GETFL 命令得到文件狀態標誌
flag = fcntl(fd,F_GETFL,0); //第二個參數爲F_GETFL時,第三個參數固定是0
if( flag == -1 )
{
perror("fcntl");
exit(1);
}
// 將文件狀態標誌添加 “追加寫” 選項
flag |= O_APPEND;
// 將文件狀態修改爲追加寫(注意,修改後文件指針會自動移到尾部!!!)
if( fcntl(fd,F_SETFL,flag) == -1 )
{
perror("fcntl");
exit(1);
}
// 再次輸入新的內容,該內容會追加到最後
if( write(fd,q,strlen(q)) == -1 )
{
perror("write again");
exit(1);
}
return 0;
}