Unix環境高階程式設計入門----Unix環境及系統資料資訊使用
Unix環境高階程式設計入門
----Unix環境及系統資料資訊使用
Unix環境高階程式設計是一種難度較大的程式設計技術,它在軟體開發領域的用途十分廣泛。目前雖處於“曲高和寡”的狀況,但卻是程式設計師不斷進階的“利器”。筆者願將個人對此的學習所得,分期總結整理後與有志深造者共享互助,攜手前行。這一系列文章擬從整體上有著連貫性,而各篇又有相對獨立性。文章中若涉及到前已釋出的《Unix作業系統的入門與基礎》、《Unix的輕巧“約取而實得”》中已解釋的知識點,將不再贅述。讀者閱讀中如果遇到還不甚明瞭的概念,可以查閱本專欄中的上述文章。同時,本系列文章假設讀者已熟悉C++語法,故不對涉及C++語法方面的知識進行解釋。文中的程式碼已在SunOS 5.8中測試通過。
一、在程式中使用環境變數
我們已經知道,環境變數可以用於定製使用者的工作環境,即用環境變數可以儲存使用者對系統進行設定的資訊。要想檢視系統已定義的所有環境變數,可以使用setenv命令。那麼,如何在程式中操作環境變數呢?
在系統為每個程式分配的空間中,都會自動儲存一份系統環境變數的拷貝。ANSI C中定義的兩個函式getenv()和petenv(),分別可以用於獲取環境變數的值和設定環境變數的值。這兩個函式的定義分別是:
#include <stdlib.h>
char* getenv(const char* name); //成功返回指向環境變數值的指標,失敗返回NULL
int putenv(const char* str); //成功返回0,失敗返回-1
我們通常使用getenv()從環境中獲取一個指定的環境變數的值,使用putenv()來改變現有環境變數的值,或增加新的環境變數。putenv()引數形式為“name = value”的字串,如果name指定的環境變數已經存在,則先刪除其原來的定義再賦新值;如果不存在,則增加此環境變數。
現來看下面的一個例程式:
[程式1]
#include <stdlib.h>
#include <iostream>
using namespace std;
int main()
{
char* env = new char[255];
char* a = getenv("ABCDE");
if(a!=NULL)
strcpy(env,a);
cout << "env: " << env << endl;
if(putenv("ABCDE=aaa")<0)
exit(-1);
strcpy(env,getenv("ABCDE"));
cout << "--------------------" << endl;
cout << "env: " << env << endl;
delete env;
return 0;
}
執行此程式,將會在程式空間的環境變量表中增加一個新的環境變數ABCDE,其值為aaa。請注意,程式是在系統為其所分配空間的環境變量表中增加環境變數,而不是在系統的環境變量表中增加環境變數。
二、獲取系統相關資訊
在Unix系統中定義的uname()函式,可以返回與主機和作業系統相關的資訊。uname()的定義是:
#include <sys/utsname.h>
int uname(struct utsname* name); //成功返回非負值,出錯返回-1
通過引數向其傳遞一個utsname結構的地址,uname函式將負責填寫此結構。utsname結構定義如下:
struct utsname {
char sysname[9]; // name of the operating system
char nodename[9]; // name of this node
char release[9]; // current release of operating system
char version[9]; // current version of this release
char machine[9]; // name of hardware type
};
utsname結構中的資訊可用“uname -a”命令來顯示,在程式中則可以如下方式來檢視。
[程式2]
#include <sys/utsname.h>
#include <iostream>
using namespace std;
#define ERR_QUIT(arg) {cout << #arg << endl; exit(-1);}
int main()
{
struct utsname buf;
if(uname(&buf) < 0) ERR_QUIT("uname error");
cout << "Operation System Name: " << buf.sysname << endl;
cout << "Node Name : " << buf.nodename << endl;
cout << "O.S. release level : " << buf.release << endl;
cout << "O.S. Version level : " << buf.version << endl;
cout << "Hardware type : " << buf.machine << endl;
return 0;
}
我們曾經介紹過使用hostname命令來獲取主機名,而要在程式中得到主機名,則可以呼叫Unix系統提供的gethostname()函式,其實hostname命令就是呼叫了gethostname()函式。
#include <unistd.h>
int gethostname(char* name, int namelen); //成功返回0,失敗返回-1
[程式3]
#include <iostream>
#include <unistd.h>
#include <errno.h>
using namespace std;
#define ERR_QUIT(arg) {cout << #arg << endl; exit(-1);}
int main()
{
char buf[256];
if(gethostname(buf,255)<0)
{
cout << "ERR: " << strerror(errno) << " : " << errno << endl;
ERR_QUIT("gethostname error.");
}
cout << "HostName is : " << buf << endl;
return 0;
}
三、獲取使用者相關資訊
使用者每次都需憑一個經系統確認的使用者名稱登入系統,而每個使用者名稱在系統中將對應一個惟一的使用者ID。使用者ID是一個數值,它用於向系統標識各個不同的使用者。在程式中如果想要獲得使用者的登入名以及使用者ID號,可以使用如下的函式:
#include<pwd.h>
char* getlogin(); //得到使用者登入名
int getuid(); //得到當前登入使用者的使用者ID號
int geteuid(); //得到當前執行該程序的有效使用者ID號
struct passwd* getpwuid(int userid); //得到一個指向passwd結構的指標,該結構中包含使用者相關資訊的記錄
passwd的結構如下表所示:
說明 | struct passwd成員 |
使用者名稱 | char* pw_name |
密碼 | char* pw_passwd |
使用者ID | uid_t pw_uid |
組ID | gid_t pw_gid |
註釋 | char* pw_gecos |
工作目錄 | char* pw_dir |
初始shell | char* pw_shell |
當用戶登入後,系統會分配給每個使用者一個組ID,它也是一個數值。一般來說,在Unix中的組可被用於將若干使用者集合到某個課題或部門中去,這種機制允許同組的各個成員之間共享資源(例如檔案)。當然,一個使用者可以參加多個課題或專案,因此也就可以同時屬於多個組。在程式中如果想要獲得使用者所屬的組ID號,可以使用如下的函式:
#include <grp.h>
int getgid(); //得到當前登入使用者的組ID號
int getegid(); //得到當前執行該程序的有效使用者的組ID號
struct group* getpgrgid(int groupid); //得到一個指向group結構的指標,該結構中包含使用者組相關資訊的記錄
group的結構如下表所示:
說明 | struct group成員 |
組名 | char* gr_name |
密碼 | char* gr_passwd |
組ID | int gr_gid |
指向各使用者名稱的指標陣列(其中各指標指向該組的使用者名稱,陣列以null結尾) | char** gr_mem |
來看下面的例程式:
[程式4]
#include <pwd.h>
#include <grp.h>
#include <unistd.h>
#include <iostream>
using namespace std;
int main()
{
struct passwd* pwd;
cout << "Login name: " << getlogin() << endl;
pwd = getpwuid(getuid());
if(pwd)
cout << "Real user: " << getuid() << "(" << pwd->pw_name <<")" << endl;
pwd = getpwuid(geteuid());
if(pwd)
cout << "Effective user: " << pwd->pw_name << endl;
struct group* grp;
grp = getgrgid(getgid());
if(grp)
cout << "Real group: " << getgid() << "(" << grp->gr_name << ")" << endl;
grp = getgrgid(getegid());
if(grp)
cout << "Effective group: " << grp->gr_name << endl;
}
通常情況下,有效使用者ID等於登入使用者ID,有效組ID等於登入使用者組ID。但是如果某一檔案的擁有者通過命令“chmod u+s filename”設定了一個特殊標誌位,則任何執行此檔案的使用者有效ID都將變為該程式擁有者的ID。這一特殊標誌位被稱為設定-使用者-ID(set-user-ID)位,其定義是“當執行此檔案時,將程序的有效使用者ID設定為檔案的所有者”。類似的,還可以通過設定使得執行此檔案的程序有效組ID變為檔案所有者的組ID。
四、時間與日期的使用
由Unix核心提供的基本時間服務是自1970年1月1日00:00:00以來國際標準時間(UTC)所經過的秒數累計值,通常被稱為日曆時間。日曆時間包括時間和日期,這種秒數是以資料型別time_t來表示的(在Solaris中time_t等同於long)。我們可以使用time()函式來獲得表示當前時間和日期的秒值。time()函式的定義是:
#include <time.h>
time_t time(time_t* mem); //成功則返回時間值,出錯返回-1;如果引數非NULL,則時間值也存放在由mem指向的單元內
因此,time()函式常有如下兩種使用方法:
(1) time_t now1;
time(&now1);
cout << now1 << endl;
(2) time_t now2;
now2=time(NULL);
cout << now2 << endl;
[程式5]
#include <time.h> //#include <ctime>
#include <iostream>
using namespace std;
int main()
{
time_t now;
if(time(&now)<0)
{
cout << "error" << endl;
exit(-1);
}
cout << now << endl;
cout << ctime(&now) << endl;
exit(0);
}
由於這種秒值的表示方式,不能直觀的讓人們明白當前的時間,因此通常需要再呼叫其它的時間函式來將其轉換為人們可讀的時間與日期。圖1說明了各種時間函式之間的關係。
(圖1)
函式localtime()和gmtime()可以將秒值轉換成以年、月、日、時、分、秒、週日表示的時間,並將這些存放在一個tm結構中。tm結構定義如下:
struct tm {
int tm_sec; /* seconds after the minute: [0, 61] */
int tm_min; /* minutes after the hour: [0, 59] */
int tm_hour; /* hours after midnight: [0, 23] */
int tm_mday; /* day of the month: [1, 31] */
int tm_mon; /* month of the year: [0, 11] */
int tm_year; /* years since 1900 */
int tm_wday; /* days since Sunday: [0, 6] */
int tm_yday; /* days since January 1: [0, 365] */
int tm_isdst; /* daylight saving time flag: <0, 0, >0 */
};
localtime()和gmtime()之間的區別是:localtime將日曆時間轉換成本地時間(考慮到本地時區和夏時制標誌),而gmtime則將日曆時間轉換成國際標準時間的年、月、日、時、分、秒、週日。它們的定義如下:
struct tm* gmtime(const time_t* mem);
struct tm* localtime(const time_t* mem);
函式mktime()則正好相反,它是以存放有本地時間年、月、日等的tm結構作為引數,將其轉換成time_t型別的秒值。mktime()函式的定義是:
time_t mktime(struct tm* tmptr); //成功返回日曆時間,失敗則返回-1
函式asctime()和ctime()可以獲得人們可讀的時間字串,表示形式如同使用date命令所獲得的系統預設的時間輸出形式。它們的定義如下:
char* asctime(const struct tm* tmptr); //引數是指向存放有本地時間年、月、日等的tm結構的指標
char* ctime(const time_t* mem); //引數是指向日曆時間的指標
函式strftime()是最為複雜的時間函式,可用於使用者自定義時間的表示形式。函式strftime()的定義如下:
size_t strftime(char* buf, size_t maxsize, const char* format,
const struct tm* tmptr); //有空間則返回所存入陣列的字元數,否則為0
自定義格式的結果存放在一個長度為maxsize的buf陣列中,如果buf陣列長度足以存放格式化結果及一個null終止符,則該函式返回在buf陣列中存放的字元數(不包括null終止符),否則該函式返回0。format引數用於控制自定義時間的表示格式,格式的定義是在百分號之後跟一個特定字元,format中的其他字元則按原樣輸出。其中特別應注意的是,兩個連續的百分號則是表示輸出一個百分號。常用的定義格式如下表所示。
格式 | 說明 | 例子 |
% a | 縮寫的週日名 | Tue |
% A | 全週日名 | Tuesday |
% b | 縮寫的月名 | Jan |
% B | 月全名 | January |
% c | 日期和時間 | Wed Aug 17 19:40:30 2005 |
% d | 月日:[01, 31] | 14 |
% H | 小時(每天2 4小時):[00, 23] | 19 |
% I | 小時(上、下午各1 2小時[01, 12] | 07 |
% j | 年日:[001, 366] | 014 |
% m | 月:[01, 12] | 01 |
% M | 分:[00, 59] | 40 |
% p | A M / P M | PM |
% S | 秒:[00, 61] | 30 |
% U | 星期日週數:[00, 53] | 02 |
% w | 週日:[ 0 =星期日,6 ] | 2 |
% W | 星期一週數:[00, 53] | 02 |
% x | 日期 | 08/17/05 |
% X | 時間 | 19:40:30 |
% y | 不帶公元的年:[00, 991] | 05 |
% Y | 帶公元的年 | 2005 |
% Z | 時區名 | MST |
下面再來看一個具體程式。在此程式中,如提供一個秒值作引數,則會依照自定義的格式輸出日期與時間資訊;如提供一個以年、月、日、時、分、秒為格式的引數,則會輸出從1970年1月1日00:00:00以來國際標準時間(UTC)所經過的秒數累計值。
[程式6]
#include <time.h>
#include <iostream>
#include <string>
#include <cstdio>
#include <stdexcept>
using namespace std;
void usage(char * proc)
{
cout << " Usage: 1." << proc << " [YYYY MM DD HH MM SS] (HH -- In 24 hours format)" << endl;
cout << " 2." << proc << " [time in seconds]" << endl;
}
int main(int argc, char* argv[])
{
if(argc == 1)
{
usage(argv[0]);
return EXIT_FAILURE;
}
if(argc == 2) //Case in 2
{
long transfer;
transfer = atol(argv[1]);
struct tm* now = NULL;
now = localtime(&transfer);
char timestamp[150];
strftime(timestamp,sizeof(timestamp),"%Y-%m-%d %H:%M:%S",now);
strcat(timestamp," (format in: YYYY-MM-DD HH:MM:SS)");
cout << endl << "/tAfter convert: " << timestamp << endl << endl;
return EXIT_SUCCESS;
}
else //Case in 1
{
if(argc != 7)
{
usage(argv[0]);
return EXIT_FAILURE;
}
try{
struct tm* now = new struct tm;
now->tm_year = atoi(argv[1]);
now->tm_mon = atoi(argv[2]);
now->tm_mday = atoi(argv[3]);
now->tm_hour = atoi(argv[4]);
now->tm_min = atoi(argv[5]);
now->tm_sec = atoi(argv[6]);
//Adjust
now->tm_mon--;
now->tm_year -= 1900;
cout << endl << "/tAfter convert: " << mktime(now) << " (format in seconds since UTC)" << endl;
delete now;
}catch(logic_error& e) {
cout << e.what() << endl;
}
return EXIT_SUCCESS;
}
return EXIT_SUCCESS;
}
五、登入會計檔案的使用
Unix系統中提供了下列兩個資料檔案:
(1)utmp檔案,用於記錄當前登入系統的各個使用者,它被放於/var/adm/utmpx;
(2)wtmp檔案,用於跟蹤所有登入與登出系統的事件,它被放於/var/adm/wtmpx。
此外,還定義了一個結構體utmpx,其包含了使用者登入與登出系統的相關資訊。使用者每次登入系統,系統會自動填寫這一結構,然後將其寫入到utmp檔案中,同時也將其添寫到wtmp檔案中。登出時,系統會將utmp檔案中相應的記錄擦除(每個位元組都填以0),並將一條新記錄添寫到wtmp檔案中。另外,在系統再次啟動以及更改系統時間和日期時,也都會在wtmp檔案中添寫特殊的記錄項。
上述兩個資料檔案為二進位制檔案,人們要是直接開啟會看到一堆的亂碼,但可以通過who命令讀utmp檔案並以可讀格式輸出其內容,使用last命令可以檢視wtmp檔案中所有的記錄。而在自己編寫的程式中,則可以使用相應的函式來檢視資料檔案的實用資訊,其程式如:
[程式7]
#include <iostream>
#include <utmpx.h>
#include <unistd.h>
#include <errno.h>
using namespace std;
#define trace(arg) cout << #arg << " = " << (arg) << endl
int main(int argc, char** argv)
{
if(argc == 2) {
utmpxname(argv[1]); //開啟新的記錄檔案,請注意由argv[1]引數指定的檔名必須以x結尾
}
struct utmpx * tmp;
tmp = getutxent(); //getutxent()可以從wtmpx中獲得一行資料記錄,即一條登入或登出事件
while(tmp != NULL)
{
trace(tmp->ut_user); //登入賬號
trace(tmp->ut_id); //登入ID
trace(tmp->ut_line); //登入裝置名,省略了“/dev/”
trace(tmp->ut_pid); //程序ID
trace(tmp->ut_type); //登入型別,7(或USER_PROCESS)代表登入,8(或DEAD_PROCESS)代表登出
trace(tmp->ut_tv.tv_sec); //執行當前操作時的時間記錄
cout << "---------------" << endl;
tmp = getutxent();
sleep(1);
}
return 0;
}
執行此程式,會自動讀取wtmp檔案中的資訊,並依照我們所定義的格式輸出。再來看一個複雜點的例程式。
[程式8]
#include <iostream>
#include <utmpx.h>
#include <unistd.h>
#include <errno.h>
using namespace std;
void show(struct utmpx*& tmp)
{
cout << tmp->ut_user << endl;
cout << tmp->ut_id << endl;
cout << tmp->ut_line << endl;
cout << tmp->ut_pid << endl;
cout << tmp->ut_type << endl;
cout << tmp->ut_tv.tv_sec << endl;
}
int main()
{
static int i=0;
struct utmpx * tmp;
struct utmpx * t = new utmpx;
tmp = getutxent();
while(tmp != NULL)
{
show(tmp);
cout << "______________" << endl;
tmp = getutxent();
sleep(1);
if(++i == 7) break;
if(i == 5)
{
cout << "************** " << endl;
memcpy(t,tmp,sizeof(struct utmpx));
show(t);
cout << "************** " << endl;
}
}
struct utmpx* ll;
setutxent(); //將當前檔案位移量置0,即從檔案頭部重新開始讀第一條記錄
cout << "************** " << endl;
ll = getutxline(t); //找到與t的ut_user相同、ut_type也相同的第一條記錄
show(ll);
cout << "************** " << endl;
while(1)
{
ll = getutxent();
show(ll);
if(ll->ut_type == USER_PROCESS || ll->ut_type == DEAD_PROCESS)
{
memcpy(t,ll,sizeof(struct utmpx));
break;
}
}
utmpxname("utmp.bakx"); //開啟一個新的記錄檔案,請注意檔名必須以x結尾
cout << " the following will be write into .. /n";
show(t);
pututxline(t); //將t指向的記錄寫入新的記錄檔案
endutxent(); //關閉檔案流
delete t;
return 0;
}
每個人都有打電話的經歷,在我們撥打電話時會進入電信計費系統,一旦撥通電話則系統會記錄登入時間,結束通話電話則系統會記錄退出時間,兩時間差即為使用者的通話時長。那麼在每個月底將某一使用者的通話記錄搜尋出來放入一個新的記錄檔案,並將其打印出來便是使用者的每月通話賬單了。因此,Unix的登入會計檔案完全可以用於電信服務中的資料採集工作,電信的資料採集系統可能也正是採用了類似的方式。