APUE_Chapter01_Introduction_笔记总结

            ***PAY A TRIBUTE TO W.Richard Stevens***

Chapter01: Unix预览

1.1 简介

什么是操作系统,操作系统其实就是为程序的运行提供服务的平台. 其中包括比如打开文件,读写文件,分配内存,获取时间等等. Unix是很神奇也很深奥的系统,这章节带大家语言Unix中都有些什么,我们需要学习写什么. 所有在本章涉及到的知识点,在之后的章节中都会详细解释.


1.2 Unix整体架构

严格意义上来说,操作系统就是控制电脑硬件资源和分配环境供上层应用执行的软件. 我们就称其为kernel. 也就是核心的意思. 一个Linux内核大概就是六七十M的样子. 在内核外围紧紧包裹着的就是system call.想必大家也听过,这个就是调用内核的入口,这都是一系列的函数接口. 再外层一些就是library routines, 这是比如C库函数,等等通用的函数库. 相邻的是shell. 到底什么是shell,我们在后面会介绍,你其实可以简单的把它当做是提供运行上层应用的特殊应用程序,学过鸟哥的私房菜的知道Linux是一堆命令,所以你现在也可以姑且当做是命令解释器. 再外层就是应用程序了,应用程序可以直接访问shell, libray routines 和system calls.
这里写图片描述
大家可以man man看看具体的信息,这里就会显示出不同的manual page的功能:
man 1: 可执行文件或者是shell命令
man 2: 系统调用(内核提供的函数)
man 3: 库函数调用(程序代码库中的函数)
man 4: 特殊的文件(一般是/dev下的文件,比如设备文件,块儿文件等)
man 5: 一些文件的格式和规定行使说明(/etc/passwd这文件格式是这么定义的等)
man 6: 游戏(这一般用不到)
man 7: 各种各样的杂情况(包括宏等)
man 8: 系统管理员命令(一般意义上是root)
man 9: kernel的东西

这里写图片描述


1.3 登录

当我们登录进系统的时候,首先碰到的就是输入用户名和密码,这时候,系统会去密码文件中检索匹配与否, 在以前这个文件是/etc/passwd, 但是现在已经加密后将密文等移动到了/etc/shadow等其他地方. 我们先看/etc/passwd:
sar:x:205:105:Stephen Rago:/home/sar:/bin/ksh
每条记录都是七列: 登录名,加密后的密码,数字化的UID,数字化的GID,备注信息, 家目录,使用的是什么shell.
用户输入给shell的东西一般都是从终端(本身就是一个可交互的shell)或从一些文件中(这些文件就是shell脚本文件).
大家可以自行在查看本机支持什么shell:
Ubuntu:
这里写图片描述
NetBSD:
这里写图片描述
这里写图片描述
Bourne Shell: sh是存在于绝大多数的Unix系统中的.
Bourne-again shell: GNUshell,几乎所有的linux都在用这个shell. 按照POSIX规则进行的编写, 兼容sh, 并且支持csh和ksh的特点.
C shell: System V Release 4(SVR4)这种Unix种用着Cshell, 其中有一些sh没有的特点: 任务控制, 历史机制, 和命令行的编辑等.
Korn shell: sh的继承者,开始用于SVR4, 向上兼容sh并且结合了Cshell多出来的那些功能.
不懂的系统默认使用不同的shell, 比如BSD系列一般是默认sh, Linux好多都是bash, 但是像Debian用的就死BSD的sh的替代品dash(Debian Almquist shell).


1.4 文件和目录

Unix中的文件是树的层级状的
这里写图片描述
一切都是起始于一个/(根目录). 在Unix中,目录也是文件,具体到文件到底包括什么,在后面会讲到. 只不过在目录的结构体中是包含目录内的文件名和描述文件的属性的结构体. 每个文件(包括目录)的属性信息:
这里写图片描述
文件类型(是常规文件, 目录, 设备文件等), 文件大小,文件所有者,文件权限,最近修改时间. stat(2), fstat(2)是可以查到具体的结构体信息的.这些结构体中的属性就是我们ls -al命令的时候显示的文件的那些属性.
但是对于大多数的Unix文件系统来说,内部文件的属性是不放置在目录块中的,因为如果一个文件有硬链接的时候很难去追踪同步,所以一般都是放出来的,这些内容后面会详解释的.
这里写图片描述
这就是目录结构体中存放的信息
当我们在命名文件的时候,不能使用/和空格, 因为在Unix中,/表示路径名称的分割,空格表示文件名命名的结束. 在不同的文件系统中对一些字符也是不进行识别的,都会以乱码的形式输出, 大家可以看看ls命令中的-q这个参数. 所以POSIX.1建议了文件名只能使用letters, numbers, period(.), dash(-), and underscore(_).老版的Unix System V限制文件名只能14字符,但是BSD就扩展到了255个字符.
每当我们创建一个新的目录,里面总会出来.和..两个东西,.表示的就是当期那目录, cd .你会发现没有变化, ..表示的就是上级目录, cd ..你会发现回到了你刚才进入前的目录中. 只有在/目录下.和..一样,因为一切开始于/嘛!
如果一个路径开始于/,那么说明是绝对路径,否则就是相对路径, 相对路径就是指相对于当前目录的路径.
接下来这段程序就是简单列出目录下的文件:
(参考: opendir(3), readdir(3), perror(3))

#include <stdio.h>
#include <sys/types.h>
#include <dirent.h>
#include <stdlib.h>
#include <string.h>

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

    if(argc != 2){
        perror("usage: ls dir_name");
    }
    if((dp = opendir(argv[1])) == NULL)
    {
        perror("Can't open this directory");
    }
    /*struct dirent *readdir(DIR *dirp);*/
    while((dirp = readdir(dp)) != NULL)
    {
        /*
        struct dirent {
               ino_t          d_ino;       /* inode number
               off_t          d_off;       /* not an offset; see NOTES
               unsigned short d_reclen;    /* length of this record
               unsigned char  d_type;      /* type of file; not supported
                                              by all filesystem types
               char           d_name[256]; /* filename
           };
        */
        printf("%s\n", dirp->d_name);
    }
    closedir(dp);
    exit(0);//0 means ok
}

cc是C的编译器,
这里写图片描述
但是像GNU系统中C编译器一般是gcc, 即使用cc也是指向了gcc
这里写图片描述
我们可以从/usr/inclulde/下面找到所有库函数的头文件.
每个进程都有自己的工作目录, 我们可以用chdir(2)来改变当前路径. 家目录就是我们刚进入系统的目录,pwd可以
查询当前目录, 这个家目录就是根据我们用户登录时, /etc/passwd中的文件.


1.5 输入和输出

文件描述符就是一些常规的非负整数,文件描述符是内核用来识别进程中使用到的文件的. 当打开一个已经存在的文件或者新建一个文件时,内核都会给用户层返回文件描述符以供用户对这个文件操作.
一般情况下,所有的shell都会默认打开三个文件描述符. 标准输入(0),标准输出(1),和标准错误(2). 如果没有特殊的指定的话, 比如执行ls这三个描述符就会指向终端. 除非用一些>进行重定向的符号.
函数像open, read, write, lseek和close都是使用无缓冲的IO,所以它们都是直接操作fd的.
让我们实现各简单的从标准输入读入数据输出到标准输出中:
(参考: read(3), write(3))

#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
#define BUFFSIZE 4096

int main(void)
{
    int n;
    char buf[BUFFSIZE];
    /*ssize_t read(int fildes, void *buf, size_t nbyte);*/
    while((n = read(STDIN_FILENO, buf, BUFFSIZE)) > 0)
    {
    /*ssize_t write(int fildes, const void *buf, size_t nbyte);*/
        if(write(STDOUT_FILENO, buf, n) != n)
        {
            perror("write err");
        }
    }
    if(n < 0)
    {
        perror("read err");
    }
    exit(0);
}

read函数返回了读到的字节数,这些字节数就是要write的字节数,当文件读取结束时,read返回0, 并且程序结束. 如果read出错了,就返回-1,对于绝大部分的系统函数而言,-1就是错误的标志.
当我们执行./io > data的时候,标准输入就是终端, 标准输出就变成了data文件.并且标准错误也是指向了终端.当我们执行./io < infile >outfile, 文件名为infile将会拷贝到outfile. 相比于无缓冲的I/O, 标准I/O可以缓解我们对bufersize的优化问题.比如,fgets就是针对整行进行读取, getc是一个符号一个符号读取,碰到EOF结束,而read函数则是需要对指定字节数进行读取. 我们最常见的标准I/O就是printf. 我们来一个stdin,stdout但是用标准I/O的代码:
(参考: getc(3), putc(3), ferror(3))
这里稍微提一下fgetc和getc的区别:
分别查看manuage和stdio.h
这里写图片描述
这里写图片描述
发现man中说不同支出在于getc有可能是个宏,但是在最新的stdio.h中已经做了优化,所以两者是一样的使用的. 然后再man中有这样以及话,”returns it as an unsigned char cast to an int”就是所将收到的char转换成了int类型,这样我们putc直接用就可以了.

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

int main(void)
{
    int c;
/*int getc(FILE *stream);*/
    while((c = getc(stdin)) != EOF)
    {
        if(putc(c, stdout) == EOF)
        {
            perror("out_put err");
        }
    }
/*clearerr, feof, ferror, fileno - check and reset stream status*/
/*int ferror(FILE *stream);*/
    if(ferror(stdin))
    {
        perror("input err");
    }
    exit(0);
}

在这段代码中,你肯定有疑问说,getc(FILE *stream)为什么传入个stdin. 那么这些东西都可以在stdio.h中找到
这里写图片描述
这里写图片描述
我们可以找到这些根源问题.


1.6 程序和进程

程序是放在硬盘上的东西,加载到内存中然后由内核去执行. 一个正在执行的程序就是进程. Unix表示每个进程都有独一无二的进程ID.
走个程序:
(参考: getpid(2))

#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
#include <stdlib.h>

int main(void)
{
    printf("hell world from process ID %ld\n", (long)getpid());
    exit(0);
}

这里写图片描述
我们可以发现每次执行都不同,是因为每次执行一个程序,都是一个单独的进程. 然后在我们的程序中为什么要强转long呢?是因为getpid这个函数返回的是pid_t数据类型, 我们并不知道它的类型,我们知道的是保证适应long类型. 其实也可以是int 但是为了更为的保险. 这样一说,进程就是相当于程序运行的平台的了, 总共有三个主要的函数进行进程控制: fork(2), exec(3), waitpid(2). 其中exec是个家族函数,有7中不同的exec,但是我们一般统称为exec.
接下来的一段程序带领大家从input输入一条命令然后执行,这里涉及到子父进程和回收问题,在之后都会详解讲解:
(参考: fgets(3), strlen(3), fork(2), execlp(3), waitpid(2))

#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <stdlib.h>
#define MAXLINE 4096

int main(void)
{
    char buf[MAXLINE];
    int status;
    pid_t pid;
    /*print prompt(printf requires %% to print %)*/
    printf("%%");
    /*char *fgets(char *s, int size, FILE *stream);*/
    while(fgets(buf, MAXLINE, stdin) != NULL)
    {
        if(buf[strlen(buf) - 1] == '\n')
        {
            //cut off
            buf[strlen(buf) - 1] = 0;
        }
        /*pid_t fork(void);*/
        if((pid = fork()) < 0)
        {
            perror("fork err");
        }
        else if(pid == 0)
        {
    /*
        int execlp(const char *file, const char *arg, ...
                      (char  *) NULL );
    */
            execlp(buf, buf, (char *)0);
            perror("couldn't execute\n");
            exit(127);
        }
        /*parent*/
        /*pid_t waitpid(pid_t pid, int *status, int options);*/
        if((pid = waitpid(pid, &status, 0) < 0))
        {
            perror("waitpid err");
        }
        printf("%%");
    }
    exit(0);
}

这个代码中涉及到的知识点还是不少的:
1. 我们用标准I/O函数fgets读取一次读取一行.
2. 因为execlp执行命令的时候的参数是要以空结尾的文件,但是我们标准输入的时候,最后
敲入的是回车键,在Linux中就是\n, 所以我们要用C标准库函数的strlen将最后一个字符
换成0, 表示null结束.
3. 我们通过fork来创建出子进程. pid为0表示子进程. fork()函数是典型的一次调用返回两次
的函数.
4. 在子进程中,用execlp执行来自标准输入的命令. 就在这时就会有一个执行命令的进程取代
了孩子进程, 这种fork和exec的结合叫做在一些操作系统上生产新的进程.
5. 因为儿子在执行程序,父亲就等待孩子结束. 这就是调用waitpid,参数中有status表示返回
孩子的状态码,但是这个简单程序中我们没有接这个状态.
这里写图片描述
在一个进程中的所有线程共享地址空间, 文件描述符,栈和进程相关的属性.每个线程在他们自己的栈上执行, 虽然在同一进程中有些线程是可以访问其他线程的栈. 因为他们能够访问同样的内存,线程需要同步操作来共享数据来避免不一致性.这里有些抽象,但是后面线程章节会着重处理.操作线程的函数与操作进程的函数是等价的.因为线程加入Unix系统是在进程模型建立后很久才实行的.


1.7 错误处理

这里写图片描述
大家可以看到这种RETURN VALUE在Unix的函数中是很常见的. -1表示发生了错误,并且errno中设置了具体的错误值,在这里能找到具体的出错原因.在errno.h中定义了errno是int类型,在Linux中errno(3)我们能够看到左右的errno的宏定义, 在Unix中我们可以通过intro(2)来看到列举, 但是NetBSD7.x中也可以从errno(3)中列举出来.
这里写图片描述
这里写图片描述
但是在多线程的系统中,进程地址空间被多个线程共享,为了线程间不被干扰,所以每个线程都有自己的errno拷贝. 所以linux中对访问errno定义为:
extern int *__errno_location(void)
define errno(*__errno_location())
我们只应该在函数返回错误发生的时候去检测errno的值,否则正常情况下它的值是不会被清除的,所以有可能这个函数执行的时候,即使成功,errno目前还是上个函数的错误状态. 在Unix中没有任何一个函数设置errno是0, 所以它不可能是0的.
我们想获取到准确的errno的描述string,就通过C标准定义的strerror(3):
这里写图片描述
这个函数返回了一个指向errno的message的指针.
在我们上面的代码中,我将错误信息全部都用perror(3)进行输出, 这个函数完整的输出情况:参数msg: (根据errno)err_message 换行
来我们看一下上面提到的perror(3)和strerror(3):

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

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

这里写图片描述
错误也是分着致命错误和非致命错误的. 致命错误就是无法恢复的错误, 顶多也就是打印到用户界面或者记录到日志文件中. 非致命错误, 可以处理.绝大多数数的非致命错误都是暂时的,比如资源存储报警, 甚至在一些系统上都不可能发生.
与资源相关的非致命错误是:
EAGAIN: Resource temporarily unavailable (may be the same value as EWOULDBLOCK) (POSIX.1)
资源暂时不可获得.
EWOULDBLOCK: Operation would block (may be same value as EAGAIN) (POSIX.1)
操作被阻了
NFILE: Too many open files in system (POSIX.1); on Linux, this is probably a result of encountering
the /proc/sys/fs/file-max limit (see proc(5)).
打开太多文件了,也即是文件描述符占用的太多了,默认是1024个fd,但是你可以重新设置.
ENOBUFS: No buffer space available (POSIX.1 (XSI STREAMS option))
没有缓存空间了
ENOLCK:No locks available (POSIX.1)
锁不够了
ENOSPC: No space left on device (POSIX.1)
设备上空间不足了
ENOMEM: Not enough space (POSIX.1)
空间不做
EBUSY: Device or resource busy (POSIX.1)
当一个资源正在使用的情况下,这个错误码也可以是非致命的

处理非致命错误的方式通常就是延迟或稍后重新尝试.这些等待的参数完全取决于开发者的设定


1.8 用户识别

用户ID在我们的密码文件中就是一个简单的数值,系统用这个数值来识别用户. 当我们用户创建之后,系统会自动分配我们是改变它的.用户ID是0的表示是root或者超级管理员.如果一个进程有超管权限,那么几乎所有的文件权限都是可访问的.(free rein完全自由)形容root.
但是对于一些系统,比如MacOS客户版本超管是不能用的.同样的,在我们的passwd文件中也是有一列是防止组ID的,用户组就是将多个用户聚集在一起共享文件等一些资源组的单独文件在/etc/group中. 在Unix系统中的任何一个文件系统都通过用户ID和组ID识别文件的所有者, 存储这些值分别是4字节的整数(旧版本的Unix是2字节). 这样省空间,并且在登录的时候验证字串比验证Integer要浪费资源.自从4.2BSD开始,用户不进能够拥有创建用户的时候同时创建的组之外,还能另外加入最大16个组中.这就是传说中的SGID(Supplementary Group IDs),这个是通过登录时读取/etc/group来获取到的.


1.9 信号

信号是用来通知某个进程某些状况的发生.比如一个进程进行了除以0的操作,那么就会产生SIGFPE的信号(浮点数异常).进程有三种处理的方式:
1. 忽略.这不是个推荐的方法.
2. 让默认的动作发生. 比如上面的除以0的操作,默认的操作就是终止进程.
3. 当信号产生的时候调用提供的处理函数.这就是叫做捕获信号.通过捕获,我们就能知道什么时候信号发生了,并且按照我们
的意愿进行处理.
在我们日常的操作中经常会产生信号操作.比如当我们敲入CTRL+C或者DELETE键的时候就产生了中断信号,CTRL+BACKSLASH就产生了退出信号,这俩信号会中断或者退出当前的进程.另外一种常见的产生信号的方式是kill(2)方法.我们也能够用这个函数从这进程发送信号到另外的进程, 前提是我们必须是那个进程的owner.


1.10 时间

纵观历史,Unix维护这两种不同的时间:
1. 日历时间: 这个时间就是从纪元开始(1970.1.1 00:00:00 UTC),到现在的秒数,协调的通用时间(UTC). 这些时间值是用来记录文件修改
的时间的.比如一种数据类型time_t就是存储这些时间值的.
这里写图片描述
我们可以看到,在bits/types.h中第139行有定义time_h这个数据类型.
这里写图片描述
在bits/typesizes.h中对其__TIME_T_TYPE进行了定义.
2. 进程时间
这个就是传说中的CPU时间, 并且通过一个进程测试CPU. 进程时间用时钟周期进行测量.一般都是每秒钟50,60或者100次.clock_t就是进程时间的数据类型.
当我们测试准确的进程时间的时候,我们会在Unix上得到三个时间:
·clock时间
表示这个进程执行了的时间,这个取决于有多少个进程在系统上正在跑.
·user CPU时间
表示这个进程在用户空间手中掌控的时间
·系统CPU时间
表示这个进程在内核手中掌控的时间
所以一般情况下user CPU + system CPU就是CPU时间.
这里写图片描述


1.11 系统调用和库函数

任何的系统都会有指向调用内核的函数入口. Unix中就提供了定义规范的进入内核的入口点,这就是系统调用(system call)。具体系统调用函数
的数量会随着内核版本不同而不同.比如4.4BSD就有110个,SVR4就有120个, Linux3.2有380个, FreeBSD8.0有450个.
在Unix上系统调用函数都有对应同名的C标准函数(这里说的并不是man3, 而是直接说道man2), 用户进程调用这些C函数,然后这些函数再通过一些机制唤醒内核服务. 比如,某个函数能 将一个或多个C参数放到通用寄存器中然后执行一些机器指令生成一个软中断给内核.
man3是标准的C函数库,大部分是不能调用内核函数的,但是有一些可以,比如printf就是直接调用内核的write系统调用来输出string.
这里举个例子,我们都清楚malloc(3)这个数是用来分配内存的,但是对这个函数目前没有任何优化. 但是Unix内核处理内存分配是用的sbrk(2)
,这个函数通过增加或减少进程地址空间通过指定的字节数. 但是malloc只是实现了一种类型的内存管理,如果我们不想用这个函数,我们完全可以直接使用sbrk系统调用.大量的软件其实都是用sbrk实现他们自己的内存非配算法的.
这里写图片描述
类似的当前时间问题,Unix内核进行的工作是获取到UTC从纪元开始到现在的秒数,然后具体根据时区等转换成可读性的时间是扔给了用户
空间.
用户空间是可以直接调用系统调用的,但是绝大部分的系统调用还是被库函数调用.比如fork, exec, waitpid
这里写图片描述


联系方式: reyren179@gmail.com

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值