APUE UNIX环境高级编程笔记

Posted by Bigzhao on December 18, 2017

第一章 UNIX 基础知识

  • 系统调用:UNIX内核的接口称之为系统调用。公用函数库构建在系统调用接口之上。应用程序既可以使用公用函数库,也可以使用系统调用。

  • Errno**:当 UNIX 系统函数出错时,通常会返回一个负值,同时整型变量 errno 通常被设置为具有特定信息的值。

文件 <errno.h> 定义了 errno 以及赋予它的各种常量,这些常量以 E字符开头

在多线程环境中,每个线程都有属于自己的局部 errno,以避免一个线程干扰另一个线程。

  • 时间:日历时间:自 UTC 1970年1月1日 00:00:00 以来经历过的秒数累计值。用 time_t数据类型来保存这种时间值。

  • 进程时间:也称作CPU时间,用于度量进程使用的CPU资源。进程时间以时间滴答来计算,用clock_t数据类型保存这种时间值。

第二章 UNIX 标准及实现

  • ISO C: 意图是提供C程序的可移植性,使得它能够适合于大量不同的操作系统。

  • –POSIX: 指的是可移植操作系统接口。该标准的目的是提升应用程序在各种UNIX系统环境之间的可移植性。它包含了ISO C的标准库函数。

第三章文件IO:不带缓冲的IO

  • 文件描述符:一个非负整数,范围是0~OPEN_MAX-1。内核用它来标识进程正在访问的文件。当进程创建时,默认为它打开了3个文件描述符,它们都链接向终端:

0: 标准输入

1: 标准输出

2: 标准错误输出

文件共享:内核使用三种数据结构描述打开文件。它们之间的关系决定了一个进程与另一个进程在打开的文件之间的相互影响。

内核为每个进程分配一个进程表项(所有进程表项构成进程表),进程表中都有一个打开的文件描述符表。每个文件描述符占用一项,其内容为:

文件描述符标志

指向一个文件表项的指针

内核为每个打开的文件分配一个文件表项(所有的文件表项构成文件表)。每个文件表项的内容包括:

文件状态标志(读、写、添写、同步和阻塞等)

当前文件偏移量

指向该文件 v 结点表项的指针

每个打开的文件或者设备都有一个 v 结点结构。 v 结点结构的内容包括:

文件类型和对此文件进行各种操作函数的指针。

对于大多数文件, v 结点还包含了该文件的 i 结点。

这些信息都是在打开文件时从磁盘读入内存的。如 i 结点包含了文件的所有者、文件长度、指向文件实际数据在磁盘上所在位置的指针等等。 v 结点结构和 i 结点结构实际上代表了文件的实体。

dup/dup2:复制一个现有的文件描述符


#include&lt;unistd.h&gt;

int dup(int fd);

int dup2(int fd,int fd2);

延迟写:UNIX操作系统在内核中设有缓冲区,大多数磁盘 I/O 都通过缓冲区进行。当我们向文件写入数据时,内核通常都首先将数据复制到缓冲区中,然后排入队列,晚些时候再写入磁盘。这种方式称为延迟写。

fcntl:改变已经打开的文件的属性。例子:fcntl(fd, F_SETFL, O_NONBLOCK)改成非阻塞io

第四章文件和目录

stat:返回文件或者目录的信息结构

文件系统

  • 一个磁盘可以划分成一个或者多个分区,每个分区可以包含一个文件系统。每个文件系统包含一些柱面组。每个柱面组包括:

  • 一个 i 节点图:用于指示哪些 i 节点已经被使用,哪些未被使用

  • 一个 块位图:用于指示哪些数据块已经被使用,哪些为被使用

  • 一个 i 节点组。它包含的是许多 i 节点。

  • 一个数据区:存放具体的数据块和目录块

      - 数据区包含两种类型的块:
    
      - 目录块:它的内容是 `&lt;i 节点编号&gt;|&lt;文件名&gt;` 这种格式的记录的列表
    
      - 数据块:它的内容就是具体文件的数据
    
      - i 节点是固定长度的记录项,它包含有关文件的大部分信息
    
      - 每个 i 节点都有一个链接计数,其值是指向 i 节点的目录的项数(这种链接类型称之为硬链接)。只有当该链接计数减少为0时,才可以删除该链接文件(也就是释放该文件占用的数据块)。
    
  • stat结构中,链接计数包含在st_nlink成员中(POSIX常量:LINK_MAX指定了一个文件链接数的最大值)

      - 每个 i 节点包含了文件有关的所有信息:文件类型、文件权限访问位、文件长度和指向文件数据块的指针
    
      - `stat`结构中的大多数信息来自于 i  结点。只有两项重要数据存放在目录项中:文件名、i节点编号
    
      - 目录项中的 i 节点编号只能指向同一个文件系统中的相应的 i 节点。
    
      &gt; 因此硬链接不能跨文件系统
    
  • 当在不更换文件系统的情况下重命名一个文件时,该文件的实际内容并未移动。只需要构造一个指向现有 i 节点的新目录项,并删除来的目录项。此时该 i节点的链接计数不会改变

      &gt; 这就是 `mv`命令的操作方式
    

硬链接:硬链接直接指向文件的i节点

软、符号连接:软链接是对一个文件的间接指针,内容系文件的位置信息

文件模式创建屏蔽字:当进程创建一个新的目录或者文件时,会使用文件模式创建屏蔽字。

  • 进程通过open或者creat创建一个新文件

chown:修改用户的ID和组ID.

    int chown(const char \*pathname,uid\_t owner,gid\_t group);

umask

进程有关的ID:

    - 实际用户 ID 和实际组 ID: 标志我们究竟是谁。当我们登录进操作系统时,这两个值就确定了!

    - 有效用户 ID、有效组ID、附属组 ID: 用于文件访问权限检查。

    - 保存的设置用户ID、保存的设置组ID:由 `exec`函数保存

Unlink:删除一个现有的目录项

Remove:解除对一个目录或者文件的链接。

读、写目录:对于某个目录具有访问权限的任何用户都可以读该目录。但是为了防止文件系统产生混乱,只有内核才能写目录。

    &gt; 一个目录的写权限和执行权限位决定了在该目录中能否创建新文件以及删除文件,它们并不能写目录本身

当前工作目录:每个进程都有一个当前工作目录。此目录是搜索所有相对路径名的起点。

    &gt; 当前工作目录是本进程的一个属性

第五章 标准IO库

FILE指针:当使用fopen函数打开一个流时,它返回一个执行FILE对象的指针。该对象通常是一个结构,包含了标准IO库为管理该流所需要的所有信息,包括:

  • 用于实际IO的文件描述符

  • 指向用于该流缓冲区的指针

  • 该流缓冲区的长度

  • 当前在缓冲区中的字符数

  • 出错标志

标准IO库提供缓冲的目的是:尽量减少使用read和write调用的次数。标准IO库对每个IO流自动地进行缓冲管理,从而避免了程序员需要手动管理这一点带来的麻烦。

标准IO库提供了三种类型的缓冲

Fopen

Fclose

对流进行读、写操作

格式化IO

标准化IO缺点:效率不高。

第六章 系统信息

阴影口令文件:现在的UNIX将加密口令存放在一个称作阴影口令的文件中(即文件/etc/shadow)。该文件至少应该包含用户名和加密口令。其他字段都是用于控制口令更改的频率。

第七章 进程环境

C程序启动过程:当内核执行 C 程序时,在调用main之前先调用一个特殊的启动例程。

内核执行C程序是通过使用一个exec函数实现的

可执行程序文件将此启动例程指定为程序的其实地址(这是由链接器设置的,而链接器由C编译器调用)

启动例程从内核取得命令行参数和环境变量值,然后为调用main函数做好安排

进程终止方式:8种。

环境表:与参数表一样,环境表也是一个字符指针数组

C程序的存储空间布局

malloc/calloc/realloc:动态分配存储空间


#include&lt;stdlib.h&gt;

void \*malloc(size\_t size);

void \*calloc(size\_t nobj,size\_t size);

void \*realloc(void \*ptr,size\_t newsize);

环境变量:环境字符串的形式是:name=value


char \*getenv(const char\*name)

int putenv(char \*str);

int setenv(const char \*name,const char \*value,int rewrite);

int unsetenv(const char \*name);

setjmp 和 longjmp:跨函数跳转功能.

getrlimit/setrlimit:每个进程都有一组资源限制,其中一些可以通过getrlimit/setrlimit函数查询和修改. 例子:关闭所有打开的文件描述符


getrlimit(RLIMIT\_NOFILE, &amp;rl);

For (int i = 0; I &lt; rl.rlim\_max; i++)

   Close(i);

第八章 进程控制

进程 ID: 内核用于唯一标识进程的一个数值 .ID = 0 是调度进程(交换进程),id=1是init进程


#include&lt;unistd.h&gt;

pid\_t getpid(void);  // 返回值:调用进程的进程ID

pid\_t getppid(void); // 返回值:调用进程的父进程ID

Fork:创建一个子进程。一次调用,两次返回,对于父进程返回子进程的id,对于子进程返回0.原因是因为子进程能够通过getpid getppid等api知道自己的pid,但是父进程没有途径能够知道子进程的id。子进程完全复制了父进程的地址空间(正文段、堆栈等)

COW写时复制:所以很多操作系统的实现并不执行一个父进程数据段、堆和栈的完全拷贝,而是使用写时赋值技术(copy-on-write:COW)这些区域由父进程和子进程共享,而且内核将它们的访问权限改变为只读,如果父子进程中有一个试图修改这些区域,则内核只为修改区域的那块内存制作一个副本

父子进程文件共享:父进程所有打开的文件描述符都复制到子进程中,父子进程共同相同的文件表项。(共享文件的偏移量)

父进程和子进程的区别:fork返回值不同,Pid不同,ppid不同,子进程不继承父进程的文件锁。

vfork:vfork用于创建一个新进程,该新进程的目的是exec一个新程序,所以vfork并不将父进程的地址空间拷贝到子进程中。

僵死进程:一个已经终止、但是等待父进程对它进行善后处理的进程

善后处理:父进程调用wait函数或者waitpid函数读取终止进程的残留信息

SIGCHLD:当一个进程终止时,内核就向其父进程发送SIGCHLD信号。

wait/waitpid

  • 如果pid==-1:则等待任意一个子进程终止

  • 如果pid>0:则等待进程ID等于pid的那个子进程终止

  • 如果pid==0:则等待组ID等于调用进程组ID的任一子进程终止

  • 如果pid<0:等待组ID等于pid绝对值的任一子进程终止

Exec:当进程调用一种exec函数时,该进程执行的程序完全替换成新程序,而新程序则从main函数开始执行。(实质上只是用磁盘上的一个新程序替换了当前进程的正文段、数据段、堆段和栈段)

System:在程序中执行一个命令字符串

最小特权模型:程序只有给定认为所需的最小的特权。

setuid/setgid

seteuid/setegid

进程调度:UNIX系统的调度策略和调度优先级是内核确定的。进程可以通过调整nice值选择以更低优先级运行

Nice:越小优先级越高

第九章 进程关系

终端登录: init–>init–>getty->login–>getpswdname–>crypt–>shell

进程组:进程组是一个或者多个进程的集合。

getpgrp/getpgid

setpgid

会话:是一个或者多个进程组的集合

Setsid/ getsid

Setsid出错原因 一般做法:如果当前进程是进程组组长,报错。反之则创建一个新的会话,当前进程是会话的首进程(控制进程),并创建一个进程组,当前进程是组长。

作业控制:一个作业由一个或者多个进程组成。只有一个前台作业和任意多的后台进程。

孤儿进程组:一个进程组不是孤儿进程组的条件是:该进程组中存在一个进程,其父进程在属于同一个会话的另一个组中。

第十章 信号

信号的概念:信号是软中断,它提供了一种处理异步事件的方法。

产生信号:1.用户按了某些终端键2.硬件引发的错误3.进程调用kill4.用户调用kill

信号处理:1.忽略 2.捕捉 3.执行系统默认操作

可重入函数:对于某一类函数,进程执行过程中接收到信号,中断处理程序中也能安全的重复调用的函数。一般来说:1.带io操作不行2.malloc操作不行3.全局、静态变量不行

低速系统调用:是可能使进程永远阻塞的一类系统调用

kill/raise:发送信号 raise:对当前进程发送信号

对于kill函数:

pid:接受信号的进程或者进程组。分为四种情况:

  • pid >0:将信号发送给进程ID为pid的进程

  • pid==0:将信号发送给与发送进程属于同一个进程组的所有其他进程(这些进程的进程组ID等于发送进程的进程组ID ),且发送进程必须要有权限向这些进程发送信号

注意:这里的所有其他进程不包括内核进程和init进程

  • pid<0:将该信号发送给其进程组ID等于pid绝对值,且发送进程具有权限向其发送信号的所有进程。

注意:这里的所有其他进程不包括内核进程和init进程

  • pid==-1:将信号发送给发送进程有权限向他们发送信号的所有进程。

注意:这里的所有其他进程不包括内核进程和init进程

alarm:为进程设置定时器 SIGALRM

pause:阻塞调用进程直到捕捉到一个信号

sigaction

abortSIGABRT

sleep/nanosleep/clock_nanosleep

第十章 线程

线程处理优点

一个进程中的所有线程都可以访问该进程的资源,如文件描述符、内存等等。

线程退出方式

pthread_join等待指定的线程结束(类似于waitpid)

线程同步


pthread\_mutex\_init/ pthread\_mutex\_destroy

pthread\_mutex\_lock/pthread\_mutex\_trylock/pthread\_mutex\_unlock

避免死锁

读写锁


pthread\_rwlock\_init/pthread\_rwlock\_destroy

pthread\_rwlock\_rdlock/pthread\_rwlock\_wrlock/pthread\_rwlock\_unlock

pthread\_rwlock\_tryrdlock/pthread\_rwlock\_trywrlock

pthread\_rwlock\_timedrdlock/pthread\_rwlock\_timedwrlock

条件变量


pthread\_cond\_init/ pthread\_cond\_destroy

pthread\_cond\_wait/pthread\_cond\_timedwait

pthread\_cond\_signal/pthread\_cond\_broadcast

自旋锁


pthread\_spin\_init/ pthread\_spin\_destroy

pthread\_spin\_lock/pthread\_spin\_trylock/pthread\_spin\_unlock

第十一章 线程控制

线程属性

pthread_attr_init/pthread_attr_destroy

函数是线程安全:如果一个函数在相同的时间点,可以被多个线程安全的调用,则称该函数是线程安全的

线程特定数据:线程特定数据也称作线程私有数据,是存储和查询某个线程相关数据的一种机制。我们采用线程特定数据的原因是:我们希望每个线程都可以访问线程各自独立的数据副本,而不需要担心与其他线程的同步问题。因为:

  • –有时候需要维护基于线程的数据
  • –线程特定数据让基于进程的接口适应于多线程环境。比如errno:为了让线程中可以使用原本基于进程的系统调用和库例程,errno被重新定义为线程私有数据。这样一个线程中做了重置errno的操作也不会影响进程中其它线程的errno值

pthread\_key\_create/ pthread\_key\_delete

pthread\_setspecific/pthread\_getspecific

线程和 fork

Pread/pwrite

第十三章 守护进程

守护进程daemon:生存期长的一种进程

  • 它们经常在系统引导启动时启动,仅在系统关闭时终止

  • 它们没有控制终端,因此是在后台运行的

  • UNIX系统有很多守护进程,它们执行日常事务活动

产生日志消息

单例守护进程

为了创建单例守护进程,可以使用文件和记录锁。

如果为守护进程创建一个固定名字的文件,并且在该文件的整体上加一把写锁,那么只允许创建一把这样的写锁。在此之后创建写锁的任何尝试都将失败。

如果某个守护进程对该文件创建写锁失败,则说明已经存在了一个守护进程的副本

当持有文件写锁的守护进程终止时,这把锁将被自动删除

第十四章 高级io

非阻塞IO:非阻塞IO能够使得我们可以在执行open、read、write等操作时永远不会阻塞。如果这些操作不能立即完成,则调用立即返回失败。

两种方法为其指定非阻塞IO:

  • 对于尚未打开的描述符,如果调用open获取描述符,则可以对open函数指定O_NONBLOCK标志

  • 对于已经打开的描述符,则可以调用fcntl函数,通过该函数打开O_NONBLOCK文件状态标志

    fcntl(fd,F\_SETFL,O\_NONBLOCK)
    

记录锁:当一个进程正在读或者修改文件的某个部分时,使用记录锁可以阻止其他进程修改同一个文件区。

两种类型的记录锁:共享式读锁、独占式写锁

IO多路转换:poll select pselect 先准备一个文件描述符表 调用以上函数 等到io准备好了才返回。

异步IO:不会阻塞,通过信号通知进程去处理。缺点:不能分辨出是哪个io准备好了。

Mmap/munmap:储存映射io。将文件映射到缓冲区中,读 缓冲区相当于读文件,写相当于将字节写进文件中。