APUE 第一章 UNIX基础知识

最近重新学习APUE,特开文章做学习笔记
apue.h被我封装在all.h中,apue配置可以参见我的另一篇文章[CentOS配置apue.h]https://www.jianshu.com/p/e16c1f48ebd6

第一章 UNIX基础知识

这一章的内容主要是UNIX的基本概念和属于简要描述

所有操作系统都为它们所运行的程序提供服务。典型的服务包括: 执行新程序、打开文件、读文件、分配存储区以及获得当前时间等,本书集中阐述不同版本的UNIX操作系统所提供的服务。
4064394-db4758b0bc50d15d.png
image.png

从广义上说,操作系统包括了内核和一些其他软件,这些软件使得 计算机能够发挥作用,并使计算机具有自己的特性。这里所说的其他软 件包括系统实用程序(system utility)、应用程序、shell以及公用函数库 等。

例如,Linux是GNU操作系统使用的内核。一些人将这种操作系统 称为GNU/Linux操作系统,但是,更常见的是简单地称其为 Linux。虽 然这种表达方法在严格意义上讲并不正确,但鉴于“操作系统”这个词 的双重含义,这种叫法还是可以理解的(这样的叫法更简洁)。

1.3 登录

1.登录名
用户在登录UNIX系统时,先键入登录名,然后键入口令。系统在 其口令文件(通常是/etc/passwd文件)中查看登录名。口令文件中的登 录项由7个以冒号分隔的字段组成,依次是:登录名、加密口令、数字 用户ID(205)、数字组ID(105)、注释字段、起始目录

sar:x:205:105:Stephen Rago:/home/sar:/bin/ksh 目前,所有的系统已将加密口令移到另一个文件中。

2.shell
用户登录后,系统通常先显示一些系统信息,然后用户就可以向 shell 程序键入命令。

shell 是一个命 令行解释器,它读取用户输入,然后执行命令。shell 的用户输入通常来 自于终端(交互式shell),有时则来自于文件(称为shell脚本)。


4064394-6d2f75a15392106b.png
image.png

系统从口令文件中相应用户登录项的最后一个字段中了解到应该为 该登录用户执行哪一个shell。

1.4文件和目录

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

目录(directory)是一个包含目录项的文件。
可以认为 每个目录项都包含一个文件名,同时还包含说明该文件属性的信息。文 件属性是指文件类型(是普通文件还是目录等)、文件大小、文件所有者、文件权限(其他用户能否访问该文件)以及文件最后的修改时间 等。stat和fstat函数返回包含所有文件属性的一个信息结构。


4064394-87a7c195cd03399f.png
image.png

2.文件名
目录中的各个名字称为文件名(filename)。只有斜线(/)和空字 符这两个字符不能出现在文件名中。

事实上,为了可移植性,POSIX.1 推荐将文件名限制在以 下字符集之内:字母(a~z、A~Z)、数字(0~9)、句点(.)、短 横线(-)和下划线(_)。

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

Research UNIX System和某些早期UNIX System V的文件系统限制 文件名的最大长度为14个字符,BSD版本则将这种限制扩展为255个字 符。现今,几乎所有商业化的UNIX文件系统都支持超过255个字符的文 件名。

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

dirent.h包含了文件操作的函数
dirent.h是用于目录操作的头文件,linux 默认在/usr/include/dirent.h目录下(会自动包含其他文件, 并且包含bits/dirent.h,后者定义了 dirent),常见的方法如下:

The <dirent.h> header shall define the following type:

DIR //结构体
A type representing a directory stream.

It shall also define the structure dirent which shall include the following members:

ino_t  d_ino       File serial number. //inode号
char   d_name[]    Name of entry.  //文件名

The type **ino_t** shall be defined as described in [*<sys/types.h>*]
The character array d_name is of unspecified size, but the number of bytes preceding the terminating null byte shall not exceed {NAME_MAX}.

The following shall be declared as functions and may also be defined as macros. Function prototypes shall be provided.

int            closedir(DIR *); //关闭句柄
DIR           *opendir(const char *); //打开名为xxx的文件,返回句柄
struct dirent *readdir(DIR *); //读取文件 返回dirent结构体

void           rewinddir(DIR *);
void           seekdir(DIR *, long);
long           telldir(DIR *); //返回当前指针的位置,表示第几个元素

不同平台下的dirent 结构体各异,如mac:

struct dirent {
        ino_t d_ino;                    /* file number of entry */
        __uint16_t d_reclen;            /* length of this record */
        __uint8_t  d_type;              /* file type, see below */
        __uint8_t  d_namlen;            /* length of string in d_name */
        char d_name[__DARWIN_MAXNAMLEN + 1];    /* name must be no longer than this */
};

这里我们写一个简单的ls命令实现

#include "../all.h"
#include "dirent.h"

int main(int argc, char * argv[]) {
    DIR * dp;
    dirent * dirp;
   
    printf("this is a simple ls \n");
    if (argc != 2) {
        printf("argument not enough\n");
        exit(-1);
    }

    if ((dp = opendir(argv[1])) == NULL) {
        printf("can't open file %s. \n");
        exit(-1);
    }
    
    while((dirp = readdir(dp)) != NULL) {
        printf("filename : %s    ino : %lld\n",dirp->d_name, dirp->d_ino);
    }

    closedir(dp);

    return 0;


}


4064394-1f00eaf4fa3dd09f.png
效果

因为各种不同 UNIX 系统目录项的实际格式是不一样的,所以使 用函数 opendir、readdir和closedir对目录进行处理。

•opendir函数返回指向DIR结构的指针,我们将该指针传送给readdir 函数。我们并不关心 DIR 结构中包含了什么。然后,在循环中调用 readdir 来读每个目录项。它返回一个指向 dirent 结构的指针,而当目录 中已无目录项可读时则返回 null 指针。在dirent 结构中取出的只是每个 目录项的名字(d_name)。使用该名字,此后就可调用stat函数(见4.2 节)以获得该文件的所有属性。

原书定义了错误处理函数,我懒得用了

程序调用了两个自编的函数对错误进行处理:err_sys和err_quit。从
上面的输出中可以看到,err_sys函数打印一条消息(“Permission denied”或“Not a directory”),说明遇到了什么类型的错误。这两个 出错处理函数在附录B中说明,1.7节将更多地叙述出错处理。

1.5输入和输出

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

2.标准输入、标准输出和标准错误 按惯例,每当运行一个新程序时,所有的 shell 都为其打开 3 个文 件描述符,即标准输入(standard input)、标准输出(standard output) 以及标准错误(standard error)。如果不做特殊处理,例如就像简单的 命令ls,则这3个描述符都链接向终端。大多数shell都提供一种方法,使 其中任何一个或所有这3个描述符都能重新定向到某个文件,例如: ls > file.list 执行ls命令,其标准输出重新定向到名为file.list的文件。

3.不带缓冲的I/O 函数open、read、write、lseek以及close提供了不带缓冲的I/O。这些 函数都使用文件描述符。

1、read()

函数定义:ssize_t read(int fd, void * buf, size_t count);

函数说明:read()会把参数fd所指的文件传送count 个字节到buf 指针所指的内存中。

返回值:返回值为实际读取到的字节数, 如果返回0, 表示已到达文件尾或是无可读取的数据。若参数count 为0, 则read()不会有作用并返回0。另外,以下情况返回值小于count:
(1)读常规文件时,在读到count个字节之前已到达文件末尾。例如,距文件末尾还有50个字节而请求读100个字节,则read返回50,下次read将返回0。
(2)对于网络套接字接口,返回值可能小于count,但这不是错误,详细解释参考这篇文章https://blog.csdn.net/hhhlizhao/article/details/73912578
注意:read时fd中的数据如果小于要读取的数据,就会引起阻塞。(关于read的阻塞情况网上有朋友有不同意见,笔者查阅资料后作如下补充。)以下情况read不会引起阻塞:

(1)常规文件不会阻塞,不管读到多少数据都会返回;

(2)从终端读不一定阻塞:如果从终端输入的数据没有换行符,调用read读终端设备会阻塞,其他情况下不阻塞;

(3)从网络设备读不一定阻塞:如果网络上没有接收到数据包,调用read会阻塞,除此之外读取的数值小于count也可能不阻塞,原因见上面链接。

由于笔者水平有限,如果文中有谬误之处还恳请诸位指出,以免误导大家。

2、write()
函数定义:ssize_t write (int fd, const void * buf, size_t count);

函数说明:write()会把参数buf所指的内存写入count个字节到所指的文件fd内。

返回值:如果顺利write()会返回实际写入的字节数。当有错误发生时则返回-1,错误代码存入errno中。

附加说明:

(1)write()函数返回值一般无0,只有当如下情况发生时才会返回0:write(fd, buf, count)中第三参数为0,此时write()什么也不做,只返回0。

(2)write()函数从buf写数据到fd中时,若buf中数据无法一次性读完,那么第二次读buf中数据时,其读位置指针(也就是第二个参数buf)不会自动移动,需要程序员来控制,而不是简单的将buf首地址填入第二参数即可。如可按如下格式实现读位置移动:write(fp, p1+len, (strlen(p1)-len))。 这样write第二次循环时便会从p1+len处写数据到fp, 之后的也一样。由此类推,直至(strlen(p1)-len)变为0。

(3)在write一次可以写的最大数据范围内(貌似是BUFSIZ ,8192),第三参数count大小最好为buf中数据的大小,以免出现错误。(经过笔者再次试验,write一次能够写入的并不只有8192这么多,笔者尝试一次写入81920000,结果也是可以,看来其一次最大写入数据并不是8192,但内核中确实有BUFSIZ这个参数,具体指什么还有待研究)

我们可以写一个简单的文件读写程序,实现从标准输入到标准输出echo(复读机)

#include "../all.h"
#include <unistd.h>
#define BUFFSIZE 4096

int main(int argc, char* argv[]) {
    int n = 0;
    char buf[BUFFSIZE];
    
    printf("This is a simple read/write test\n");

    while((n = read(STDIN_FILENO, buf, BUFFSIZE)) > 0) {
        if(write(STDOUT_FILENO, buf, n)!=n) {
            printf("write error\n");
        }
    }

    if(n < 0) {
        printf("read error\n");
    }
    return 0;
}

4064394-3411706c61eb42ba.png
echo复读机

还可以打开文件,输出到标准输出 实现cat

#include "../all.h"
#include <fcntl.h>
#include <unistd.h>
#define BUFFSIZE 4096
int main(int argc, char* argv[]) {
    printf("this is a simple cat, will show the file\n");
    
    if(argc  < 2) {
        printf("argument not enough , at least 2\n");
        exit(-1);
    }

    char buf[BUFFSIZE];
    memset(buf,0,BUFFSIZE);
    int n =0;
    int fd = open(argv[1], O_RDWR|O_CREAT);
    
    while(true) {
        int len = 0;
        while((n= read(fd, buf,BUFFSIZE)) > 0) {
            if(write(STDOUT_FILENO,buf + len,n) !=n) {
                printf("write error\n");
                exit(-1);
            } 
            len+=n;
        }

        break;
    }
    
    return 0;
}


4064394-07b564d373fbbb41.png
简单的cat

头文件<unistd.h>(apue.h中包含了此头文件)及两个常量 STDIN_FILENO和STDOUT_FILENO是POSIX标准的一部分(下一章 将对此做更多的说明)。头文件<unistd.h>包含了很多UNIX系统服务的 函数原型。

两个常量STDIN_FILENO和STDOUT_FILENO定义在<unistd.h>头 文件中,它们指定了标准输入和标准输出的文件描述符。在POSIX标准 中,它们的值分别是0和1,但是考虑到可读性,我们将使用这些名字来 表示这些常量。

将详细讨论BUFFSIZE常量,说明它的各种不同值将如何影响 程序的效率。但是不管该常量的值如何,此程序总能复制任一UNIX普 通文件。

4.标准I/O
read函数返回读取的字节数,此值用作要写的字节数。当到达输入 文件的尾端时,read返回0,程序停止执行。如果发生了一个读错误, read返回−1。出错时大多数系统函数返回−1。 标准I/O函数为那些不带缓冲的I/O函数提供了一个带缓冲的接 口。使用标准I/O函数无需担心如何选取最佳的缓冲区大小。例如,fgets函数读取一个完整的 行,而read函数读取指定字节数。

我们最熟悉的标准I/O函数是printf。在调用printf的程序中,总是包 含<stdio.h>(在本书中,该头文件包含在apue.h中),该头文件包括了 所有标准I/O函数的原型。

1.6 程序和进程

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

2.进程和进程ID
程序的执行实例被称为进程(process)。本书的每一页几乎都会使 用这一术语。某些操作系统用任务(task)表示正在被执行的程序。 UNIX系统确保每个进程都有一个唯一的数字标识符,称为进程 ID(process ID)。进程 ID总是一个非负整数。

我们写一个简单的程序获取进程pid

#include <unistd.h>
#include <stdio.h>
#include <iostream>
#include <memory>

int main() {
    long pid = 0;

    printf("this process show the pid\n");
    printf("pid : %ld\n",static_cast<long>(getpid()));
    return 0;
}

4064394-0fe6cf2a790b697d.png
getpid

3.进程控制
有3个用于进程控制的主要函数:fork、exec和waitpid。(exec函数 有7种变体,但经常把它们统称为exec函数。)
事实上我们在shell执行程序时就是shell fork出一个子进程,exec执行
我们可以写一个简单的shell从标准输入读取命令,然后执行这些命令。
它类似于shell 程序的基本实施部分。

#include "../all.h"
#include <stdio.h>
#include "sys/wait.h"
#include <iostream>

int main() {
    char buf[MAXLINE];
    pid_t pid;
    int status;

    printf("myshell$ ");
    while(fgets(buf, MAXLINE, stdin) != NULL) {
        if(buf[strlen(buf) - 1] == '\n') {
            buf[strlen(buf)-1] = 0; //replace the newline with null
        }

        if((pid = fork()) < 0 ) {
            printf("fork error\n");
            exit(-1);
        } else if (pid == 0) {  //child
            execlp(buf,buf,(char*)0);
            printf("couldn't excute %s\n", buf);
            exit(127);//子进程退出,返回127
        }

        /*parent*/
        if((pid = waitpid(pid, &status, 0)) < 0) {
            printf("wait pid error\n");
        }
        printf("myshell$ ");
    }

    return 0;
}

4064394-5365064150b08e42.png
实现一个简单的myshell

在这个30行的程序中,有很多功能需要考虑。

•用标准I/O函数fgets从标准输入一次读取一行。当键入文件结束符 (通常是Ctrl+D)作为行的第一个字符时,fgets 返回一个 null 指针,于 是循环停止,进程也就终止。

•因为fgets返回的每一行都以换行符终止,后随一个null字节,因此 用标准C函数strlen计算此字符串的长度,然后用一个null字节替换换行 符。这样做是因为execlp函数要求的参数是以null结束的而不是以换行符 结束的。

•调用fork创建一个新进程。新进程是调用进程的一个副本,我们称 调用进程为父进程,新创建的进程为子进程。fork对父进程返回新的子 进程的进程ID(一个非负整数),对子进程则返回0。因为fork 创建一 个新进程,所以说它被调用一次(由父进程),但返回两次(分别在父 进程中和在子进程中)。

•在子进程中,调用 execlp 以执行从标准输入读入的命令。这就用新的程序文件替换了子进程原先执行的程序文件。fork和跟随其后的 exec两者的组合就是某些操作系统所称的产生(spawn)一个新进程。在
UNIX系统中,这两部分分离成两个独立的函数。

•子进程调用 execlp 执行新程序文件,而父进程希望等待子进程终 止,这是通过调用waitpid实现的,其参数指定要等待的进程(即pid参数 是子进程ID)。waitpid函数返回子进程的终止状态(status 变量)。在 我们这个简单的程序中,没有使用该值。
如果需要,可以用此值准确地判定子进程是如何终止的。

当然这个小shell并不能执行复杂的指令,只能执行 ls而不能执行ls -a这种组合命令。

该程序的最主要限制是不能向所执行的命令传递参数。例如不能 指定要列出目录项的目录名,只能对工作目录执行ls命令。为了传递参 数,先要分析输入行,然后用某种约定把参数分开(可能使用空格或制 表符),再将分隔后的各个参数传递给execlp函数。尽管如此,此程序 仍可用来说明UNIX系统的进程控制功能。

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

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

  • 1、首先是定义

进程:是执行中一段程序,即一旦程序被载入到内存中并准备执行,它就是一个进程。进程是表示资源分配的的基本概念,又是调度运行的基本单位,是系统中的并发执行的单位。

线程:单个进程中执行中每个任务就是一个线程。线程是进程中执行运算的最小单位。

  • 2、一个线程只能属于一个进程,但是一个进程可以拥有多个线程。多线程处理就是允许一个进程中在同一时刻执行多个任务。

  • 3、线程是一种轻量级的进程,与进程相比,线程给操作系统带来侧创建、维护、和管理的负担要轻,意味着线程的代价或开销比较小。

  • 4、线程没有地址空间,线程包含在进程的地址空间中。线程上下文只包含一个堆栈、一个寄存器、一个优先权,线程文本包含在他的进程 的文本片段中,进程拥有的所有资源都属于线程。所有的线程共享进程的内存和资源。 同一进程中的多个线程共享代码段(代码和常量),数据段(全局变量和静态变量),扩展段(堆存储)。但是每个线程拥有自己的栈段, 寄存器的内容,栈段又叫运行时段,用来存放所有局部变量和临时变量。

  • 5、父和子进程使用进程间通信机制,同一进程的线程通过读取和写入数据到进程变量来通信。

  • 6、进程内的任何线程都被看做是同位体,且处于相同的级别。不管是哪个线程创建了哪一个线程,进程内的任何线程都可以销毁、挂起、恢复和更改其它线程的优先权。线程也要对进程施加控制,进程中任何线程都可以通过销毁主线程来销毁进程,销毁主线程将导致该进程的销毁,对主线程的修改可能影响所有的线程。

  • 7、子进程不对任何其他子进程施加控制,进程的线程可以对同一进程的其它线程施加控制。子进程不能对父进程施加控制,进程中所有线程都可以对主线程施加控制。

  • 相同点:

进程和线程都有ID/寄存器组、状态和优先权、信息块,创建后都可更改自己的属性,都可与父进程共享资源、都不能直接访问其他无关进程或线程的资源

控制线程的函数与控制进程的函数类似,但另有一套。线程模型是 在进程模型建立很久之后才被引入到UNIX系统中的,然而这两种模型 之间存在复杂的交互

1.7 出错处理

当UNIX系统函数出错时,通常会返回一个负值,而且整型变量 errno通常被设置为具有特定信息的值。例如,open 函数如果成功执行 则返回一个非负文件描述符,如出错则返回−1。在 open出错时,有大 约15种不同的errno值(文件不存在、权限问题等)。而有些函数对于出 错则使用另一种约定而不是返回负值。例如,大多数返回指向对象指针 的函数,在出错时会返回一个null指针。
文件<errno.h>中定义了errno以及可以赋与它的各种常量。这些常 量都以字符E开头。另外,UNIX系统手册第2部分的第1页,intro(2)列出 了所有这些出错常量。例如,若errno等于常量EACCES,表示产生了权 限问题(例如,没有足够的权限打开请求文件)。

#include "../all.h"
#include <errno.h>
int main(int argc , char *argv[]){
    fprintf(stderr, "EACCES: %s\n", strerror(EACCES));
    errno = ENOENT;
    perror(argv[0]);
    return 0;
}

可将在<errno.h>中定义的各种出错分成两类:致命性的和非致命性 的。对于致命性的错误,无法执行恢复动作。最多能做的是在用户屏幕 上打印出一条出错消息或者将一条出错消息写入日志文件中,然后退 出。对于非致命性的出错,有时可以较妥善地进行处理。大多数非致命 性出错是暂时的(如资源短缺),当系统中的活动较少时,这种出错很 可能不会发生。
与资源相关的非致命性出错包括:EAGAIN、ENFILE、 ENOBUFS、ENOLCK、ENOSPC、EWOULDBLOCK,有时ENOMEM 也是非致命性出错。当EBUSY指明共享资源正在使用时,也可将它作为 非致命性出错处理。当 EINTR 中断一个慢速系统调用时,可将它作为 非致命性出错处理(在10.5节对此会进行更多说明)。

对于资源相关的非致命性出错的典型恢复操作是延迟一段时间,然 后重试。这种技术可应用于其他情况。例如,假设出错表明一个网络连 接不再起作用,那么应用程序可以采用这种方法,在短时间延迟后,尝 试重建该连接。一些应用使用指数补偿算法,在每次迭代中等待更长时 间。
最终,由应用的开发者决定在哪些情况下应用程序可以从出错中恢
复。如果能够采用一种合理的恢复策略,那么可以避免应用程序异常终 止,进而就能改善应用程序的健壮性。

1.8 用户标识

1.用户ID
口令文件登录项中的用户ID(user ID)是一个数值,它向系统标识 各个不同的用户。系统管理员在确定一个用户的登录名的同时,确定其 用户ID。用户不能更改其用户ID。通常每个用户有一个唯一的用户 ID。下面将介绍内核如何使用用户 ID 来检验该用户是否有执行某些操 作的权限。
用户 ID 为 0 的用户为根用户(root)或超级用户(superuser)。在 口令文件中,通常有一个登录项,其登录名为 root,我们称这种用户的 特权为超级用户特权。我们将在第 4 章中看到,如果一个进程具有超级 用户特权,则大多数文件权限检查都不再进行。某些操作系统功能只向 超级用户提供,超级用户对系统有自由的支配权。
Mac OS X客户端版本交由用户使用时,禁用超级用户账户,服务器 版本则可使用该账户。在Apple 的网站可以找到使用说明,它告知如何 才能使用该账户。参见 http://support.apple.com/kb/HT1528

2.组ID
口令文件登录项也包括用户的组ID(group ID),它是一个数值。 组ID也是由系统管理员在指定用户登录名时分配的。一般来说,在口令 文件中有多个登录项具有相同的组 ID。组被用于将若干用户集合到项 目或部门中去。这种机制允许同组的各个成员之间共享资源(如文 件)。4.5 节将介绍可以通过设置文件的权限使组内所有成员都能访问 该文件,而组外用户不能访问。
组文件将组名映射为数值的组ID。组文件通常是/etc/group。

使用数值的用户ID和数值的组ID设置权限是历史上形成的。对于 磁盘上的每个文件,文件系统都存储该文件所有者的用户ID和组ID。 存储这两个值只需4个字节(假定每个都以双字节的整型值存放)。如 果使用完整 ASCII 登录名和组名,则需更多的磁盘空间。另外,在检验 权限期间,比较字符串较之比较整型数更消耗时间。
但是对于用户而言,使用名字比使用数值方便,所以口令文件包含 了登录名和用户 ID 之间的映射关系,而组文件则包含了组名和组ID之 间的映射关系。例如,ls -l命令使用口令文件将数值的用户ID映射为登 录名,从而打印出文件所有者的登录名。

我们写一个简单的程序来获取当前用户的id和组id

#include "../all.h"
#include <iostream>

int main() {
    printf("this is a simple uid and gid query code\n");

    printf("uid = %d, gid = %d\n", getuid(), getgid());
    return 0;
}

3.附属组ID 除了在口令文件中对一个登录名指定一个组ID外,大多数 UNIX系
统版本还允许一个用户属于另外一些组。这一功能是从4.2BSD开始的, 它允许一个用户属于多至16个其他的组。登录时,读文件/etc/group,

寻找列有该用户作为其成员的前 16 个记录项就可以得到该用户的附属 组ID(supplementary group ID)。在下一章将说明,POSIX要求系统至 少应支持8个附属组,实际上大多数系统至少支持16个附属组。

1.9信号

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

1)忽略信号。有些信号表示硬件异常,例如,除以0或访问进程 地址空间以外的存储单元等,因为这些异常产生的后果不确定,所以不 推荐使用这种处理方式。
(2)按系统默认方式处理。对于除数为0,系统默认方式是终止该 进程。
(3)提供一个函数,信号发生时调用该函数,这被称为捕捉该信 号。通过提供自编的函数,我们就能知道什么时候产生了信号,并按期 望的方式处理它。

很多情况都会产生信号。终端键盘上有两种产生信号的方法,分别 称为中断键(interrupt key,通常是Delete键或Ctrl+C)和退出键(quit key,通常是Ctrl+),它们被用于中断当前运行的进程。另一种产生信 号的方法是调用kill函数。在一个进程中调用此函数就可向另一个进程发 送一个信号。当然这样做也有些限制:当向一个进程发送信号时,我们 必须是那个进程的所有者或者是超级用户。

为了能捕捉到此信号,程序需要调用signal函数,其中指定了当产生 SIGINT信号时要调用的函数的名字。函数名为 sig_int,当其被调用时, 只是打印一条消息,然后打印一个新提示符。

#include "signal.h"
#include <iostream>
#include <stdio.h>
using namespace std;
void sig_int(int signo) {
    cout << "interrupt " << signo << endl;
    // exit(-1);
}
int main(int argc, char* argv[]) {
    while(true){
        cout << "hello" << endl;
        if(signal(SIGINT,sig_int) == SIG_ERR) {
            errno = ENOENT;
            perror(argv[0]);
            exit(-1);
        }
    }
    return 0;
}

1.10 时间值

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

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

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

当度量一个进程的执行时间时
UNIX系统为一个进程 维护了3个进程时间值:
•时钟时间;
•用户CPU时间;
•系统CPU时间。
时钟时间又称为墙上时钟时间(wall clock time),它是进程运行的
时间总量,其值与系统中同时运行的进程数有关。每当在本书中提到时 钟时间时,都是在系统中没有其他活动时进行度量的。

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

1.11系统调用和库函数

有的操作系统都提供多种服务的入口点,由此程序向内核请求服 务。各种版本的UNIX实现都提供良好定义、数量有限、直接进入内核 的入口点,这些入口点被称为系统调用(system call,见图1-1)。 Research UNIX系统第7版提供了约50个系统调用,4.4BSD提供了约110个 系统调用,而SVR4则提供了约120个系统调用。具体数字在不同操作系 统版本中会不同,新近的大多数系统大大增加了支持的系统调用的个 数。Linux 3.2.0提供了380个系统调用,FreeBSD 8.0提供的系统调用超过 450个。

UNIX所使用的技术是为每个系统调用在标准C库中设置一个具有同 样名字的函数。用户进程用标准C调用序列来调用这些函数,然后,函 数又用系统所要求的技术调用相应的内核服务。例如,函数可将一个或 多个C参数送入通用寄存器,然后执行某个产生软中断进入内核的机器 指令。从应用角度考虑,可将系统调用视为C函数。

《UNIX程序员手册》的第3部分定义了程序员可以使用的通用库函 数。虽然这些函数可能会调用一个或多个内核的系统调用,但是它们并 不是内核的入口点。例如,printf 函数会调用write系统调用以输出一个 字符串,但函数strcpy(复制一个字符串)和atoi(将ASCII转换为整 数)并不使用任何内核的系统调用。

从实现者的角度来看,系统调用和库函数之间有根本的区别,但从 用户角度来看,其区别并不重要。在本书中,系统调用和库函数都以C 函数的形式出现,两者都为应用程序提供服务。但是,我们应当理解, 如果希望的话,我们可以替换库函数,但是系统调用通常是不能被替换 的。 以存储空间分配函数malloc为例。有多种方法可以进行存储空间分 配及与其相关的无用空间回收操作(最佳适应、首次适应等),并不存 在对所有程序都最优的一种技术。UNIX系统调用中处理存储空间分配 的是 sbrk(2),它不是一个通用的存储器管理器。它按指定字节数增加或 减少进程地址空间。如何管理该地址空间却取决于进程。存储空间分配 函数malloc(3)实现一种特定类型的分配。如果我们不喜欢其操作方式, 则可以定义自己的malloc函数,它很可能将使用sbrk 系统调用。事实 上,有很多软件包,它们使用 sbrk 系统调用实现自己的存储空间分配算 法。

另一个可说明系统调用和库函数之间差别的例子是,UNIX 系统提 供的判断当前时间和日期的接口。一些操作系统分别提供了一个返回时 间的系统调用和另一个返回日期的系统调用。任何特殊的处理,例如正 常时制和夏令时之间的转换,由内核处理或要求人为干预。UNIX 系统 则不同,它只提供一个系统调用,该系统调用返回自协调世界时1970年 1月1日零时这个特定时间以来所经过的秒数。对该值的任何解释,例如 将其变换成人们可读的、适用于本地时区的时间和日期,都留给用户进 程进行处理。在标准C库中,提供了若干例程以处理大多数情况。这些 库函数处理各种细节,如各种夏令时算法等。

4064394-c8fc30efc2fab52c.png
image.png
4064394-8a8f34690a1b029b.png
image.png

系统调用和库函数之间的另一个差别是:系统调用通常提供一种最 小接口,而库函数通常提供比较复杂的功能。我们从sbrk系统调用和 malloc库函数之间的差别中可以看到这一点。当我们比较不带缓冲的I/O 函数(见第3章)和标准I/O函数时,还将看到这种差别。

进程控制系统调用(fork、exec 和 wait)通常由用户应用程序直接
调用(请回忆图 1-7中的基本 shell)。但是为了简化某些常见的情况, UNIX 系统也提供了一些库函数,如 system和popen

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值