最近在找工作, 额, 从七月份一直休息到九月初, 开始找, 结果发现快到十月了, 节点不是很好, 要过两个节, 所以估计入职什么的要到节后了>_<#

这几个月也在思考一些东西, 顺手也解决掉了Python源码剖析, 目前在逐步梳理笔记

发现自己首次做笔记还是太杂太乱, 堆在wiki里面一大坨的感觉, 还是要梳理画图, 思路更清晰些, 静候吧, 还是十篇左右的样子

读APUE, 做法比较残忍, 把书切开, 拆成一章一章地装订, 方便携带和阅读, 最后发现画满了一堆东西

发现还是不方便自己查阅, 所以还是决定重读, 转成笔记, 放到博客上方便搜索/查阅

对了, 代码之前只是下了看到的时候run下, 这次重读写写注释, 放到github了, 链接

在此感谢作者W.Richard Stevens :) 这本书五星好评, 建议如果搞linux相关后端, 可以读下


第一章 UNIX基础知识

Unix体系结构: 内核与系统调用

内核(kernel): 严格意义上, 将操作系统定义为一种软件, 它控制计算机硬件资源, 提供程序运行环境(相对较小, 位于环境的中心)

系统调用(system call): 内核的接口

关系: 公用函数库构建在系统调用接口之上, 应用软件既可以使用公用函数库, 也可以使用系统调用

登陆

口令文件/etc/passwd, 七个字段,冒号分隔

登录名:加密口令:数值用户 ID: 数值组 ID: 注释字段:起始目录:shell

加密口令已经转移到另一个文件

shell: 一个命令行解释器, 它读取用户输入, 然后执行命令

Linux 默认shell是Bourne-again shell

文件和目录

文件系统: 目录和文件组成的一种层次接口, 目录的起点称为根(root), 其名字是\\

目录(directory)是一个包含许多目录项的文件

文件名(filename): 不能出现斜线/和空操作符null(好的习惯只使用印刷字符的一个子集作为文件名字符)

创建目录时, 会自动创建两个文件名, 当前目录. 以及父目录..

路径名(pathname): 一个或多个以斜线分割的文件名序列. 以斜线开头的是绝对路径(absolute pathname), 否则是相对路径(relative pathname)

ls.c源代码

#include "apue.h"
#include <dirent.h>

// 可编译执行
// apue.h, 包含某些标准头文件, 定义了很多常量及库函数

int
main(int argc, char *argv[])
{
    DIR                *dp;
    // 结构体
    struct dirent    *dirp;

    // 需要至少一个参数
    if (argc != 2)
        // apue.h自定义函数
        err_quit("usage: ls directory_name");

    // 赋值后判断, opendir返回指向 DIR 结构体的指针
    if ((dp = opendir(argv[1])) == NULL)
        // apue.h自定义函数err_sys
        err_sys("can't open %s", argv[1]);

    // 赋值后判断, 读每一项, 返回指向readdir结构的指针或null(没有目录项可读时)
    while ((dirp = readdir(dp)) != NULL)
        // 取出每个目录的名字
        printf("%s\n", dirp->d_name);

    closedir(dp);

    // 终止程序, 0正常结束, 1-255出错
    exit(0);
}

工作目录(working directory), 每个进程都有一个, 优势成为当前工作目录, 进程可以通过chdir函数更改其工作目录.

输入和输出

文件描述符(file descriptor), 一个小的负整数, 内核使用它标识一个特定进程正在访问的文件. 当内核打开或创建一个新文件时, 返回一个文件描述符, 在读写的时候使用

每当运行一个新程序时, 所有的shell都为其打开三个文件描述符: 标准输入(standard input), 标准输出(standard output)以及标准错误(standard error)

不用缓冲的I/O
函数open/read/write/lseek/close

mycat.c源码

#include "apue.h"

/* 缓冲区大小, 常量 */
#define    BUFFSIZE    4096

int
main(void)
{
    int     n;
    char    buf[BUFFSIZE];

    /* STDIN_FILENO/STDOUT_FILENO -> apue.h -> unisted.h, 标准输入文件描述符0/标准输出文件描述符1 */

    /* 从标准输入读, read返回读得的字节数, 读到末端返回0, 发生错误返回-1 */
    while ((n = read(STDIN_FILENO, buf, BUFFSIZE)) > 0)
        /* 写到标准输入 */
        if (write(STDOUT_FILENO, buf, n) != n)
            err_sys("write error");

    /* 发生错误 */
    if (n < 0)
        err_sys("read error");

    exit(0);
}

标准I/O函数: 提供了一种对不用缓冲 I/O 函数的带缓冲接口, 可以无需担心如何选取最佳的缓冲区大小

getputc.c源码

#include "apue.h"


/* stdin/stdout -> apue.h -> stdio.h 标准输入文件/标准输出文件 */
/* EOF为stdio.h中定义的常量 */

int
main(void)
{
    int c;

    /* 从标准输入中读入一个字符 */
    while ((c = getc(stdin)) != EOF)
        /* 输出到标准输出 */
        if (putc(c, stdout) == EOF)
            err_sys("output error");

    if (ferror(stdin))
        err_sys("input error");

    exit(0);
}

程序和进程

程序(program): 存放在磁盘上, 处于某个目录中的一个可执行文件.(使用6个exec函数中的一个有内核将程序读入存储器, 并使其执行)

进程(process): 程序的执行实例

进程ID(process ID), 每个进程都有一个唯一的数字标识符, 总是一非负整数

hello.c 源码

#include "apue.h"

int
main(void)
{
    /* getpid得到进程pid */
    printf("hello world from process ID %ld\n", (long)getpid());
    exit(0);
}

进程控制: 三个主要函数, fork/exec(六种变体)/waitpid

shell1.c 源码

#include "apue.h"
#include <sys/wait.h>


/* fork创建一个新进程, 它被调用一次(由父进程调用), 返回两次(在父进程中返回子进程的进程ID, 在子进程中返回0) */

int
main(void)
{
    char    buf[MAXLINE];    /* from apue.h */
    pid_t    pid;
    int        status;

    printf("%% ");    /* print prompt (printf requires %% to print %) */

    /* 读入一行, 每一行命令会产生一个子进程用于执行 */
    while (fgets(buf, MAXLINE, stdin) != NULL) {
        /* 去掉换行符 */
        if (buf[strlen(buf) - 1] == '\n')
            buf[strlen(buf) - 1] = 0; /* replace newline with null */

        /* 执行读入的命令 */
        /* fork创建一个子进程, 返回<0则表示fork发生了错误 */
        if ((pid = fork()) < 0) {
            err_sys("fork error");

        /* 对于子进程, fork返回的pid=0(父进程fork返回的pid>0) */
        } else if (pid == 0) {        /* child */

            /* 调用execlp以执行从标准输入读入的命令 */
            /* fork+exec组合, 是某些操作系统所称的产生(spawn)一个新的进程 */
            execlp(buf, buf, (char *)0);
            err_ret("couldn't execute: %s", buf);
            /* 退出 */
            exit(127);
        }

        /* 父进程, 等待子进程终止 */
        /* pid为子进程id, status为子进程终止状态(用于判断其实如何终止的) */
        /* parent */
        if ((pid = waitpid(pid, &status, 0)) < 0)
            err_sys("waitpid error");
        printf("%% ");
    }
    exit(0);

通常, 一个进程只有一个控制线程(thread), 同一时刻只执行一组机器指令.(对于某些问题, 如果不同部分各使用一个控制线程, 那么整个问题解决相对容易, 多个控制线程也能充分利用多处理器系统的并行性)

在一个进程内的所有线程共享同一地址空间/文件描述符/栈以及与进程相关的属性(所以各线程在访问共享数据时需要采取同步措施以避免不一致性)

线程也用 ID 标识, 但只在其所属进程内起作用

出错处理

UNIX 函数出错的时候, 常常返回一个负值, 而整型变量errno通常被设置为含有附加信息的一个值

errno.h中, 定义了符号errno以及可以赋予它的各种常量.(errno(3)手册中)

对于errno两条规则

1. 如果没有出错, 其值则不会被一个例程清楚, 因此, 仅当函数的返回值指明出错时, 才检验其值
2. 任一函数都不会将errno值设置为0, 在errnoh中定义的所有常量都不为0

testerror.c 源码

#include "apue.h"
#include <errno.h>


/* strerror, 将errnum映射为一个出错信息字符串, 并且返回此字符串的指针 */
/* perror, 基于errno的当前值, 在标准出错上产生一条出错信息, 然后返回 */

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

出错恢复: errno.h中定义的各种出错分为致命性和非致命性两类.

致命性出错: 无法执行恢复动作, 最多只能在屏幕上打印一条出错信息, 或写入日志, 然后终止

非致命性出错: 可以较为妥善地处理

用户标识

用户ID(user ID), 数值, 系统中标识各个不同的用户, 每个用户唯一(用户不能更改其用户 ID)

用户 ID 为0的用户为根( root) 或超级用户(superuser)

组ID(group ID), 一个数值, 指定用户登陆名时同时指定的. 允许同组各个成员之间共享资源

组文件将组名映射为数字 ID, /etc/group

口令文件包含: 登录名 = 用户 ID 的映射

组文件包含: 组名 = 组ID 的映射

打印用户ID和组ID

uidgid.c 源码

#include "apue.h"

int
main(void)
{
    /* getuid / getgid */
    printf("uid = %d, gid = %d\n", getuid(), getgid());
    exit(0);
}

附加组ID

允许一个用户属于多个组, 最多16个.

信号

信号(signal): 通知进程已发生某种情况的一种技术.

e.g. 一个进程执行除法操作, 其除数为0, 则将名为SIGFPE的信号发给该进程

进程如何处理信号?

1. 忽略该信号
2. 按系统默认方式处理.
3. 提供一个函数, 信号发生时则调用该函数(捕捉信号)

信号捕捉

shell2.c 源码

#include "apue.h"
#include <sys/wait.h>

/* 声明信号处理函数 */
static void    sig_int(int);        /* our signal-catching function */

int
main(void)
{
    char    buf[MAXLINE];    /* from apue.h */
    pid_t    pid;
    int        status;


    /* signal函数, 指定SIGINT 到处理函数 sig_int */
    /* 机制, 类似于直接注册到了进程, 观察是否异常发生后捕获处理 */
    if (signal(SIGINT, sig_int) == SIG_ERR)
        err_sys("signal error");

    printf("%% ");    /* print prompt (printf requires %% to print %) */
    while (fgets(buf, MAXLINE, stdin) != NULL) {
        if (buf[strlen(buf) - 1] == '\n')
            buf[strlen(buf) - 1] = 0; /* replace newline with null */

        if ((pid = fork()) < 0) {
            err_sys("fork error");
        } else if (pid == 0) {        /* child */
            execlp(buf, buf, (char *)0);
            err_ret("couldn't execute: %s", buf);
            exit(127);
        }

        /* parent */
        if ((pid = waitpid(pid, &status, 0)) < 0)
            err_sys("waitpid error");
        printf("%% ");
    }
    exit(0);
}


/* 处理函数, 打印 */
void
sig_int(int signo)
{
    printf("interrupt\n%% ");
}

时间值

UNIX系统两种不同的时间值

日历时间, time_t, 从1970年1月1日00:00:00以来的国际标准时间UTC锁经过的秒数

进程时间, clock_t, CPU时间, 度量进程使用的中央处理器资源, 以始终滴答计算

Unix系统使用三个进程时间

1. 时钟时间, 总时间, real
2. 用户cpu时间, 执行用户指令耗时, user
3. 系统cpu时间, 执行内核程序耗时, sys

系统调用和库函数

所有操作系统都提供多种服务的入口点(系统调用), 程序由此想内核请求服务

UNIX所使用的技术是为每个系统调用在标准 C 库中设置一个具有同样名字的函数. 用户进程用标准 C 调用序列来调用这些函数, 然后函数又用系统所要求的技术调用相应的内核服务

系统调用通常提供一种最小接口,而库函数通常提供比较复杂的功能。

系统调用: 最小接口, 单一职责, 不可替换

C库函数: 复杂功能, 可替换, 可自行定义