UNIX基础知识:UNIX体系结构、登录、文件和目录、输入和输出、程序和进程、出错处理、用户标识、信号、时间值、系统调用和库函数

引言:

所有的操作系统都为运行在其上的程序提供服务,比如:执行新程序、打开文件、读写文件、分配存储区、获得系统当前时间等等

1. UNIX体系结构

从严格意义上来说,操作系统可被定义为一种软件,它控制计算机硬件资源,提供程序运行的环境。我们通常将这种软件称为内核(kernel),因为它相对较小,而且位于环境的核心。图1-1 显示了 UNIX 操作系统的体系结构。

内核的接口被称为系统调用(system call),公用函数库构建在系统调用接口之上,应用程序可以使用公用函数库提供的接口,也可以使用内核提供的接口(系统调用)。shell 是一个特殊的应用程序,为运行其他应用程序提供了一个接口。从广义上说,操作系统包括内核和一些其他软件,这些软件使得计算机能够发挥作用,并使计算机具有自己的特性。这里所说的其他软件包括系统实用程序(system utility)、shell、公用函数库以及应用程序等。Linux 是 GUN 操作系统使用的内核,一些人将这种操作系统称为 GUN/Linux 操作系统,但是更常见的是简单地称其为 Linux。

2. 登录

2.1 用户名

用户在登录 UNIX 系统时,输入用户名,然后输入用户密码。系统将在其口令文件(通常是 /etc/passwd 文件)中查看登录名。口令文件中的登录项由 7 个以冒号(:) 分隔的字段组成,依次是【登录名:加密口令:用户id:用户组id:注释字段:用户登录进入的起始工作目录:shell 程序路径】

在 shell 终端下输入以下命令查看 /etc/passwd 文件的内容进行验证:

bash 复制代码
cat /etc/passwd
bash 复制代码
用户名:root
加密口令:x
用户id:0
用户组id:0
注释字段:root
用户登录进入的起始工作目录:/root
用户的 shell 程序路径:/bin/bash

加密口令:x 是一个占位符,较早期的 UNIX 系统版本中,该字段存放加密口令字。将加密口令字存放在一个人人可读的文件中是一个安全漏洞,所以现在将加密口令字存放在另一个文件中。第6章将说明这种文件以及访问它们的函数。

2.2 shell

用户登录后,系统通常先显示一些系统信息,然后用户就可以向 shell 程序输入命令。shell 是一个命令行解释器,它读取用户输入,然后执行命令。shell 的用户输入通常来自终端(交互式 shell),有时则来自于文件(称为 shell 脚本)。

3. 文件和目录

3.1 文件系统

UNIX 文件系统是目录和文件的一种层次结构,所有东西的起点是根目录(root),这个目录的名称是一个字符 "/"。

目录是一个包含目录项的文件。在逻辑上,可以认为每个目录项都包含一个文件名,同时还包含说明该文件属性的信息。文件属性是指文件类型(普通文件还是目录等)、文件大小,文件所有者、文件权限(其他用户是否有访问该文件的权限)以及文件的最后修改时间等。

3.2 文件名

文件的名字称为文件名(filename),只有斜线(/)和 空字符这两个字符不能出现在文件名中。斜线用来分隔构成路径名的,空字符则用来终止一个路径名。

创建新目录时会自动创建两个文件名:. (点)和 ..(点点) 。点指向当前目录,点点指向父目录。最高层次的根目录中,点和点点都指向当前目录。

3.3 路径名

由斜线分隔的一个或多个文件名的序列(也可以斜线开头)构成路径名(pathname)。以斜线开头的路径名称为绝对路径(absolute pathname),否则称为相对路径(relative pathname),相对路径名指向相对于当前目录的文件。文件系统根的名字(/)是一个特殊绝对路径名,它不包含文件名。

以下代码功能:列出一个目录中所有文件的名字,ls 命令的简要实现

err.h

cpp 复制代码
#ifndef ERR_H_
#define ERR_H_

#include <stdarg.h>

#define MAX_BUF 4096

// __attribute__((noreturn)) 属性,告诉编译器这个函数永远不会有返回值,避免当函数有返回值,在某种条件下未能执行到返回值代码处,编译器警告或报错
__attribute__((noreturn)) void err_quit(const char *format, ...);
__attribute__((noreturn)) void err_sys(const char *format, ...);
void err_ret(const char *format, ...);

#endif /*ERR_H_*/

err.c

cpp 复制代码
#include "err.h"

#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

static void err_doit(const int errno_flag, const int errorno, const char *format, va_list ap);

static void err_doit(const int errno_flag, const int errorno, const char *format, va_list ap)
{
    char buf[MAX_BUF] = "";

    vsnprintf(buf, MAX_BUF-1, format, ap); // 将可变参数列表格式化到字符串中
    if (errno_flag)
        snprintf(buf+strlen(buf), MAX_BUF-strlen(buf)-1, ": %s", strerror(errorno));

    strcat(buf, "\n"); // 将参数字符串 "\n" 拷贝到参数 buf 所指的字符串尾。buf 要有足够的空间来容纳要拷贝的字符串。
    fflush(stdout); // 刷新 stdout
    fputs(buf, stderr); // 将 buf 所指的字符串写入到 strerr
    fflush(NULL); // 参数为 NULL, fflush() 会刷新所有 stdio
}

void err_quit(const char *format, ...)
{
    va_list ap;
    va_start(ap, format); // 获取可变参数列表的第一个参数的地址
    err_doit(0, 0, format, ap);
    va_end(ap); // 清空va_list可变参数列表
    exit(1);
}

void err_sys(const char *format, ...)
{
    va_list ap;
    va_start(ap, format);
    err_doit(1, errno, format, ap);
    va_end(ap);
    exit(1);
}

void err_ret(const char *format, ...)
{
    va_list ap;
    va_start(ap, format);
    err_doit(1, errno, format, ap);
    va_end(ap);
}

list_dir_filename.c

cpp 复制代码
#include "../common/err.h"

#include <dirent.h>
#include <stdio.h>

int main(int argc, char *argv[])
{
    DIR *dirp = NULL;
    struct dirent *direntp = NULL;

    if (argc != 2)
        err_quit("ls dir_name");
    
    /* 打开目录 */
    if ((dirp = opendir(argv[1])) == NULL)
        err_sys("can't open %s", argv[1]);

    /* 逐一读取目录条目 */
    while ((direntp = readdir(dirp)) != NULL)
        printf("%s\n", direntp->d_name);
    
    /* 关闭目录 */
    closedir(dirp);

    return 0;
}

3.4 工作目录

每个进程都有一个工作目录(working directory),有时称为当前工作目录(current working directory)。所有相对路径名都从工作目录开始解释的。进程可以用 chdir() 函数更改其工作目录。

例如,相对路径名 unix_env_program/chapter_one/test 指的是当前工作目录中的 unix_env_program 目录中的 chapter_one 目录中的 test 文件(或目录)。从路径名可以看出 unix_env_program 和 chapter_one 是目录,但是不能分辨 test 是文件还是目录。/etc/passwd 是一个绝对路径名,它指的是根目录下的 etc 目录中的 passwd 文件(或目录)

3.5 起始目录

登录时,工作目录设置为起始目录(home directory),起始目录从口令文件(/etc/passwd)中相应用户的登录项中获得。

4. 输入和输出

4.1 文件描述符

文件描述符(file descriptor)通常是一个小的非负整数,内核用以标识一个特定进程正在访问的文件。当内核打开一个现有的文件或创建一个新文件时,它都会返回一个文件描述符。在读写这个文件时,可以使用这个文件描述符。

4.2 标准输入、标准输出和标准错误

每当运行一个新程序时,所有的 shell 都为其打开 3 个文件描述符,即标准输入(standard input)、标准输出(standard output)和标准错误(standard error),STDIN_FILENO(0)、 STDOUT_FILENO(1)和 STDERR_FILENO(2)为 3 个常量,定义在 #include <unistd.h> 头文件中,它们指向了标准输入、标准输出和标准出错的文件描述符。如果不做特殊处理,例如就像简单的命令 ls,则这 3 个文件描述符都链接向终端。大多数 shell 都提供一种方法,使其中任何一个或所有这 3 个文件描述符都能重新定向到某个文件,例如:

ls > file.list // 执行 ls 命令,将其标准输出重新定向到名为 file.list 的文件中。

4.3 不带缓冲的 I/O

函数 open()、read()、write()、lseek() 以及 close() 提供了不带缓冲的 I/O,这些函数操作的都是文件描述符。

以下代码功能:从标准输入中读,并向标准输出中写,用于复制任一个 UNIX 普通文件

stdin_r_stdout_w.c

cpp 复制代码
#include "../common/err.h"

#include <unistd.h>

#define BUF_SIZE 4096

int main(int argc, char *argv[])
{
    int nread = 0;
    char buf[BUF_SIZE] = "";
    
    while ((nread = read(STDIN_FILENO, buf, BUF_SIZE)) > 0) // 从标准输入文件描述符中读
    {
        if (write(STDOUT_FILENO, buf, nread) != nread) // 写入标准输出文件描述符中
            err_sys("write error");
    }

    if (nread < 0)
        err_sys("read error");

    return 0;
}

将程序编译成 stdin_r_stdout_w 可执行文件,在 shell 终端输入以下命令

bash 复制代码
$./stdin_r_stdout_w > data.stdout // 标准输入是终端,标准输出重定向到 data.stdout 文件,标准出错也是终端

键盘输入:111,再换行,再输入:222,再换行,再按下文件结束符(ctrl +d),将终止本次复制。

在 shell 终端输入以下命令

bash 复制代码
$ ./stdin_r_stdout_w < data.stdout > newdata.stdout
// 标准输入重定向为 data.stdout 文件,标准输出重定向为 newdata.stdout 文件,标准出错是终端

此时,是将 data.stdout 文件复制一份,并命名为 newdata.stdout

4.4 标准 I/O

标准 I/O 函数为那些不带缓冲的 I/O 函数提供了一个带缓冲的接口。使用标准 I/O 函数无需担心如何选取最佳的缓冲区大小。使用标准的 I/O 函数还简化了对输入行的处理。例如,fgets() 函数读取一个完整的行,而 read() 函数读取指定的字节数。我们最熟悉的标准 I/O 函数是 printf(),在调用 printf() 函数的程序中,总是要 #include <stdio.h> ,该头文件包括了所有标准 I/O 函数的原型。

标准 I/O 操作的对象是文件指针:FILE*

特殊的文件指针:stdin、stdout、stderr

以下代码功能:从标准输入复制到标准输出,也是用于复制任一个 UNIX 普通文件

stdin_copy_to_stdout.c

cpp 复制代码
#include "../common/err.h"

#include <stdio.h>

int main(int argc, char *argv[])
{
    int c = 0;
    
    while ((c = getc(stdin)) != EOF) // 一次读取一个字符,直到读入最后的一个字节时,返回 EOF
    {
        if (putc(c, stdout) == EOF) // 将字符写入到标准输出
            err_sys("output error");
    }

    if (ferror(stdin))
        err_sys("input error");
    
    return 0;
}

常量 EOF ,标准 I/O 常量 标准输入(stdin)和 标准输出(stdout)都在 #include <stdio.h> 头文件中定义。

5. 程序和进程

5.1 程序

程序(program)是一个存储在磁盘上某个目录中的可执行文件。内核使用 exec() 函数,将程序读入内存,并执行程序。

5.2 进程和进程ID

程序的执行实例被称为进程(progress)。UNIX 系统确保每个进程都有一个唯一的数字标识符,称为进程ID(progress ID)。进程ID 总是为一个非负整数。

以下代码功能:打印进程ID

print_pid.c

cpp 复制代码
#include <unistd.h>
#include <stdio.h>

int main(int argc, char *argv[])
{
    printf("pid = %ld\n", getpid());

    return 0;
}

5.3 进程控制

fork()、exec() 和 waitpid() 是 3 个主要用于进程控制的主要函数。

以下代码功能:从标准输入中读取命令,然后执行这些命令,类似于 shell 程序的基本实施部分。

execlp_demo.c

cpp 复制代码
#include "../common/err.h"

#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>
#include <string.h>
#include <stdlib.h>

int main(int argc, char *argv[])
{
    char buf[MAX_BUF] = "";
    pid_t pid = 0;
    int status = 0;

    printf("%%");
    while ((fgets(buf, MAX_BUF, stdin)) != NULL) // 从标准输入 stdin 中读取一行
    {
        if (buf[strlen(buf) - 1] == '\n') // 最后一个是换行符时
            buf[strlen(buf) - 1] = 0; // 替换成 NULL,因为 execlp() 函数的参数要求字符串必须要以字符串结束符('\0')结尾

        if ((pid = fork()) < 0) {
            err_sys("fork error");
        } else if (pid == 0) { // 子进程
            /*
            int execlp(const char *file, const char *arg, ...);
            (1)execlp 函数名中 l 是 list,说明这个函数的参数是可变参数列表的意思
            (2)execlp 函数名中 p 是环境变量 $PATH 的意思,说明,这个函数的第一个参数可以是一个文件名,这时函数会从环境变量中去搜索这个文件的
                完整路径名;这个函数的第一个参数也可以是绝对路径或相对路径,这时函数不会从环境变量中去搜索这个文件的完整路径名
            (3)第二个参数 arg 是命令行起始地址
             (4) 传递给这个函数的最后一个参数必须为
            */ 
            execlp(buf, buf, (char *)NULL); // 执行读取的命令
            err_ret("can't exec: %s", buf); // 如果程序能跑到这里,说明执行 execlp() 函数出错了
            exit(127); // 子进程退出
        }

        /*
        pid_t waitpid(pid_t pid, int *status, int options);
        默认情况下 (当 options=0 时 ),waitpid挂起调用进程的执行,直到它的等待集合 (wait set) 中的一个子进程终止。如果等待集合中的一个进程
        在刚调用的时刻就已经 终止了,那么 waitpid 就立即返回 。在这两种情况中,waitpid返回导致 waitpid 返回的已终止子进程的PID此时,已终止
        的子进程已经被回收,内核会从系统中删除掉它的所有痕迹。
        */
        // 父进程
        if ((pid = waitpid(pid, &status, 0)) < 0) // 子进程退出时,父进程回收子进程的资源,options=0,父进程被挂起,直到子进程退出,waitpid 立即返回
            err_sys("waitpid error");
        
        printf("%%");
    }

    return 0;
}

5.4 线程和线程ID

通常,一个进程只有一个控制线程(thread)-- 某一个时刻执行的一组机器指令。对于某些问题,如果有多个控制线程分别作用于它的不同部分,那么解决起来就容易的多。另外,多个控制线程也可以充分利用多核处理器的并行执行任务的能力。

一个进程内的所有线程共享同一个地址空间、文件描述符、栈以及与进程相关的属性。因为它们能访问同一个存储区,所以各线程在访问共享数据时需要采取同步措施以避免数据不一致的问题。

和进程相同,线程也有 ID 标识。但是,线程ID 只在它所属的进程内起作用。一个进程的线程ID 在另一个进程中没有意义。当在一个进程中对某个特定线程进行处理时,我们可以使用该线程的 ID 引用它。

6. 出错处理

6.1 errno 以及出错打印函数 strerror() 和 perror()

当 UNIX 系统函数出错时,通常会返回一个负值,而且整型变量 errno 通常被设置为具有特定出错信息的值。例如,open() 函数如果成功执行则返回一个非负文件描述符,如果出错则返回 -1,errno 将会被设置,通过调用 char *str = strerror(errno); 获取到出错信息。而有些函数出错则使用另一种约定而不是返回一个负值。例如,大多数返回指向对象指针的函数,在出错时会返回一个 NULL 指针。

头文件 <errno.h> 中定义了 errno 以及赋予它的各种常量。这些常量都以字符 E 开头。在 linux 操作系统中,出错常量在 man 3 errno 手册页查看

POSIX 和 ISO C 将 errno 定义为一个符号,它扩展成为一个可修改的整型左值(lvalue),它可以是一个包含出错编号的整数,也可以是一个返回出错编号指针的函数。以前使用的定义是:extern int errno;

但在支持线程的环境中,多个线程共享进程地址空间,每个线程都有属于它自己的局部 errno,以避免一个线程干扰另一个线程。linux 操作系统支持多线程存取 errno,将其定义为:

extern int* __errno_location(void);

#define errno *(__errno_location())

对于 errno 应当注意两条规则。

(1)如果没有出错,其值不会被例程清除,因此,仅当函数的返回值指明出错时,才检验其值。

(2)任何函数都不会将 errno 的值设置为 0,而且在 <errno.h> 中定义的所有常量都不为 0。

C 标准定义了两个函数,用于打印出错信息

cpp 复制代码
#include <string.h>
char* strerror(int errnum); // 返回值指向出错信息字符串的指针

strerror() 函数将 errnum(通常就是一个 errno 值)映射为一个出错信息字符串,并且返回此出错信息字符串的指针。

cpp 复制代码
#include <stdio.h>
void perror(const char *msg);

perror 函数基于 errno 的当前值,在标准错误上参生一条出错信息,然后返回。它输出的格式为:

参数 msg 指向的字符串,然后是一个冒号,一个空格,接着是 errno 值对应的出错信息字符串,最后是一个换行符。

以下代码功能:展示 strerror() 和 perror() 函数的使用方法

strerror_perror.c

cpp 复制代码
#include <stdio.h>
#include <errno.h>
#include <string.h>

int main(int argc, char *argv[])
{
    fprintf(stderr, "EACCES: %s\n", strerror(EACCES));
    errno = ENOENT;
    perror(argv[0]);

    return 0;
}

注意:我们将程序名(argv[0])作为参数传递给 perror() 函数,这是一个标准的 UNIX 惯例,使用这种方法,在程序作为管道的一部分执行时,例如:

prog1 < inputfile | prog2 | prog3 > outputfile

我们就能够分清 3 个程序中哪一个产生了一条特定的出错信息。

6.2 出错恢复

可将在 <errno.h> 中定义的各种出错分成两类:致命性的和非致命性的。

对于致命性的错误,无法执行恢复动作。最多能做的是在用户屏幕上打印一条出错消息或将一条出错消息写入日志文件中,然后退出。

对于非致命性出错,有时可以较为妥善地进行处理。大多数非致命性出错是短暂的(如资源短缺),当系统中活动较少时,这种出错很可能不会发生。

与资源相关的非致命性出错包括:

EAGAIN 资源暂时不可用(可能与 EWOULDBLOCK 的值相同)

ENFILE 系统中打开的文件太多

ENOBUFS 没有可用的缓冲空间

ENOLCK 没有可用的锁

ENOSPC 设备上没有剩余空间

EWOULDBLOCK 操作将阻塞(可能与 EAGAIN 的值相同)

ENOMEM 没有足够的空间。有时也是非致命性出错。

EBUSY 设备或资源忙。当指明共享资源正在使用时,也可将它作为非致命性的出错处理。

EINTR 中断函数调用。当中断一个慢速系统调用时,也可将它作为非致命性出错处理。

对于资源相关的非致命性出错的典型恢复操作是延迟一段时间,然后重试。这种技术可应用于其他情况,例如,假设出错表明一个网络连接不再起作用,那么应用程序可以采取这种方法,在短时间延迟后,尝试重新连接。一些应用使用指数补偿算法,在每次迭代中等待更长时间。

最终,由应用的开发者决定在哪些情况下应用程序可以从出错中恢复。如果能够采用一种合理的恢复策略,那么可以避免应用程序异常终止,进而就能改善应用程序的健壮性。

7. 用户标识

7.1 用户ID

口令文件登录项中的用户ID(user ID)是一个数值,系统用它来标识各个不同的用户。系统管理员在确定一个用户的登录名的同时,确认其用户ID。用户不能更改其用户ID。通常每个用户有一个唯一的用户ID。下面将介绍内核如何使用用户ID 来检验该用户是否有执行某些操作的权限。

用户ID为 0 的用户为根用户(root)或超级用户(superuser)。在口令文件中,有一个登录项,其登录名为 root,我们称这种用户的特权为超级用户特权。如果一个进程具有超级用户特权,则大多数文件权限检查都不再进行。

7.2 用户的组ID

口令文件登录项也包括用户的组ID(group ID),它是一个数值。组ID 也是由系统管理员在指定用户登录名时分配的。一般来说,在口令文件中有多个登录项具有相同的组ID。组被用于将一个或多个用户集合到项目或部门中去,这种机制允许同组的各种成员之间共享资源(如文件)。组文件将组名映射为数值的组ID。组文件通常是 /etc/group。

使用数值的用户ID 和数值的组ID 设置权限是历史上形成的。对于磁盘上的每个文件,文件系统都存储该文件所有者的用户ID 和组ID。存储这两个值只需 4 个字节(假定每个都以双字节的整形数存放)。如果使用完整 ASCII 登录名和组名,则需要更多的磁盘空间。另外,在检验权限期间,比较字符串比比较整形数更消耗时间。

但是,对于用户而言,使用名字比使用数值方便,所以口令文件中包含了登录名和用户名ID 直接的映射关系,而组文件中包含了组名跟组ID 之间的映射关系。例如,ls -l 命令使用口令文件将数值的用户ID映射为登录名,从而打印出文件所有者的登录名。

以下程序功能:打印用户ID 和组ID

print_uid_gid.c

cpp 复制代码
#include <stdio.h>
#include <unistd.h>

int main(int argc, char *argv[])
{
    printf("uid = %d, gid = %d\n", getuid(), getgid());
    return 0;
}

7.3 附属组ID

除了口令文件中对一个登录名指定一个组ID外,大多数 UNIX 系统版本还允许一个用户属于另外一些组,登录时,读文件 /etc/group,寻找列有该用户作为其成员的前 16 个记录项就可以得到该用户的附属组ID(supplementary group ID)。POSIX 要求系统至少支持 8个附属组,实际上大多数 UNIX 系统至少支持 16 个附属组。

8. 信号

信号(signal)用于通知进程发生某种情况。例如,某一进程执行除法操作,其除数为 0,则将名为 SIGFPE(浮点异常)的信号发送给该进程。

进程有以下 3 种处理信号的方式。

(1)忽略信号。有些信号表示硬件异常,例如,除以 0 或访问进程地址空间以外的存储单元等,因为这些异常产生的后果不确定,所以不推荐使用这种处理方式。

(2)按系统默认方式处理。对于除数为 0,系统默认处理方式是终止该进程。

(3)提供一个函数,信号发生时调用该函数,这被称为捕捉该信号,通过提供自编的处理函数,我们就能知道什么时候产生了信号,并按期望的方式处理它。

很多情况都会产生信号。终端键盘上有两种产生信号的方法:

(1)中断键(interrupt key,通常是 delete 键或 ctrl+c)和退出键(quit key,通常是 ctrl+\),它们被用于中断当前运行的进程。

(2)另一种产生信号的方法是调用 kill 函数,在一个进程中调用 kill 函数就可以向另一个进程发送一个信号,当然这样做也是有限制的,当向一个进程发送信号时,我们必须是那个进程的所有者或超级用户。

以下代码功能:测试捕捉 SIGINT 信号,即捕捉按下中断键产生的信号

signal_int_demo.c

cpp 复制代码
#include "../common/err.h"

#include <unistd.h>
#include <stdio.h>
#include <sys/wait.h>
#include <signal.h>
#include <string.h>
#include <stdlib.h>

static void sig_interrupt(int signo);

int main(int argc, char *argv[])
{
    char buf[MAX_BUF] = "";
    int status = 0;
    pid_t pid = 0;

    if (signal(SIGINT, sig_interrupt) == SIG_ERR)
        err_sys("signal error");

    printf("%% ");
    while (fgets(buf, MAX_BUF, stdin) != NULL)
    {
        if (buf[strlen(buf)-1] == '\n')
            buf[strlen(buf)-1] = 0;
        
        if ((pid = fork()) < 0)
            err_sys("fork error");
        else if (pid == 0) // 子进程
        {
            execlp(buf, buf, (char*)NULL);
            err_ret("can't exec: %s", buf);
            exit(127);
        }

        // 父进程
        if ((pid = waitpid(pid, &status, 0)) < 0) // 父进程回收子进程资源
            err_sys("waitpid error");

        printf("%% ");
    }

    return 0;
}

void sig_interrupt(int signo)
{
    printf("interrupt\n%% ");
}

9. 时间值

历史上,UNIX 系统使用过两种不同的时间值。

(1)日历时间。该值是自协调世界时(Coordinated Universal Time,UTC),自 1970年1月1日 00:00:00 这个特定时间以来所经过的秒数累计值(早期的手册称 UTC 为格林尼治标准时间)。这些时间值可用于记录文件最近一次的修改时间等。系统基本数据类型 time_t 用于保存这种时间值。

(2)进程时间,也被称为 CPU时间,用以度量进程使用的中央处理器资源。进程时间以时钟滴答计算,每秒钟曾经取为50、60或100个时钟滴答。系统基本数据类型 clock_t 保存这种时间值,用 sysconf 函数可以得到每秒的时钟滴答数。

当度量一个进程的执行时间时,UNIX 系统为一个进程维护了3个进程时间值:

  • 时钟时间
  • 用户CPU时间
  • 系统CPU时间

时钟时间又称为墙上时钟时间(wall clock time),它是进程运行的时间总量,其值与系统中同时运行的进程数有关。

用户CPU时间是执行用户指令所用的时间量。系统CPU时间是为该进程执行内核程序所经历的时间。例如,每当一个进程执行一个系统服务时,如 read() 或 write(),在内核内执行该服务所花费的时间就计入该进程的系统CPU时间。用户CPU时间和系统CPU时间之和常被称为 CPU时间。

要取得任一进程的时钟时间、用户CPU时间和系统CPU时间是很容易的,只要执行命令 time,其参数是要度量其执行时间的命令,例如:

cpp 复制代码
$ cd /usr/include
$ time -p grep _POSIX_SOURCE */*.h > /dev/null

10. 系统调用和库函数

11. 习题

(1)在系统上验证,除根目录外,目录. 和目录.. 是不同的

ls 命令的下面有两个参数:

-i 打印文件或目录的 i 节点编号。

-d 仅打印目录信息,而不是打印目录中所有文件的信息。

(2)分析以下打印进程ID的程序,说明进程ID为 852 和 853 的进程发生了什么?

因为 UNIX 系统是多任务操作系统,在第一次执行程序打印了该程序的进程ID 后,系统中有其它两个新的进程被执行,所以占用了进程ID 为 852 和 853 的进程ID 了,我们再执行程序打印进程ID 时,此时的进程ID 则为 854

(3)perror() 函数的参数是用 const 修饰定义的,而 strerror() 函数的整型参数没有用 const 修饰定义,为什么?

因为 perror() 函数的 msg 参数是一个指针,perror() 函数内部就可以改变这个指针指向的字符串,用 const 修饰后,说明这个指针指向的内容是常量,perror() 函数内部就不能修改这个指针指向的字符串。

而 strerror() 函数的 errnum 参数是一个整数类型,是按值传递,因此即使 strerror() 函数内部对这个参数进行修改,对于函数外是无效的,也就没必要使用 const 属性。

(4)若日历时间存放在带符号的 32 位整型数中,那么到哪一年它会溢出?可以用什么方法扩展溢出浮点数?采用的策略是否与现有的应用相兼容?

2^31 / 365*24*60*60 约等于 68,而日历时间是从1970年1月1日 00:00:00 开始的,所以 2038年会溢出。将 time_t 定义为 64 位整型数就可以了。如果它现在是 32 位整型数,修改为 64 位整型数后,要保证应用程序正常工作,应当对其重新编译。但是还有一个更糟糕的地方,某些文件系统及备份介质是 32 位整型数存放时间的,对于这些同样需要更新,但又需要能兼容旧的格式。

(5)若进程时间存放在带符号的 32 位整形数中,而且每秒为 100 时间滴答,那么经过多少天后,该时间会溢出?

2^31 / 24*60*60*100 约等于 248 天

注:本文大部分内容摘抄自《UNIX环境高级编程》(第3版),少部分内容是自己的操作验证以及写的代码实例,本文只作为学习笔记

相关推荐
客观花絮说3 分钟前
DSC+DW实时+异步搭建部署
运维
大耳朵土土垚9 分钟前
【Linux 】开发利器:深度探索 Vim 编辑器的无限可能
linux·编辑器·vim
极客小张17 分钟前
基于STM32MP157与OpenCV的嵌入式Linux人脸识别系统开发设计流程
linux·stm32·单片机·opencv·物联网
x66ccff22 分钟前
【linux】4张卡,坏了1张,怎么办?
linux·运维·服务器
jjb_23642 分钟前
LinuxC高级作业2
linux·bash
OH五星上将1 小时前
OpenHarmony(鸿蒙南向开发)——小型系统内核(LiteOS-A)【扩展组件】上
linux·嵌入式硬件·harmonyos·openharmony·鸿蒙开发·liteos-a·鸿蒙内核
隔窗听雨眠1 小时前
基于Prometheus和Grafana的现代服务器监控体系构建
服务器
拾光师1 小时前
linux之网络命令
linux·服务器·网络
毕设木哥2 小时前
25届计算机专业毕设选题推荐-基于python+Django协调过滤的新闻推荐系统
大数据·服务器·数据库·python·django·毕业设计·课程设计
我命由我123452 小时前
GPIO 理解(基本功能、模拟案例)
linux·运维·服务器·c语言·c++·嵌入式硬件·c#