在应用程序当中,有时往往需要去获取到一些系统相关的信息,譬如时间、日期、以及其它一些系统相关信息,本章将向大家介绍如何通过 Linux 系统调用或 C 库函数获取系统信息,譬如获取系统时间、日期以及设置系统时间、日期等;除此之外,还会向大家介绍 Linux 系统下的/proc 虚拟文件系统,包括/proc 文件系统是什么以及如何从/proc 文件系统中读取系统、进程有关信息。
除了介绍系统信息内容外,本章还会向大家介绍有关系统资源的使用,譬如系统内存资源的申请与使用等。好了,废话不多少,开始本章内容的学习吧!
用于获取系统相关信息的函数;
使程序进入休眠;
在堆中申请内存;
proc 文件系统介绍;
1、系统信息
1.1 系统标识 uname
系统调用 uname()用于获取有关当前操作系统内核的名称和信息,函数原型如下所示(可通过"man 2 uname"命令查看):
#include <sys/utsname.h>
int uname(struct utsname *buf);
使用该函数需要包含头文件<sys/utsname.h>。
函数参数和返回值含义如下:
buf:struct utsname 结构体类型指针,指向一个 struct utsname 结构体类型对象。
返回值:成功返回 0;失败将返回-1,并设置 errno。
uname()函数用法非常简单,先定义一个 struct utsname 结构体变量,调用 uname()函数时传入变量的地址即可,struct utsname 结构体如下所示:
struct utsname {char sysname[]; /* 当前操作系统的名称 */char nodename[]; /* 网络上的名称(主机名) */char release[]; /* 操作系统内核版本 */char version[]; /* 操作系统发行版本 */char machine[]; /* 硬件架构类型 */#ifdef _GNU_SOURCEchar domainname[];/* 当前域名 */#endif
};
可以看到,struct utsname 结构体中的所有成员变量都是字符数组,所以获取到的信息都是字符串。
1.2 sysinfo 函数
sysinfo 系统调用可用于获取一些系统统计信息,其函数原型如下所示:
#include <sys/sysinfo.h>
int sysinfo(struct sysinfo *info);
函数参数和返回值含义如下:
info:struct sysinfo 结构体类型指针,指向一个 struct sysinfo 结构体类型对象。
返回值:成功返回 0;失败将返回-1,并设置 errno。
struct sysinfo {long uptime; /* 自系统启动之后所经过的时间(以秒为单位) */unsigned long loads[3]; /* 1, 5, and 15 minute load averages */unsigned long totalram; /* 总的可用内存大小 */unsigned long freeram; /* 还未被使用的内存大小 */unsigned long sharedram; /* Amount of shared memory */unsigned long bufferram; /* Memory used by buffers */unsigned long totalswap; /* Total swap space size */unsigned long freeswap; /* swap space still available */unsigned short procs; /* 系统当前进程数量 */unsigned long totalhigh; /* Total high memory size */unsigned long freehigh; /* Available high memory size */unsigned int mem_unit; /* 内存单元大小(以字节为单位) */char _f[20-2*sizeof(long)-sizeof(int)]; /* Padding to 64 bytes */
};
1.3 gethostname 函数
此函数可用于单独获取 Linux 系统主机名,与 struct utsname 数据结构体中的 nodename 变量一样,gethostname 函数原型如下所示(可通过"man 2 gethostname"命令查看):
#include <unistd.h>
int gethostname(char *name, size_t len);
函数参数和返回值含义如下:
name:指向用于存放主机名字符串的缓冲区。
len:缓冲区长度。
返回值:成功返回 0,;失败将返回-1,并会设置 errno。
1.4 sysconf()函数
sysconf()函数是一个库函数,可在运行时获取系统的一些配置信息,譬如页大小(page size)、主机名的最大长度、进程可以打开的最大文件数、每个用户 ID 的最大并发进程数等。其函数原型如下所示:
#include <unistd.h>
long sysconf(int name);
调用 sysconf()函数获取系统的配置信息,参数 name 指定了要获取哪个配置信息,参数 name 可取以下任何一个值(都是宏定义,可通过 man 手册查询):
_SC_ARG_MAX:exec 族函数的参数的最大长度,exec 族函数后面会介绍,这里先不管!
_SC_CHILD_MAX:每个用户的最大并发进程数,也就是同一个用户可以同时运行的最大进程数。
_SC_HOST_NAME_MAX:主机名的最大长度。
_SC_LOGIN_NAME_MAX:登录名的最大长度。
_SC_CLK_TCK:每秒时钟滴答数,也就是系统节拍率。
_SC_OPEN_MAX:一个进程可以打开的最大文件数。
_SC_PAGESIZE:系统页大小(page size)。
_SC_TTY_NAME_MAX:终端设备名称的最大长度。
……
除以上之外,还有很多,这里就不再一一列举了,可以通过 man 手册进行查看,用的比较多的是_SC_PAGESIZE 和_SC_CLK_TCK,在后面章节示例代码中有使用到。
若指定的参数 name 为无效值,则 sysconf()函数返回-1,并会将 errno 设置为 EINVAL。否则返回的值便是对应的配置值。注意,返回值是一个 long 类型的数据。
2、时间、日期
2.1 Linux 系统中的时间
点时间和段时间
通常描述时间有两种方式:点时间和段时间;点时间顾名思义指的是某一个时间点,譬如当前时间是2021 年 2 月 22 日星期一 11:12 分 35 秒,所以这里指的就是某一个时间点;而对于段时间来说,顾名思义指的是某一个时间段,譬如早上 8:00 到中午 12:00 这段时间。
实时时钟 RTC
操作系统中一般会有两个时钟,一个系统时钟(system clock),一个实时时钟(Real time clock),也叫 RTC;系统时钟由系统启动之后由内核来维护,譬如使用 date 命令查看到的就是系统时钟,所以在系统关机情况下是不存在的;而实时时钟一般由 RTC 时钟芯片提供,RTC 芯片有相应的电池为其供电,以保证系统在关机情况下 RTC 能够继续工作、继续计时。
Linux 系统如何记录时间
Linux 系统在开机启动之后首先会读取 RTC 硬件获取实时时钟作为系统时钟的初始值,之后内核便开始维护自己的系统时钟。所以由此可知,RTC 硬件只有在系统开机启动时会读取一次,目的是用于对系统时钟进行初始化操作,之后的运行过程中便不会再对其进行读取操作了。
而在系统关机时,内核会将系统时钟写入到 RTC 硬件、已进行同步操作。
jiffies 的引入
jiffies 是内核中定义的一个全局变量,内核使用 jiffies 来记录系统从启动以来的系统节拍数,所以这个变量用来记录以系统节拍时间为单位的时间长度,Linux 内核在编译配置时定义了一个节拍时间,使用节拍率(一秒钟多少个节拍数)来表示,譬如常用的节拍率为 100Hz(一秒钟 100 个节拍数,节拍时间为 1s /100)、200Hz(一秒钟 200 个节拍,节拍时间为 1s / 200)、250Hz(一秒钟 250 个节拍,节拍时间为 1s /250)、300Hz(一秒钟 300 个节拍,节拍时间为 1s / 300)、500Hz(一秒钟 500 个节拍,节拍时间为 1s /500)等。由此可以发现配置的节拍率越低,每一个系统节拍的时间就越短,也就意味着 jiffies 记录的时间精度越高,当然,高节拍率会导致系统中断的产生更加频繁,频繁的中断会加剧系统的负担,一般默认情况下都是采用 100Hz 作为系统节拍率。
内核其实通过 jiffies 来维护系统时钟,全局变量 jiffies 在系统开机启动时会设置一个初始值,上面也给大家提到过,RTC 实时时钟会在系统开机启动时读取一次,目的是用于对系统时钟进行初始化,这里说的初始化其实指的就是对内核的 jiffies 变量进行初始化操作,具体如何将读取到的实时时钟换算成 jiffies 数值,这里便不再给大家介绍了。
所以由此可知,操作系统使用 jiffies 这个全局变量来记录当前时间,当我们需要获取到系统当前时间点时,就可以使用 jiffies 变量去计算,当然并不需要我们手动去计算,Linux 系统提供了相应的系统调用或 C库函数用于获取当前时间,譬如系统调用 time()、gettimeofday(),其实质上就是通过 jiffies 变量换算得到。
2.2 获取时间 time
系统调用 time()用于获取当前时间,以秒为单位,返回得到的值是自 1970-01-01 00:00:00 +0000 (UTC)以来的秒数,其函数原型如下所示(可通过"man 2 time"命令查看):
#include <time.h>
time_t time(time_t *tloc);
函数参数和返回值含义如下:
tloc:如果 tloc 参数不是 NULL,则返回值也存储在 tloc 指向的内存中。
返回值:成功则返回自 1970-01-01 00:00:00 +0000 (UTC)以来的时间值(以秒为单位);失败则返回-1,并会设置 errno。
3、产生随机数
在应用编程当中可能会用到随机数,譬如老板让你编写一个抽奖的小程序,编号 0~100,分为特等奖 1个、一等奖 2 个、二等级 3 以及三等级 4 个,也就是说需要从 0~100 个编号中每次随机抽取一个号码,这就需要用到随机数。那在 Linux 应用编程中如何去产生随机数呢?本小节就来学习生成随机数。
随机数与伪随机数
随机数是随机出现,没有任何规律的一组数列。在我们编程当中,是没有办法获得真正意义上的随机数列的,这是一种理想的情况,在我们的程序当中想要使用随机数列,只能通过算法得到一个伪随机数序列,那在编程当中说到的随机数,基本都是指伪随机数。
C 语言函数库中提供了很多函数用于产生伪随机数,其中最常用的是通过 rand()和 srand()产生随机数,本小节就以这两个函数为例向大家介绍如何在我们的程序中获得随机数列。
rand 函数
rand()函数用于获取随机数,多次调用 rand()可得到一组随机数序列,其函数原型如下:
#include <stdlib.h>
int rand(void);
函数参数和返回值含义如下:
返回值:返回一个介于 0 到 RAND_MAX(包含)之间的值,也就是数学上的[0, RAND_MAX]。
程度当中调用 rand()可以得到[0, RAND_MAX]之间的伪随机数,多次调用 rand()便可以生成一组伪随机树序列,但是这里有个问题,就是每一次运行程序所得到的随机数序列都是相同的,那如何使得每一次启动应用程序所得到的随机数序列是不一样的呢?那就通过设置不同的随机数种子,可通过 srand()设置随机数种子。
srand 函数
使用 srand()函数为 rand()设置随机数种子,其函数原型如下所示:
#include <stdlib.h>
void srand(unsigned int seed);
函数参数和返回值含义如下:
seed:指定一个随机数中,int 类型的数据,一般尝尝将当前时间作为随机数种子赋值给参数 seed,譬如 time(NULL),因为每次启动应用程序时间上是一样的,所以就能够使得程序中设置的随机数种子在每次启动程序时是不一样的。
返回值:void
常用的用法 srand(time(NULL));
4、休眠
4.1 秒级休眠: sleep
sleep()是一个 C 库函数,从函数名字面意思便可以知道该函数的作用了,简单地说,sleep()就是让程序
“休息”一会,然后再继续工作。其函数原型如下所示:
#include <unistd.h>
unsigned int sleep(unsigned int seconds);
函数参数和返回值含义如下:
seconds:休眠时长,以秒为单位。
返回值:如果休眠时长为参数 seconds 所指定的秒数,则返回 0;若被信号中断则返回剩余的秒数。
sleep()是一个秒级别休眠函数,程序在休眠过程中,是可以被其它信号所打断的,关于信号这些内容,将会在后面章节向大家介绍。
4.2 微秒级休眠: usleep
usleep()同样也是一个 C 库函数,与 sleep()的区别在于休眠时长精度不同,usleep()支持微秒级程序休眠,其函数原型如下所示:
#include <unistd.h>
int usleep(useconds_t usec);
函数参数和返回值含义如下:
usec:休眠时长,以微秒为单位。
返回值:成功返回 0;失败返回-1,并设置 errno。
4.3 高精度休眠: nanosleep
nanosleep()与 sleep()以及 usleep()类似,都用于程序休眠,但 nanosleep()具有更高精度来设置休眠时间长度,支持纳秒级时长设置。与 sleep()、usleep()不同的是,nanosleep()是一个 Linux 系统调用,其函数原型如下所示:
#include <time.h>
int nanosleep(const struct timespec *req, struct timespec *rem);
函数参数与返回值含义如下:
req:一个 struct timespec 结构体指针,指向一个 struct timespec 变量,用于设置休眠时间长度,可精确到纳秒级别。
rem:也是一个 struct timespec 结构体指针,指向一个 struct timespec 变量,也可设置 NULL。
返回值:在成功休眠达到请求的时间间隔后,nanosleep()返回 0;如果中途被信号中断或遇到错误,则返回-1,并将剩余时间记录在参数 rem 指向的 struct timespec 结构体变量中(参数 rem 不为 NULL 的情况下,如果为 NULL 表示不接收剩余时间),还会设置 errno 标识错误类型。
5、申请堆内存
在操作系统下,内存资源是由操作系统进行管理、分配的,当应用程序想要内存时(这里指的是堆内存),可以向操作系统申请内存,然后使用内存;当不再需要时,将申请的内存释放、归还给操作系统;在许多的应用程序当中,往往都会有这种需求,譬如为一些数据结构动态分配/释放内存空间,本小节向大家介绍应用程序如何向操作系统申请堆内存。
5.1 在堆上分配内存:malloc 和 free
Linux C 程序当中一般使用 malloc()函数为程序分配一段堆内存,而使用 free()函数来释放这段内存,先来看下 malloc()函数原型,如下所示:
#include <stdlib.h>
void *malloc(size_t size);
函数参数和返回值含义如下:
size:需要分配的内存大小,以字节为单位。
返回值:返回值为 void *类型,如果申请分配内存成功,将返回一个指向该段内存的指针,void *并不是说没有返回值或者返回空指针,而是返回的指针类型未知,所以在调用 malloc()时通常需要进行强制类型转换,将 void *指针类型转换成我们希望的类型;如果分配内存失败(譬如系统堆内存不足)将返回 NULL,如果参数 size 为 0,返回值也是 NULL。
#include <stdlib.h>
void free(void *ptr);
函数参数和返回值含义如下:
ptr:指向需要被释放的堆内存对应的指针。
返回值:无返回值。
调用 free()还是不调用 free()
在学习文件 IO 基础章节内容时曾向大家介绍过,Linux 系统中,当一个进程终止时,内核会自动关闭它没有关闭的所有文件(该进程打开的文件,但是在进程终止时未调用 close()关闭它)。同样,对于内存来
说,也是如此!当进程终止时,内核会将其占用的所有内存都返还给操作系统,这包括在堆内存中由 malloc()函数所分配的内存空间。基于内存的这一自动释放机制,很多应用程序通常会省略对 free()函数的调用。
这在程序中分配了多块内存的情况下可能会特别有用,因为加入多次对 free()的调用不但会消耗品大量的 CPU 时间,而且可能会使代码趋于复杂。
虽然依靠终止进程来自动释放内存对大多数程序来说是可以接受的,但最好能够在程序中显式调用
free()释放内存,首先其一,显式调用 free()能使程序具有更好的可读性和可维护性;其二,对于很多程序来说,申请的内存并不是在程序的生命周期中一直需要,大多数情况下,都是根据代码需求动态申请、释放的,如果申请的内存对程序来说已经不再需要了,那么就已经把它释放、归还给操作系统,如果持续占用,将会导致内存泄漏,也就是人们常说的“你的程序在吃内存”!
6、proc 文件系统
proc 文件系统是一个虚拟文件系统,它以文件系统的方式为应用层访问系统内核数据提供了接口,用户和应用程序可以通过 proc 文件系统得到系统信息和进程相关信息,对 proc 文件系统的读写作为与内核进行通信的一种手段。但是与普通文件不同的是,proc 文件系统是动态创建的,文件本身并不存在于磁盘当中、只存在于内存当中,与 devfs 一样,都被称为虚拟文件系统。
最初构建 proc 文件系统是为了提供有关系统中进程相关的信息,但是由于这个文件系统非常有用,因此内核中的很多信息也开始使用它来报告,或启用动态运行时配置。内核构建 proc 虚拟文件系统,它会将内核运行时的一些关键数据信息以文件的方式呈现在 proc 文件系统下的一些特定文件中,这样相当于将一些不可见的内核中的数据结构以可视化的方式呈现给应用层。
proc 文件系统挂载在系统的/proc 目录下,对于内核开发者(譬如驱动开发工程师)来说,proc 文件系统给了开发者一种调试内核的方法:通过查看/proc/xxx 文件来获取到内核特定数据结构的值,在添加了新功能前后进行对比,就可以判断此功能所产生的影响是否合理。
/proc 目录下中包含了一些目录和虚拟文件,如下所示:
可以看到/proc 目录下有很多以数字命名的文件夹,譬如 100038、2299、98560,这些数字对应的其实就是一个一个的进程 PID 号,每一个进程在内核中都会存在一个编号,通过此编号来区分不同的进程,这个编号就是 PID 号,关于 PID、以及进程相关的信息将会在后面章节内容中向大家介绍。
所以这些以数字命名的文件夹中记录了这些进程相关的信息,不同的信息通过不同的虚拟文件呈现出来,关于这些信息将会在后面章节内容中向大家介绍。
/proc 目录下除了文件夹之外,还有很多的虚拟文件,譬如 buddyinfo、cgroups、cmdline、version 等等,不同的文件记录了不同信息,关于这些文件记录的信息和意思如下:
cmdline:内核启动参数;
cpuinfo:CPU 相关信息;
iomem:IO 设备的内存使用情况;
interrupts:显示被占用的中断号和占用者相关的信息;
ioports:IO 端口的使用情况;
kcore:系统物理内存映像,不可读取;
loadavg:系统平均负载;
meminfo:物理内存和交换分区使用情况;
modules:加载的模块列表;
mounts:挂载的文件系统列表;
partitions:系统识别的分区表;
swaps:交换分区的利用情况;
version:内核版本信息;
uptime:系统运行时间;