下图展示了各种I/O包:

系统级I/O

  • Unix I/O模型是在操作系统内核实现的。应用程序可以通过诸如open, close, lseek, read, write, stat这样的函数来访问Unix I/O。
  • 较高级别的RIO和标准I/O函数都是基于Unix I/O函数实现的。RIO函数是专门位本书开发的readwrite的健壮的包装函数。
  • 标准I/O函数提供了Unix I/O函数的一个更加完整的带缓冲的替代品,包括格式化I/O例程。
  • 使用原则:
    • G1: 只要有可能就是用标准I/O。对磁盘和终端设备I/O来说,标准I/O是首选方法。
    • G2: 不要使用scanfrio_readlineb来读取二进制文件,像scanfrio_readlineb这样的函数是专门设计来读取文本文件的。
    • G3: 对网络套接字的I/O使用RIO函数。
  • 标准I/O流,从某种意义上而言是全双工的,因为程序能够在同一个流上执行输入和输出。然而,对流的限制和对套接字的限制,有时候会相互冲突。
    • 限制一: 跟在输出函数之后的输入函数。如果中间没有插入对fflushfseek,fsetpos或者rewind的调用,一个输入函数不能跟随在一个输出函数之后。fflush函数清空与流相关的缓冲区。后三个函数使用Unix I/O lseek函数来重置当前的文件位置。
    • 限制二: 跟在输入函数之后的输出函数。如果中间没有插入对fseekfsetpos或者rewind的调用,一个输出函数不能跟随在一个输入函数之后,除非该输入函数遇到了一个文件结束。
    • 这些限制给网络应用来了一个问题,因为对套接字使用lseek是非法的。因此,在网络套接字上不要使用标准I/O函数来进行输入和输出,而要使用健壮的RIO函数。

网络编程

  • 套接字接口是一组函数,它们和Unix I/O函数结合起来,用以创建网络应用。
  • 大多数现代系统上都实现套接字接口,包括所有的Unix变种,Windows和Macintosh系统。

并发编程

  • 如果逻辑控制流在时间上重叠,那么它们就是并发的。
  • 现代操作系统提供了三种基本的构造并发程序的方法:
    • 进程。因为进程有独立的虚拟地址空间空间,想要与其他逻辑控制流通信,控制流必须使用某种显式的进程间通信机制(IPC)。
    • I/O多路复用。
    • 线程。 线程是运行在一个单一进程上下文中的逻辑流,由内核进行调度,你可以把线程看成是两种方式的混合体。

基于线程的并发编程

  • 线程就是运行在进程上下文中的逻辑流。
  • 现代系统允许我们编写一个进程同时运行多个线程的程序。线程由内核自动调度。每个线程都有它自己的线程上下文,包括一个唯一的线程ID,栈,栈指针,程序计数器,通用目的寄存器和条件码。所有运行在一个进程里的线程共享该进程的整个虚拟内存地址。
  • 多线程的执行模型在某些方面和多进程的执行模型是相似的。每个进程开始生命周期时都是单一线程,这个线程称为主线程。在某一时刻,主线程创建一个对等线程,从这个时间点开始,两个线程就并发地运行。最后,因为主线程执行一个慢速系统调用,例如read或者sleep,或者因为被系统的间隔计时器中断,控制就会通过上下文切换传递到对等线程。对等线程会执行一段时间,然后控制传递回主线程,以此类推。
    • 线程的上下文切换要比进程的上下文切换快得多。
    • 线程不像进程那样,不是按照严格的父子层次来组织的。
    • 主线程和其他线程的区别仅在于他总是进程中第一个运行的线程。
    • 一个线程可以杀死它的任何对等线程。
  • Posix线程是C程序中处理线程的一个标准接口。Pthreads定义了大约60个函数,允许程序创建,杀死和回收线程,与对等线程安全地共享数据,还可以通知对等线程系统状态的变化。
  • 线程可以通过调用pthread_create函数来创建其他线程。
      #include <pthread.h>
      typedef void *(func)(void *);
      int pthread_create(pthread_t *tid, pthread_attr_t *attr, func *f, void *arg);
    
  • pthread_create函数创建一个新的线程,并带有一个输入变量arg,在新线程的上下文中运行线程例程f。能用attr参数来改变新创建线程的默认属性。
  • pthread_create返回时,参数tid包含新创建线程的ID。新线程可以通过调用pthread_self函数它自己的线程ID
      #include <pthread.h>
      pthread_t pthread_self(void);
    
  • 线程终止方式:
    • 当顶层的线程例程返回时,线程会隐式地终止。
    • 通过调用pthread_exit函数,线程会显式地终止。如果主线程调用pthread_exit,它会等待所有其他对等线程终止,然后再终止主线程和整个进程,返回值为pthread_return
        #include <pthread.h>
        void pthread_exit(void* thread_return);
      
    • 如果某个对等线程调用Linux的exit函数,该函数终止进程以及所有与该进程相关的线程。
    • 另一个对等线程通过以当前线程ID作为参数调用pthread_cancel函数来终止当前线程。
        #include <pthread.h>
        int pthread_cancel(pthread_t tid);
      
    • 线程通过调用pthread_join函数等待其他线程终止。
      #include <pthread.h>
      int pthread_join(pthread_t tid, void **thread_return);
      
    • pthread_join函数会阻塞,直到线程tid终止,当线程例程返回的通用指针复制为thread_return指向的位置,然后回收已终止线程占用的所有内存资源。
    • 在任何一个时间点上,线程是可结合的或者是分离的。一个可结合的线程能够被其他线程收回和杀死。在被其他线程回收之前,它的内存资源是不释放的。相反,一个分离的线程是不能被其他线程回收或杀死的,它的内存资源在它终止时由系统自动释放。
    • 默认情况下,线程被创建为可结合的。为了避免内存泄漏,每个可结合线程都应该要么被其他线程显式地收回,要么通过调用pthread_detach函数被分离。
        #include <pthread.h>
        int pthread_detach(pthread_t tid);
      
    • 线程能够通过以pthread_self为参数的pthread_detach调用来分离它们自己。

多线程程序中的共享变量

  • 线程很容易共享相同的程序变量。
  • 一个变量是共享的,当且仅当多个线程引用这个变量的某个实例。
  • 寄存器是从不共享的,而虚拟内存总是共享的。
  • 多线程的C程序中变量根据它们的存储类型被映射到虚拟内存:
    • 全局变量。 全局变量是定义在函数之外的变量。在运行时,虚拟内存的读/写区域值包含每个全局变量的一个实例,任何线程都可以引用。
    • 本地自动变量。 本地自动变量就是定义在函数内部但是没有static属性的变量。在运行时,每个线程的栈都包含它自己的所有本地自动变量的实例
    • 本地静态变量。 本地静态变量是定义在函数内部并有static属性的变量。和全局变量一样,虚拟内存的读写区域只包含在程序中声明的每个本地静态变量的一个实例。

用信号量同步线程

  • volatile: 编译器在每次用到这个变量时都必须小心的重新读取这个变量的值,而不是使用保存在寄存器中的备份。