1. 程式人生 > >Linux 下幾個檔案操作命令的程式碼實現

Linux 下幾個檔案操作命令的程式碼實現

用 C 語言實現命令 cp、df、mkdir、rm、tac

 

本文章中的示例程式碼是在 CentOS 5.4 64 位環境下執行通過的,在其它 unix 系統上沒有測試過。

Linux 作業系統中的命令實際上是編譯好的可執行程式,比如說 ls 這個命令,這個檔案位於 /bin 目錄下面,當我們用 file /bin/ls 命令檢視的時候會有以下輸出:

1

2

3

4

[[email protected] ~]# file /bin/ls

/bin/ls: ELF 64-bit LSB executable,

AMD x86-64, version 1 (SYSV), for GNU/Linux 2.6.9,

dynamically linked (uses shared libs), for GNU/Linux 2.6.9, stripped

這個命令通過呼叫 stat 系統呼叫和 /usr/share/file/magic.mgc 檔案來決定檔案的型別。如上的 /bin/ls 是一個 ELF 格式的動態連結的 64 位的可執行檔案。

系統呼叫是使用者程式和作業系統核心之間的介面,我們可以使用作業系統提供的系統呼叫來請求分配資源和服務。我們可以通過 man 2 章節來查詢 Linux 提供的系統呼叫的具體使用方法。有關檔案操作的常見系統呼叫命令有:open、creat、close、read、write、lseek、opendir、readdir、mkdir、stat 等等。

cp 命令的實現

cp 命令的模擬實現

大家也都知道 cp 這個命令主要的作用就是把一個檔案從一個位置複製到另一個位置。比如現在 /root 目錄下有一個 test.txt 檔案,如果我們用 cp test.txt test2.txt 命令的話,在同一個目錄下面就會生成一個同樣內容的 test2.txt 檔案了。

那麼 cp 命令是怎麼實現的呢,我們看如下程式碼:

清單 1. cp 命令實現程式碼

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

#include    <stdio.h>

#include    <unistd.h>

#include    <fcntl.h>

#include    <stdlib.h>

 

#define BUFFERSIZE    4096

#define COPYMODE     0644

 

void oops(char *, char *);

 

main(int argc, char * argv[])

{

   int   in_fd, out_fd, n_chars;

   char  buf[BUFFERSIZE];

 

   if ( argc != 3 ){

       fprintf( stderr, "usage: %s source destination\n", *argv);

       exit(1);

   }

 

   if ( (in_fd=open(argv[1], O_RDONLY)) == -1 ){

       oops("Cannot open ", argv[1]);

   }

   if ( (out_fd=creat( argv[2], COPYMODE)) == -1 ){

       oops( "Cannot creat", argv[2]);

   }

 

   while ( (n_chars = read(in_fd , buf, BUFFERSIZE)) > 0 ){

       if ( write( out_fd, buf, n_chars ) != n_chars ){

          oops("Write error to ", argv[2]);

       }

   }

   if ( n_chars == -1 ){

       oops("Read error from ", argv[1]);

   }

 

   if ( close(in_fd) == -1 || close(out_fd) == -1 )

       oops("Error closing files","");

}

 

void oops(char *s1, char *s2)

{

   fprintf(stderr,"Error: %s ", s1);

   perror(s2);

   exit(1);

}

該程式的主要實現思想是:開啟一個輸入檔案,建立一個輸出檔案,建立一個 BUFFERSIZE 大小的緩衝區;然後在判斷輸入檔案未完的迴圈中,每次讀入多少就向輸出檔案中寫入多少,直到輸入檔案結束。

cp 命令實現的說明

讓我來詳細的講述一下這個程式:

  • 開頭四行包含了 4 個頭檔案,<stdio.h> 檔案包含了 fprintf、perror 的函式原型定義;<unistd.h> 檔案包含了 read、write 的函式原型定義;<fcntl.h> 檔案包含了 open、creat 的函式原型定義、<stdlib.h> 檔案包含了 exit 的函式原型定義。這些函式原型有些是系統呼叫、有些是庫函式,通常都可以在 /usr/include 目錄中找到這些標頭檔案。
  • 接下來的 2 行以巨集定義的方式定義了 2 個常量。BUFFERSIZE 用來表示緩衝區的大小、COPYMODE 用來定義建立檔案的許可權。
  • 接下來的一行定義了一個函式原型 oops,該函式的具體定義在最後出現,用來輸出出錯資訊到 stderr,也就是標準錯誤輸出的檔案流。
  • 接下來主程式開始。首先定義了 2 個檔案描述符、一個存放讀出位元組數的變數 n_chars、和一個 BUFFERSIZE 大小的字元陣列用來作為拷貝檔案的緩衝區。
  • 接下來判斷輸入引數的個數是否為 3,也就是程式名 argv[0]、拷貝原始檔 argv[1]、目標檔案 argv[2]。不為 3 的話就輸出錯誤資訊到 stderr,然後退出程式。
  • 接下來的 2 行,用 open 系統呼叫以 O_RDONLY 只讀模式開啟拷貝原始檔,如果開啟失敗就輸出錯誤資訊並退出。如果想了解檔案開啟模式的詳細內容請使用命令 man 2 open,來檢視幫助文件。
  • 接下來的 2 行,用 creat 系統呼叫以 COPYMODE 的許可權建立一個檔案,如果建立失敗函式的返回值為 -1 的話,就輸出錯誤資訊並退出。
  • 接下來的迴圈是拷貝的主要過程。它從輸入檔案描述符 in_fd 中,讀入 BUFFERSIZE 位元組的資料,存放到 buf 字元陣列中。在正常讀入的情況下,read 函式返回實際讀入的位元組數,也就是說只要沒有異常情況和檔案沒有讀到結尾,那麼 n_chars 中存放的就是實際讀出的位元組的數字。然後 write 函式將從 buf 緩衝區中,讀出 n_chars 個字元,寫入 in_out 輸出檔案描述符。由於 write 系統呼叫返回的是實際寫入成功的位元組數。所以當讀出 N 個字元,又成功寫入 N 個字元到輸出檔案描述符中去的時候,就表示寫成功了,否則就報告寫入錯誤。
  • 最後就是用 close 系統呼叫關閉開啟的輸入和輸出檔案描述符。

rm 命令的實現

rm 命令的模擬實現

rm 命令主要是用來刪除一個檔案。

該命令的實現程式碼如下:

清單 2. rm 命令程式碼

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

#include <unistd.h>

#include <stdio.h>

#include <stdlib.h>

 

int main(int argc , char * argv[]) {

 

 int rt;

 if(argc != 2){

   exit(2);  

 }else{

 

   if((rt = unlink(argv[1])) !=  0){

       fprintf(stderr,"error.");

       exit(3);

   }

 

 }

   

 return 0;

 

}

其中程式的關鍵是 unlink 系統呼叫,unlink 函式原型包含在 <unistd.h> 標頭檔案裡面。

用 strace 來跟蹤命令

我們從這個程式的建立過程來分析這個程式。

這個命令的模擬程式是怎麼寫出來的呢?

首先,我們可以在機器上 touch test 建立一個 test 檔案,然後呼叫 strace rm test 命令來檢視 rm 命令具體使用了那些系統呼叫。

通過檢視,我們看到主要使用的系統呼叫如下:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

[[email protected] aa]# strace rm test

execve("/bin/rm", ["rm", "test"], [/* 24 vars */]) = 0

brk(0)                  = 0xcc66000

mmap(NULL, 4096, PROT_READ|PROT_WRITE,

      MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x2aff83ffb000

uname({sys="Linux", node="localhost.localdomain", ...}) = 0

...

...

lstat("test", {st_mode=S_IFREG|0644, st_size=0, ...}) = 0

stat("test", {st_mode=S_IFREG|0644, st_size=0, ...}) = 0

geteuid()                = 0

getegid()                = 0

getuid()                 = 0

getgid()                 = 0

access("test", W_OK)         = 0

unlink("test")             = 0

close(1)                = 0

exit_group(0)              = ?

我們可以看到起主要作用的就是 unlink(“test”) 這個系統呼叫。

讓我們來分析一下這些輸出的含義:

  • 首先第一行 execve 系統呼叫。該系統呼叫執行引數“/bin/rm”中的程式(以 #! 開頭的可執行指令碼也可以),後面第一個方括號中表示執行的引數,第二個方括號中表示執行的環境變數。
  • 接下來的 brk 和 mmap 命令,主要是用來給可執行命令分配記憶體空間。
  • 後面的 lstat 系統呼叫用來確定檔案的 mode 資訊,包括檔案的型別和許可權,檔案大小等等。
  • 然後 access 系統呼叫檢查當前使用者程序對於 test 檔案的寫入訪問許可權。這裡返回值為 0 也就是說程序對於 test 檔案有寫入的許可權。
  • 最後呼叫 unlink 系統呼叫刪除檔案。

這裡如果我們建立一個目錄 test1,然後用 rm test1 去刪除這個目錄會有什麼結果呢?

我們看到有如下輸出:

1

rm: cannot remove `test1': Is a directory

這時我們用 strace 命令來追蹤一下,發現輸出主要是如下不同。

1

unlink("test")              = -1 EISDIR (Is a directory)

這裡說明了刪除不掉的原因是 unlink 系統呼叫報錯,unlink 它認為 test 是一個目錄,不予處理。

那麼怎麼刪除一個目錄呢?應該是用 rmdir 系統呼叫,這樣就不會出現上述的問題了。

mkdir 命令的實現

mkdir 命令的模擬實現

再讓我們來看看 mkdir 的實現。

完整的程式碼如下:

清單 3. mkdir 實現程式碼

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

#include <sys/stat.h>

#include <sys/types.h>

#include <stdio.h>

 

 

int main(int argc, char *argv[]){

 

  int rt;

  if( (rt = mkdir (argv[1],10705)) == -1 ){

     fprintf(stderr,"cannot mkdir");

  }

 

  return 0;

 

}

這段程式碼也比較簡單,我這裡就不逐行解釋了,主要說以下幾點:

首先 mkdir 函式是定義於 <sys/stat.h> 和 <sys/types.h> 標頭檔案之中的。

而 fprintf 函式是位於 <stdio.h> 檔案之中的。

mkdir 的函式原型如下:

1

int mkdir(const char *pathname, mode_t mode);

mode 宣告為 mode_t 型別。

那麼 mode_t 資料型別是什麼資料型別,應該從哪個檔案去檢視它的定義呢?

mode_t 資料型別究竟是什麼型別

讓我們逐步查詢一下。

首先從檔案 /usr/include/sys/stat.h 中找到 mode_t 型別

/usr/include/sys/stat.h -> typedef __mode_t mode_t;

說明 mode_t 只是對 __mode_t 的一種定義。

然後從 /usr/include/bits/types.h 中找到 __mode_t 型別

/usr/include/bits/types.h -> __STD_TYPE __MODE_T_TYPE __mode_t;

說明 __mode_t 也只是對 __MODE_T_TYPE 的一種定義。

/usr/include/bits/typesizes.h -> #define __MODE_T_TYPE __U32_TYPE

說明 __MODE_T_TYPE 是對 __U32_TYPE 的一種定義。

/usr/include/bits/types.h -> #define __U32_TYPE unsigned int

最後 __U32_TYPE 是一種無符號的整數的定義。

從上述推導可以看出,mode_t 實際上也就是一種無符號整數。

另外如下結構 struct stat 定義中的 st_mode 成員變數也是使用的 mode_t 型別的變數。

從 man 2 stat 中可以找到結構 struct stat 的定義,如下:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

struct stat {

  dev_t   st_dev;   /* ID of device containing file */

  ino_t   st_ino;   /* inode number */

  mode_t  st_mode;  /* protection */

  nlink_t   st_nlink;   /* number of hard links */

  uid_t   st_uid;   /* user ID of owner */

  gid_t   st_gid;   /* group ID of owner */

  dev_t   st_rdev;  /* device ID (if special file) */

  off_t   st_size;  /* total size, in bytes */

  blksize_t st_blksize; /* blocksize for filesystem I/O */

  blkcnt_t  st_blocks;  /* number of blocks allocated */

  time_t  st_atime;   /* time of last access */

  time_t  st_mtime;   /* time of last modification */

  time_t  st_ctime;   /* time of last status change */

    };

該結構也是我們在後面的 tac 命令實現中需要用到的結構體。我們需要用到結構體中的 st_size 成員,該成員反映了被讀取的檔案描述符對應的檔案的大小。

tac 命令的實現

tac 命令的模擬實現

tac 命令主要用來以倒序的方式顯示一個文字檔案的內容,也就是先顯示最後一行的內容,最後顯示第一行的內容。程式碼如下:

清單 4. tac 命令實現程式碼

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

65

#include <stdio.h>

#include <sys/types.h>

#include <sys/stat.h>

#include <fcntl.h>

#include <string.h>

#include <unistd.h>

#include <stdlib.h>

#include <errno.h>

 

#define SIZE  1000001

#define NLINE '\n'

 

 

int main(int argc , char *argv[]){

 

 char buf[SIZE];

 char *p1,*p2,*p3,*p4;

 struct stat  *fp;

 int fd;

 fp=(struct stat *)malloc(sizeof(struct stat));

 

 if(argc != 2){

      fprintf(stderr,"input error %s \n");

      exit(1);

 }

 

 if( (fd=open(argv[1],O_RDONLY)) == -1 ){

      fprintf(stderr,"open error %s \n",strerror(errno));

      exit(1);

 }

  

 if(fstat(fd,fp)== -1){

      fprintf(stderr,"fstat error %s \n",strerror(errno));

      exit(2);

 }

  

 if(fp->st_size > (SIZE-1)){

      fprintf(stderr,"buffer size is not big enough \n");

      exit(3);

 }

 

 if(read(fd,buf,fp->st_size) == -1){

      fprintf(stderr,"read error.\n");

      exit(4);

 }

 

 p1=strchr(buf,NLINE);

 p2=strrchr(buf,NLINE);

 *p2='\0';

 

 do{

 p2=strrchr(buf,NLINE);

 p4=p2;

 p3=p2+sizeof(char);

 printf("%s\n",p3);

 *p4='\0';

 }while(p2 != p1);

  

 if(p2 == p1){

   *p2 = '\0';

   printf("%s\n",buf);

 }

 

 return 0;

}

讓我們來執行一下該程式:

程式的執行情況如下,假設編譯後的可執行檔名為 emulatetac,有一個文字檔案 test.txt。

1

2

3

4

5

6

7

8

9

10

11

12

13

# gcc emulatetac.c  -o  emulatetac

# cat test.txt

1

2

3

a

b

# ./emulatetac test.txt

b

a

3

2

1

可以看出檔案內容以倒序方式顯示輸出了。

tac 命令實現的說明

下面逐行講解:

  • #include 的標頭檔案,都應該通過 man 2 系統呼叫命令來查詢,這裡就不多說了。
  • 下面定義了一個巨集常量 SIZE,該常量主要用來表示能夠讀入最大多少個位元組的檔案,當檔案過大的時候程式就不執行,直接退出。然後定義了巨集常量 NLINE 表示換行符'\n'。
  • 接下來主程式體開始了:首先定義一個字元陣列 buf,用來把讀入檔案的每個位元組都存在該數組裡面。
  • 然後定義了 4 個字串指標,一個指向結構體 struct stat 的指標 fp,一個檔案描述符。
  • 然後為指向結構體的指標 fp 分配儲存空間。
  • 接下來判斷輸入引數是否為 2 個,也就是命令本身和檔名。不是 2 個就直接退出。
  • 然後以只讀方式開啟輸入檔名的檔案,也就是 test.txt。開啟成功的話,把開啟的檔案賦值到檔案描述符 fd 中,錯誤的話退出。
  • 然後用 fstat 系統呼叫把檔案描述符 fd 中對應檔案的元資訊,存放到結構體指標 fp 指向的結構中。
  • 下面判斷當檔案的大小超過緩衝區陣列 buf 的大小 SIZE-1 時,就退出。
  • 下面將把檔案 test.txt 中的每個字元存放到陣列 buf 中。
  • 下面是程式的核心部分:首先我們找到字串 buf 中的第一個換行字元存放到 p1 指標裡面,然後把最後一個換行字元置為字串結束符。
  • 接下來我們從後往前查詢字串 buf 中的換行符,直到遇到第一個換行符 p1。同時列印每個找到的換行符'\n'中的下一個字元開始的字串,也就剛好是一行文字。
  • 最後當從後向前找到第一個換行字元時,列印第一行,程式結束。

df 命令的實現

df 命令的模擬實現

通過 strace 命令檢視 df 主要使用瞭如下的系統呼叫:open、fstat、read、statfs

我這裡實際上是模擬實現的 df --block-size=4096 這個命令,也就是說以 4096 位元組為塊大小來顯示磁碟使用情況。

這裡最為關鍵的是 statfs 這個結構體,該結構體的某些欄位被用作 df 命令的輸出欄位:

1

2

3

4

5

6

7

8

9

10

11

struct statfs {

 long  f_type;   /* type of filesystem (see below) */

 long  f_bsize;  /* optimal transfer block size */

 long  f_blocks;   /* total data blocks in file system */

 long  f_bfree;  /* free blocks in fs */

 long  f_bavail;   /* free blocks avail to non-superuser */

 long  f_files;  /* total file nodes in file system */

 long  f_ffree;  /* free file nodes in fs */

 fsid_t  f_fsid;   /* file system id */

 long  f_namelen;  /* maximum length of filenames */

};

比如:df --block-size=4096 的輸出如下(縱向列出):

1

2

3

4

5

6

7

8

9

10

11

12

Filesystem 

/dev/sda1  

4K-blocks

5077005    f_blocks 欄位

Used

145105    f_blocks 欄位 -f_bfree 欄位

Available

4669841    f_bavail 欄位

Use%

4%   (f_blocks-f_bfree)/ f_blocks*100% 來計算磁碟使用率。

Mounted on

/

模擬實現的程式碼如下:

清單 5. 模擬實現程式碼

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

#include <stdio.h>

#include <errno.h>

#include <stdlib.h>

#include <string.h>

#include <sys/vfs.h>

#include <math.h>

#define SIZE1 100

#define FN "/etc/mtab"

#define SPACE ' '

 

int displayapartition(char * pt,char * pt1);

 

int main(void){

 

 char tmpline[SIZE1];

 FILE * fp;

 char * pt1;

 char * pt2;

 char * pt3;

 

 

 if( (fp = fopen(FN,"r")) == NULL ){

    fprintf(stderr,"%s \n",strerror(errno));

    exit(5);

 }

 

 while( fgets(tmpline, SIZE1, fp) != NULL ){

     pt1=strchr(tmpline, SPACE);

     pt2=pt1+sizeof(char);

     *pt1='\0';

     pt3=strchr(pt2,SPACE);

     *pt3='\0';

     if(strstr(tmpline,"/dev") != NULL ){

        displayapartition(tmpline,pt2);

     }

 }

 return 0;

}

 

int displayapartition(char * pt,char * pt1){

  

 struct statfs buf;

 statfs(pt1,&buf);

 int usage;

 usage=ceil((buf.f_blocks-buf.f_bfree)*100/buf.f_blocks);

 

 printf("%s ",pt);

 printf("%ld ",buf.f_blocks);

 printf("%ld ",buf.f_blocks-buf.f_bfree);

 printf("%ld ",buf.f_bavail);

 printf("%d%% ",usage);

 printf("%s ",pt1);

 printf("\n");

 

 return 0;

}

df 命令實現的說明

下面解釋一下這個程式:

  • 首先,該程式定義了一個函式 displayapartition, 這裡先定義它的函式原型。
  • 然後我們從主程式說起:首先定義了一個 char tmpline[SIZE1] 陣列,該陣列用來存放從巨集定義常量 FN 代表的檔案中,開啟後存入檔案的每行記錄。
  • 接著定義了一個檔案流指標和 3 個字串指標。
  • 接下來開啟檔案 FN 並把結果賦值給檔案流變數 fp, 如果開啟失敗就退出。
  • 下面從開啟的檔案流中讀出 SIZE1 個字元到臨時陣列 tmpline。比如讀出一行資料為:/dev/sda1 / ext3 rw 0 0  將把 /dev/sda1 放入陣列 tmpline,把載入點 / 放入指標 pt2,同時判斷字串 tmpline 是否包含 /dev 字串,這樣來判斷是否是一個磁碟檔案,如果是的話就呼叫子函式 displayapartition,不是則返回。
  • 子函式 displayapartition 是做什麼的呢?該函式接受 2 個引數,一個是行 /dev/sda1 / ext3 rw 0 0 中的第一列比如:/dev/sda1 也就是實際磁碟作為 pt 指標,一個是行中的第二列比如:/ 也就是掛載點作為 pt1 指標。然後子函式通過 pt1 指標,讀取掛載上的檔案系統資訊到 buf 資料結構裡面。
  • 根據開頭介紹過的 statfs 結構體,buf.f_blocks 表示開啟的檔案系統的總資料塊,buf.f_blocks-buf.f_bfree 表示已經使用的資料塊,buf.f_bavail 表示非超級使用者可用的剩餘資料塊,磁碟使用率就是前面列出過的計算表示式:(f_blocks- f_bfree)/ f_blocks*100%。通過子函式就可以打印出 df 需要顯示的所有資訊到標準輸出了。

小結

本文依次講述了 cp、rm、mkdir、tac、df 命令的主要功能實現程式碼,當然每個命令還有很多引數,我這個模擬實現程式碼甚至連主要功能的很多細節都沒有實現,比如 df 命令的輸出頭我沒有打印出來,這牽涉到列印頭和輸出格式化等很多細節。所以,從這裡我們就可以推斷出,真實的原始碼肯定是考慮得非常全面、嚴謹和健壯的。我這裡只是拋磚引玉,希望能給愛好 Linux 的朋友們提供一種理解 Linux 系統的思路。

 

相關主題