1. 程式人生 > >Linux系統程式設計第一課: CentOS7下who命令的實現

Linux系統程式設計第一課: CentOS7下who命令的實現

  今天是學Linux系統程式設計的第一天,然而明天六級考試我卻在這裡寫程式碼。。。。不管怎麼樣先記錄下這次實驗再說。
  這一次的內容是手工實現who命令。who命令在Linux系統內用來檢視各個使用者的登入情況,可以顯示在哪個終端登陸以及登陸時間等資訊。自己實現的話自然是需要和系統互動啦,所以我們需要去找出來用什麼標頭檔案可以讓我們和系統互動並且獲取使用者的登陸資訊。程式碼先放在下面。

#include <stdio.h>   //standard input and output stream
#include <stdlib.h>  //function exit
#include <utmp.h> //user login records relating #include <fcntl.h> //file controal relating, open function #include <unistd.h> //function read and close #include <time.h> #define SHOWHOST void showtime(long timeval); void show_info(struct utmp *); int main(){ struct utmp utbuf; int
utmpfd; if((utmpfd = open(UTMP_FILE, O_RDONLY))==-1){ printf("error\n"); perror(UTMP_FILE); exit(1); } while(read(utmpfd, &utbuf, sizeof(utbuf)) == sizeof(utbuf)) show_info(&utbuf); close(utmpfd); return 0; } void show_info(struct utmp * utbufp){ if
(utbufp->ut_type != USER_PROCESS) return ; printf("% -8.8s ", utbufp->ut_user); printf("% -8.8s ", utbufp->ut_line); showtime(utbufp->ut_time); #ifdef SHOWHOST if(utbufp->ut_host[0] != '\0') printf("(%s)", utbufp->ut_host); #endif puts(""); } void showtime(long timeval){ char *cp; cp = ctime(&timeval); printf("%12.12s", cp); }

  我在CentOS下編譯通過,實現功能和who基本一樣。
跟著課本實現很輕鬆。man who可以檢視who的資訊,在man手冊靠後的地方我們發現了who命令需要和一個檔案互動:/var/run/utmp(預設),那麼我們來看看utmp是幹什麼的吧。
  man utmp之後可以基本知道這東西就是系統用來處理登陸資訊的了.標頭檔案bits/utmp.h

struct utmp
{
    short int ut_type;
    pid_t ut_pid;
    char ut_line[UT_LIENSIZE[;
    char ut_id[4];
    char ut_user[UT_NAMESIZE];
    char ut_host[UT_HOSTSIZE];
    strcut exit_status ut_exit;
#ifdef __WORDSIZE_TIME64_COMPAT32
    int32_t ut_session;
    struct
    {
        int32_t tv_sec;
        int32_t tv_usec;
    } ut_tv;
#else
    long int ut_session;
    struct timeval tu_tv;
#endif
#define ut_name ut_user
#ifndef _NO_UT_TIME
#define ut_time ut_tv.tv_sec
#endif
#define ut_xtime ut_tv.tv.sec
#define ut_addr ut_addr_v6[0]    
}

好的, 我們注意到了這個結構,裡面有和使用者登陸相關的所有資訊. 我們的問題就變得簡單了, 只要寫一個小程式開啟/var/run/utmp檔案, 按照定義的utmp結構把檔案裡面的內容統統讀取出來然後列印到螢幕上就OK啦.
但是有一個問題: 用什麼開啟?用什麼讀取? 也許使用fgets之類的函式? 我相信用過的人都是對這些函式充滿了嫌棄的. 我們現在有更好的選擇.read, open, close函式.
三個函式定義在不同的地方, open在標頭檔案fcntl.h裡面,而read和close在unistd.h裡面. 三個檔案各自負責不同的部分. open函式接受一個字串指標和一個flag, flag包括O_RDONLY, O_WRONLY, O_RDWR, 我們這裡不希望修改utmp的內容, 所以使用O_RDONLY, 同時,open返回一個檔案描述符(整型), 如果是0, 代表標準輸入, 1代表標準輸出, 2表示標準錯誤輸出. -1表示錯誤. 其他的正數表示檔案成功開啟而且已經建立了聯絡,可以通過這個檔案描述符來訪問檔案.
所以程式碼裡,我們用utmpfd是否等於-1來判斷是否能夠進行輸出. 當不為-1的時候, 我們的程式可以順序執行, 通過檔案描述符來訪問檔案utmp, while迴圈呼叫read函式來進行讀取.
read函式接受三個引數,檔案描述符,一個指標, 以及讀取的字元數量.原型是


ssizt_t read(int fd, void* buf, size_t count)

那麼接受open返回值的utmpfd就可以作read的第一個引數, 而中間的指標, 考慮到utmp在utmp中已經定義是結構體, 那麼我們就用struct utmp來處理它, 至於最後一個, 由於檔案內資料是按序排布的, 我們只有一直讀取和結構等大位元組的資料出來就可以了. 而利用read的返回值是成功讀取的字元數量, 那麼利用utmp結構的大小就可以控制讀取合適結束, 如程式碼已經展示的那樣.

程式碼接下來的部分就是一些格式化輸出啦, 但是在依照課本修改程式碼的時候, 我發現一個問題, 用來格式化輸出時間, 讓時間readable的函式ctime在man手冊內接受的是一個time_t指標, 但是在utmp.h內並沒有定義time_t型別,而且, 也沒有程式碼中出現的ut_time變數, 程式碼是怎麼正常工作的呢?
返回去看utmp.h, 在靠近結構定義的結尾部分,我們看到了一串巨集定義, 前面已經打出來了.有一對看不明白的關於字寬的檢查和一個很關鍵的typedef ut_time ut_tv.tv_sec, 很好, 我們順利找到了ut_time的定義, 但是再返回去一看, 我們用的ut_tv.tv_sec是哪一個?這會影響到我們具體在showtime裡面用long還是int還是別的什麼型別來作引數. 圖國條件編譯裡面條件為真, 那麼型別是int32_t, 我們寫一個小程式int32.c, 如下:

#include <stdio.h>    
#include <stdlib.h>
int main(){
    int32_t num;
    return 0;
}      

然後用命令gcc -E int32.c | grep int32_t可以檢視到最終的一個typedef, 是這樣的typedef signed int int32_t, 那麼我們自然就可以用signed int來讀取時間, 但是如果條件為假呢? timeval結構體是什麼? 為了解決這個問題, 我開始仿照書上照read等一系列的函式的方法開始了尋找timeval.

先看一看, timeval是和時間相關的, 那麼去和時間有關的標頭檔案裡面看看好了. 到/usr/inclue裡面, 查看了time.h檔案, 發現裡面第118行有一個定義

 struct timeval
{
        __time_t tv_sec;
        __susecond_t tv_usec;
 };

很好, 我們找到了timaval的原型, 不過問題來了, __time_t和time_t是什麼關係?繼續找好了, 很好, 75行出現了typedef __time_t time_t, 感覺到這裡大功告成了. 然而我不知為何腦袋有洞, 之前提到關於字寬的定義, 為真還是假? 於是我又開始了尋找, 回到utmp.h裡面, 我發現幾乎都是extern宣告, 顯然沒什麼用, 但是隨之我發現出現了一個頭檔案sys/types.h, 這個是幹嘛的? 好奇開啟看了看, 沒有收穫. 但是又發現了另外一個巨集: bits/utmp.h, 開啟一看

#include <paths.h>
#include <sys/time.h>
#include <sys/types.h>
#include < bits/wordsize.h>

!!!!!wordsize不正是我要找的嗎?!趕緊開啟

#if defined __x86_64__ && !defined __ILP32__
# define __WORDSIZE 64
#else
# define __WORDSIZE 32
#endif
#ifdef __x86_64__
# define __WORDSIZE_TIME64_COMPAT32 1
# define __SYSCALL_WORDSIZE 64
#endif

雖然沒有發現十分有頭緒的地方, 但是到目前位置, 為了utmp, 我們已經知道utmp.h同時和time.h, sys/types.h, sys/times.h所關聯, 他們聯合在一起為timeval提供了定義, 同時還要避免重複定義和重複包含的問題, 然後utmp.h還和bits/utmp.h關聯, 而這個檔案又依靠和bits/wordsize.h的關聯為utmp.h和sys/utmp.h提供了WORDSIZE方面的定義, 於是整個系統的使用者資訊才能夠被完整而且準確的記錄, 同時由於int32_t和long的大小關係, who命令製作時優先選用long便可以避免32位系統和64位系統不相容的情況.而事實上, 還有更復雜的地方, time.h裡面還定義了一個叫timespec的時間結構, 提供納秒級的時間, 而sys/time.h裡面還提供了一個巨集定義以來進行timeval和timespec的轉換以保證程式正確.

我實際找timeval的定義是花了比寫這篇部落格還久得多的時間, 因為沒有看到time.h裡面對ut_time的巨集定義, 所以滿世界搜尋標頭檔案來匹配. 雖然花了很多時間, 但是也是第一次那麼認真地把各個標頭檔案之間相互包含關聯的內容檢視一遍.

不得不感慨系統製作之不易, 443個頭檔案, 靜態庫和動態庫被儲存在/usr/include目錄下, 彼此之間相互關聯之複雜, 我僅查看了8個頭檔案就已經被震撼. 尤其在無數的複雜的巨集呈現, 然而其目的卻只是保證粗心的使用者和程式設計師能夠正常使用,不用擔心各類庫的呼叫會帶來嚴重的後果. 工程師代替使用者做好了一切的防護措施, 以後再也不敢甩鍋給系統…不過最佩服的還是Linus居然能夠只靠一個人就完成Linux核心的構建工作, 實在不敢想象在當時沒有各類強大IDE支援的時代他是如何完成如此複雜異常的工作的.