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的登录会计文件完全可以用于电信服务中的数据采集工作,电信的数据采集系统可能也正是采用了类似的方式。