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");elseprintf("%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");elseprintf("%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

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

  1. 遇到一个Linux文件系统因bios时间错误变成只读的问题

    正在进行的项目所用的系统是基于ubuntu构建的,前文也有说明.由于某些原因,需要在一块主板上用dd命令拷贝已经做好的系统镜像到硬盘,然后将这个硬盘换到另一块板子上使用.近期发现一个问题,就是将拷贝好 ...

  2. Linux编程:mktime通过时间获取时间戳

    Linux编程:获取GMT(UTC)与Local时间,及其线程安全_风静如云的博客-CSDN博客 描述了如果通过时间戳获取对应的UTC及Local时间. Linux提供了函数mktime用于完成反向操 ...

  3. Linux编程获取网络信息总结

    Linux下C获取所有可用网卡信息 在Linux下开发网络程序时,经常会遇到需要取本地网络接口名.IP.广播地址 .子网掩码或者MAC地址等信息的需求,最常见的办法是配合宏SIOCGIFHWADDR. ...

  4. linux的编程命令,linux编程常用命令

    学习linux编程最基本的就是要掌握常用的编程命令,下面由学习啦小编为大家整理了linux编程常用命令相关知识,希望大家喜欢! linux编程常用命令1.编译应用程序 make -f makefile ...

  5. 想学python都要下载什么软件-学编程闲余时间建议下载的软件_Python新手入门教程...

    原标题:学编程闲余时间建议下载的软件_Python新手入门教程 Python新手入门教程_在手机上就能学习编程的软件 很多小伙伴会问:我在学编程,想利用坐地铁坐公交吃饭间隙学编程,在手机上能学编程的软 ...

  6. 学习Unix/Linux编程要学些什么

    最近利用空余时间看了一下<Unix/Linux编程实践教程>,原书名为:Understanding Unix/Linux Programming: A Guide to Theory an ...

  7. 牛人整理分享的面试知识:操作系统、计算机网络、设计模式、Linux编程,数据结构总结...

    网站地址:http://www.itmian4.com 基础篇:操作系统.计算机网络.设计模式 一:操作系统 1. 进程的有哪几种状态,状态转换图,及导致转换的事件. 2. 进程与线程的区别. 3. ...

  8. RHCE实验室NTP时间服务器配置最终版

    以RHCE实验室为准,一个简单的NTP时间服务器标准配置如下: 服务器端: 首先修正下系统时间,并将系统时间写进硬件时间里 date MMDDhhmmYYYY.ss;hwclock -w vim /e ...

  9. linux编程参数列表,Linux编程 14 文件权限(用户列表passwd,用户控制shadow,useradd模板与useradd命令参数介绍)...

    一. 概述 linux安全系统的核心是用户账户. 创建用户时会分配用户ID(UID). UID是唯一的,但在登录系统时不是用UID,而是用登录名.在讲文件权限之之前,先了解下linux是怎样处理用户账 ...

最新文章

  1. 【深度】清华黄高等人新作:动态神经网络首篇综述
  2. python显示图片
  3. python数据科学讲解_数据科学的概念-Python数据科学技术详解与商业项目实战精讲 - Python学习网...
  4. SQL语句inner join,left join ,right join连接的不同之处
  5. 幅度调制(AM调制、DSB(双边带)调制、SSB、VSB)
  6. centos系统mysql连接workbench
  7. Docker-创建支持ssh服务的镜像
  8. 手把手教你创建 Alexa Smart Home Skill (二)
  9. CRM系统提高企业核心竞争力
  10. mac忘记mysql用户名和密码_mac 下 忘记mysql密码如何找回
  11. PulseAudio安装流程
  12. 关于IE主页被篡改成2345、360、hao123等页面的说明
  13. 程序人生之项目团队那些人与事(1)
  14. json解析教程(1)程序员不得不掌握的数据格式json
  15. Spirent Testcenter基本配置使用说明_1022
  16. 数据库实战20_获取所有员工的emp_no、部门编号dept_no以及对应的bonus类型btype和received,没有分配奖金的员工不显示对应的bonus类型btype和received
  17. 招生通知+4,北京大学计算机学院+中国科学技术大学信息技术学院+吉林大学人工智能学院+深圳大学计算机学院
  18. 编程实现AdaBoost算法
  19. 变革的腾讯:一个游戏之外的帝国
  20. 大学生简单个人静态HTML网页设计作品 DIV布局个人介绍网页模板代码 DW学生个人网站制作成品下载 明星个人主页介绍(10页) HTML+CSS+JavaScript

热门文章

  1. 淘宝店铺托管打造人群的目的是什么?
  2. 金山词霸无法屏幕取词的解决方法
  3. 由一种颜色得到对应的浅色及深色
  4. 国产网管软件的本土化及其发展
  5. 使用VUE(uniapp)和Spring boot做的小游戏 远古帝国
  6. fedora23_x86_64通过dnf升级到fedora24
  7. Android中定位经纬度问题
  8. jeecgboot vue2启动后台报错 jeecgboot ERROR o.s.d.redis.listener.RedisMessageListenerContainer:665 - Connec
  9. Java学习笔记-Day42 HTML概述
  10. 服务器高并发处理/服务器宕机了怎么处理?