木头骑士的Linux编程实验室(一)——时间、错误、限制

Linux上层软件编程,除了一门必须的编程语言,比如C语言,还需要了解的,就是Linux的编程环境了。这里最常打交道的就是Linux的各种系统调用了。这里会涉及到Linux以及其先祖——UNIX的各种标准,这里不打算深入探讨这些标准的历史与关系、区别,重点在于研究Linux环境下编程所用到的系统调用。
系统调用,长得很像普通的函数,但其实现过程却要复制一些,因为系统调用是用户空间与内核态的接口。在用户空间下,我们不能直接操纵内核资源,不能读写磁盘,不能创建进程,不能与别的进程交互,甚至进程不能“自行了断”,这些事都需要内核来帮我们做,而用户空间下的程序,就是通过这些系统调用,切换到内核态,做了想要做的内核态的操作,再切换回用户态。
Linux编程实验室旨在对各种Linux系统调用弄进行实验。系统调用主要针对的是文件操作、线程、进程管理以及进程间通信等问题提供接口,在讨论这些问题之前,先来讨论一些简单的东西,包括时间、错误以及系统限制。

1.时间
时间对于人类来说很重要,对于计算机也很重要,弄不清时间,就会产生很多问题,还记得15年前的千年虫吧,时间混乱了,计算机也蒙圈了。
为了度量时间,UNIX采用两种不同的时间值:
1.日历时间。自协调世界时间(Coordinated Universal Time, UTC)1970年1月1日00:00:00这个特定时间以来所经过的秒数累积。
2.进程时间。也被称为CPU时间,用于度量一个进程使用CPU资源的时钟滴答数。
1.1 日历时间(calendar Time)
先来罗列一下日历时间用到的函数,主要包括三类:获取时间、设置时间以及时间格式的转换。
获取时间的函数:
#include <time.h>
time_t time(time_t *calptr);
#include <sys/time.h>
int clock_gettime(clockid_t clock_id, struct timespec *tsp);
#include <sys/time.h>
int gettimeofday(struct timeval *restrict tp,struct timezone *tz);

设置时间的函数:

#include <sys/time.h>
int settimeofday(const struct timeval *tv, const struct timezone *tz);
#include <sys/time.h>
int adjtime(struct timeval *delta, struct timeval *olddelta);
时间格式转换函数:

#include <time.h>
struct tm *gmtime(const time_t *calptr);
struct tm *localtime(const time_t *calptr);
#include <time.h>
time_t mktime(struct tm *tmptr);
#include <time.h>
size_t strftime(char *restrict buf, size_t maxsize, const char *restrict format, const struct tm *restrict tmptr);
size_t strftime_l(char *restrict buf, size_t maxsize, const char *restrict format, const struct tm *restrict tmptr, locale_t locale);
#include <time.h>
char *strptime(const char *restrict buf, const char *restrict format, struct tm *restrict tmptr);

先来看获取时间的三个函数。
time用于返回自Epoch(通用协调时间的1970年1月1日0点)以来的秒数,存放在time_t结构中。若出错,返回-1
clock_gettime函数提供更多的功能和更高的精度。可以通过第一个参数指定所要返回的时间,在指定为CLOCK_REALTIME时,与time一样。第二个参数是一个timespec结构的指针。timespec是UNIX中表示时间的一个通用结构,至少包含如下两个字段:
time_t    tv_sec;            // 秒
long      tv_nsec;          // 纳秒
gettimeofday函数在SUSv4中已被标记为弃用,该函数提供了比time更高的精度(到微秒级)。第一个参数用于返回时间,timeval结构如下:
struct timeval {
    time_t                tv_sec;        // 从Epoch开始的秒数
    suseconds_t<span style="font-family:微软雅黑;">                   </span>tv_usec;       // 额外的微秒数(long int)
};
第二个参数原本用于指定时区,现在已经废弃,应将其指定为NULL。
目前这三个获取时间的函数都是以Epoch开始的秒数来计算的,谁看到那么一长串数字都得蒙圈,所以还要将其转换为我们能认识的年月日时分秒的形式。UNIX已经为我们准备好了转换的函数,先简单看一下。
gmtime和localtime将一个time_t结构的时间转换为一个tm结构,两者的区别是,gmtime将日历时间转换为协调统一时间,localtime将日历时间转换成本地时间。
tm结构定义如下:
struct tm {
    int    tm_sec;        // 秒数(0-60)
    int    tm_min;        // 分钟数(0-59)
    int    tm_hour;       // 小时数(0-23)
    int    tm_mday;       // 日数(1-31)
    int    tm_mon;        // 月数(0-11)
    int    tm_year;       // 从1900年算起的年数
    int    tm_wday;       // 从周日算起的星期数(0-6)
    int    tm_yday;       // 从一月一日起的日数(0-365)
    int    tm_isdst;      // 夏令时标志(小于0表示不使用该标志,等于零表示非夏令时,大于0表示夏令时)
};
OK,有了这几个函数的准备,我们就可以看看实验一下获取当前的时间了。准备下面一段程序:
#include <stdio.h>
#include <time.h>
#include <sys/time.h>

#include "timeTest.h"


void print_tm(const struct tm *ptm){
 printf("tm_sec:	 %d\n", ptm->tm_sec);
 printf("tm_min:	 %d\n", ptm->tm_min);
 printf("tm_hour:	%d\n", ptm->tm_hour);
 printf("tm_mday:	%d\n", ptm->tm_mday);
 printf("tm_mon:	 %d\n", ptm->tm_mon);
 printf("tm_year:	%d\n", ptm->tm_year);
 printf("tm_wday:	%d\n", ptm->tm_wday);
 printf("tm_yday:	%d\n", ptm->tm_yday);
 printf("tm_isdst:	%d\n", ptm->tm_isdst);
}

void timeTest(){
 printf("==========timeTest1==========\n");
 time_t paratime;
 time(<span style="font-family:微软雅黑;">&</span>time);
 printf("test gmtim():\n");
 struct tm *utc_tm = gmtime(<span style="font-family:微软雅黑;">&</span>time);
 print_tm(utc_tm);

 printf("test localtime():\n");
 struct tm *loc_tm = localtime(<span style="font-family:微软雅黑;">&</span>time);
 print_tm(loc_tm);


 printf("==========timeTest2==========\n");
 printf("test clock_gettime():\n");
 struct timeval tvl;
 clock_gettime(CLOCK_REALTIME, &tvl);
 utc_tm = gmtime(&tvl.tv_sec);
 print_tm(utc_tm);
 printf("micro second: %ld\n", tvl.tv_usec);

 printf("==========timeTest3==========\n");
 printf("test mktime()\n");
 time_t ttmp = mktime(utc_tm);
 printf("time_t from mktime() is: %ld\n", ttmp);
}
print_tm用来打印一个tm结构,timeTest函数测试了两个获取日历时间的函数time和clock_gettime。然后用gmtime和localtime将获取到的秒数转换为tm结构,并打印出来。下面是该函数的执行结果:
==========timeTest1==========
test gmtim():
tm_sec: 3
tm_min: 30
tm_hour: 0
tm_mday: 2
tm_mon: 8
tm_year: 115
tm_wday: 3
tm_yday: 244
tm_isdst: 0
test localtime():
tm_sec: 3
tm_min: 30
tm_hour: 0
tm_mday: 2
tm_mon: 8
tm_year: 115
tm_wday: 3
tm_yday: 244
tm_isdst: 0
==========timeTest2==========
test clock_gettime():
tm_sec: 3
tm_min: 30
tm_hour: 0
tm_mday: 2
tm_mon: 8
tm_year: 115
tm_wday: 3
tm_yday: 244
tm_isdst: 0
micro second: 510315670
==========timeTest3==========
test mktime()
time_t from mktime() is: 1441153803

这里看到,我的环境下用gmtime和localtime执行得到的tm结构是一样的,这是因为我的Linux环境时间一直没设置明白,用的就是UTC时间。
当然,还有函数可以将tm结构转换回表示秒数的time_t结构,这个函数就是mktime,最后的测试我们将转换的tm结构又转换了回去,获得了秒数,在目前的Linux实现中,time_t结构实现为long long。
tm结构与格式化输出:
类似于sprintf,有两个函数可以将tm结构进行字符串形式的格式化输出,它们是strftime和strftime_l,后者多一个locale参数用于设置时区,而strftime通过TZ环境变量指定时区。
ISO C规定了37种转换符,下图说明了这37种转换符,摘自《APUE》。

37个转换符,就不一一测试了,这里使用《APUE》中的代码做一个测试,代码如下:
void timePrintTest(){
    time_t t;
    struct tm *tmp;
    char buf1[16];
    char buf2[64];

    time(&t);
    tmp = localtime(&t);
    if (strftime(buf1, 16, "time and date: %r, %a, %b, %d, %Y", tmp) == 0)
        printf("buffer length 16 is too small\n");
    else
        printf("%s\n", buf1);
    if (strftime(buf2, 64, "time and date: %r, %a, %b, %d, %Y", tmp) == 0)
        printf("buffer length 64 is too small\n");
    else
        printf("%s\n", buf2);

    struct tm tmp2;
    char *p = buf2 + strlen("time and date: ");
    if (NULL == strptime(p, "%r, %a, %b, %d, %Y", &tmp2)) {
        printf("some error occured\n");
    }
    print_tm(&tmp2);
}
运行结果:
buffer length 16 is too small
time and date: 12:59:32 AM, Wed, Sep, 02, 2015
tm_sec: 32
tm_min: 59
tm_hour: 0
tm_mday: 2
tm_mon: 8
tm_year: 115
tm_wday: 3
tm_yday: 244
tm_isdst: 924975104

这里看到,如果第二个参数指定的最大字节数不足以存放格式化输出的字符串时,strftime将返回0。
strptime函数是strftime的反过来的版本,将字符串时间转换为分解时间。上面试验中最后一小段就是将buf2中存放的格式化字符串的时间转换回tm结构并打印了出来。
至此,对于系统时钟,我们就试验了获取时间和时间格式转换的函数,对于设置系统时间这东西我们一般用不到,更多的还是用NTP进行时间同步,这里就先不做试验了。
最后,贴出一张《APUE》中各个时间函数的关系图:

1.2进程时间
进程时间是一个进程占用CPU的时间总量,适用于对程序、算法性能的检查和优化。
内核把CPU时间分成如下两部分:
  • 用户CPU时间是在用户模式下执行所花费的时间数量。有时也成为虚拟时间(virtual time),这对于程序来说,是它已经得到的CPU的时间。
  • 系统CPU时间是在内核模式中执行所花费的时间数量。这是内核用于执行系统调用或代表程序执行的其他任务(例如,服务页错误)的时间。
两个函数用于获取进程时间:times和clock
#include <sys/times.h>
clock_t times(struct tms *buf);
该函数将调用该函数的进程时间信息放到buf指向的结构体中。tms结构体格式如下:
struct tms {
    clock_t tms_utimes;        // 调用进程的用户CPU时间
    clock_t tms_stimes;        // 调用进程的系统CPU时间
    clock_t tms_cutime;        // 等待的所有子进程的用户CPU时间
    clock_t tms_cstime;        // 等待的所有子进程的系统CPU时间
};
其中后两个字段返回的信息是:父进程执行了系统调用wait()的所有已经终止的子进程使用的CPU时间。
数据类型clock_t是用时钟计时单元(clock tick)为单位度量时间的整型值。怎么把它转化成秒数呢,可以调用sysconf(_SC_CLK_TCK)来获取每秒钟包含的时钟计时单元数,然后用这个数字除以clock_t转换为秒,这里需要注意的是,要使用_SC_CLK_TCK这个宏,需要包含unistd.h。
另一个函数clock提供了一个简单的接口用于取得进程时间。它返回一个描述了调用进程使用的总的CPU时间(包括用户和系统)。该函数的返回值虽然也是clock_t类型,但与times函数不同,该值需要除以CLOCKS_PER_SEC来专换成秒。
接下来实验这两个函数:
void printTimes(struct tms *buf){
	long clockTicks = sysconf(_SC_CLK_TCK);
	printf("tms_utime  on seconds: %.2f\n", (double)(buf->tms_utime)/clockTicks);
	printf("tms_stime  on seconds: %.2f\n", (double)(buf->tms_stime)/clockTicks);
	printf("tms_cutime on seconds: %.2f\n", (double)(buf->tms_cutime)/clockTicks);
	printf("tis_cstime on seconds: %.2f\n", (double)(buf->tms_cstime)/clockTicks);
}

void sysTimeTest(){
	//long clockTicks = sysconf(_SC_CLK_TCK);
	printf("sysconf(_SC_CLK_TCK): %ld\n", sysconf(_SC_CLK_TCK));
	struct tms buf;
	int i = 0;
	//clock_t start = clock();
	for(i = 0; i < 1000000; i++)
		getppid();
	times(&buf);
	clock_t tmp = clock();
	printf("after 100000 times getppid(), test times()\n");
	printTimes(&buf);
	printf("and test clock()\n");
	printf("clock() value on seconsd: %.2f\n", (double)tmp/CLOCKS_PER_SEC);
}
这里将getppid这个系统调用执行100000此,观察使用的进程时间,执行结果如下:
sysconf(_SC_CLK_TCK): 100
after 100000 times getppid(), test times()
tms_utime  on seconds: 0.05
tms_stime  on seconds: 0.14
tms_cutime on seconds: 0.00
tis_cstime on seconds: 0.00
and test clock()
clock() value on seconsd: 0.19
可以看到,clock的结果是用户进程时间和系统集成时间的和。由于该进程在本实验中没有创建子进程,所以times结果的后面两个字段都是0。
2.错误
Linux的系统调用的返回值基本上都用-1表示出错,在系统调用返回-1的同时,内核会将一个全局变量设置为一个表示错误的值,这个全局变量是errno,定义在<errno.h>中,其声明形式如下:
extern int errno;
这是一个全局变量,我们可以对其赋值,但是我们主要还是用它来获取当前的出错信息,这里需要注意,因为errno是个全局变量,而且对于一个进程而言,所有的出错信息都要修改这个全局变量,所以在系统调用发现错误时,需要立即将该值取出,否则该值可能被新的errno值覆盖。有两个函数可以帮助我们从一个错误值中获取所需的错误信息。
#include <string.h>
char *strerror(int errnum);
该函数返回一个字符串,用于说明错误信息。
#include <stdio.h>
void perror(const char *msg);
该函数先输出由msg指向的字符串,然后是一个冒号,一个空格,接着是对应于errno值的出错消息,最后是一个换行符。
接下来做个实验,看看Linux的这套系统调用的错误处理机制。我们用一个打开文件的系统调用产生一个错误。
ls -l /tmp/rootfile
-rw-------. 1 root root 5 Sep  7 00:43 /tmp/rootfile
可以看到,在/tmp下创建了一个普通用户没有权限的文件rootfile,然后以普通用户的身份执行下面这段程序
void errorTest(){
	if (-1 == open("/tmp/rootfile", O_RDONLY)) {
		int err = errno;
		printf("strerror() test:\n");
		printf("%s\n", strerror(err));
		printf("perror() test:\n");
		perror("perror");
	}
}
这里我试图以普通用户的身份打开这个文件,但没有权限,系统调用open就会发成错误。这里我编译出的课执行文件是LinuxTest,在执行:
./LinuxTest > /tmp/a.txt时,发现终端出现了如下打印信息:
perror: Permission denied
然后打开a.txt,其中的内容如下:
strerror() test:
Permission denied
perror() test:
发现perror("perror")这句程序的输出并没有被重定向到a.txt中,说明perror函数的输出是输出到了标准出错。
重新执行:
./LinuxText >/tmp/a.txt 2>&1
这样,将标准输出和标准出错都重定向到a.txt中,终端没有输出了,而a.txt的内容变为:
perror: Permission denied
strerror() test:
Permission denied
perror() test:
这里发现perror("perror")这句程序的输出跑到了最前面,这事儿我也解释不清,我的猜测是:标准输出和标准出错重定向到同一个文件后,并非实时写入的,写入的顺序不定。
3.限制
这里所谓的限制,其实是程序运行环境的一些环境特性,比如一个int有多少位啦,一个文件的名字最长允许多长啦,还有前面时间不符我们看到的,每秒钟有多少个时钟滴答数啦,如此种种。限制值的获得分为两种,一种是编译时获得,一般通过宏来实现,一种是运行时获得,一般通过3个conf结尾的函数取得。为什么要设置这些限制呢,其实是为了方便程序在不通的运行环境之间进行移植。下面截取几个《APUE》中的关于编译时获得的限制的图。
ISO C限制:

POSIX限制:



还有一些限制值需要在运行时才能确定,这种限制又分为两类,一类是与文件无关的限制,一种是与文件有关的限制,与文件无关的限制通过sysconf函数在运行时获取,与文件有关的限制可通过pathconf或fpathconf在运行时获取。三个函数的原型为:
#include <unistd.h>
long sysconf(int name);
long pathconf(const char *pathname, int name);
long fpathconf(int fd, int name);
    返回值:成功,返回相应值,出错返回-1
其中的name参数的取值见如下两个图:


在时间部分的试验中,我们已经通过sysconf(_SC_CLK_TCK)获取了每秒钟的时钟滴答数,其他运行时限制的获取与此相似。


说明:本文的实验使用eclipse建立工程,已上传至github:
https://github.com/haoranzeus/LinuxProgrammingLib.git



评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值