深入理解计算机操作系统(笔记)
文章出自:https://www.cnblogs.com/zzdbullet/p/9540436.html
第一章 计算机系统漫游
- C文件编译过程:.c文件–预处理器–.i文件–编译器–.s文件(汇编语言)–汇编器–.o文件(机器语言指令,可重定位目标程序)–链接(调用printf)–可执行文件
- 总线:携带信息并负责在各个部件间传递
- I/O设备:系统与外部世界的联系通道,通过控制器或适配器与设备连接,控制器通常是主电路板的芯片组,适配器为插在主板插槽上的卡
- 主存:在处理器执行程序时,用来存放程序和程序处理的数据(DRAM)
- 处理器CPU:执行存储在主存的指令(不断执行程序计数器指向的指令),包含程序计数器,寄存器和ALU
- 加载:主存到寄存器
- 存储:寄存器到主存
- 操作:把两个寄存器的内容利用ALU(算术/逻辑单元)运算,结果放到寄存器
- 跳转:从指令抽取一个字,到程序计数器(PC)中
- 程序运行步骤:
- ”hello”字段从键盘通过I/O桥经总线传输至寄存器
- 寄存器将字段存入主存
- 敲击回车后,shell指令将代码和数据从磁盘复制到主存,输出”helloworld”
- 处理器执行main函数的机器语言指令,将输出字符串加载,在从寄存器复制到显示设备
- 并发执行:一个进程的指令和另一个进程的指令是交错执行的
- 操作系统实现这种交错执行的机制叫上下文切换,保持跟踪进程运行所需的所有状态信息
- 进程的切换由操作系统的内核进行管理
- 虚拟内存提供一个假象,即每个进程都在独占地使用内存,基本思想是把一个进程虚拟内存的内容存储在磁盘上,然后用主存作 为磁盘的高速缓存
- Amdahl定律:想要显著加速整个系统,必须提升全系统中相当大的部分的速度
- 操作系统内核是应用程序和硬件之间的媒介,它提供了三个抽象:
- 文件是对I/O设备的抽象
- 虚拟内存是对主存和磁盘的抽象·
- 进程是处理器、主存和I/O设备的抽象
第二章 信息的表示和处理
- 对于一个字长为w位的机器而言,虚拟地址的范围为0~2w-1,程序最多访问2w个字节
- x&0xFF,0xFF可以被看做掩码
- C语言标准没有明确定义对于有符号数应该使用哪种类型的右移,一般来说有符号数使用算术右移,而无符号数必须是逻辑右移
- 双射:y=f(x)—–f^(-1)(y)=x
- 53191的补码和-12345的补码相同,他们两个的和为65536,即2的16次方,对于其他位数的数的规律也是如此
- 创建一个无符号常量,必须在后面加U,0x1234默认为有符号型,而0x1234U表示无符号型
- 截断有符号数与截断无符号数,截断补码
- %d输出的是有符号数,输出无符号数用%u
- 有符号数的前扩展是根据符号位,无符号数的前扩展是0
- 正数x+y的和如果没有溢出,则是正常值,如果溢出了,计算结果为x+y-2w,其中w为x和y的位数。如果是负数,结果为x+y+2w
- 将一个无符号数截断为w位等价于计算该值对于2的w次方的余数
第三章 程序的机器级表示
- 处理器状态:
- 程序计数器:(%rip)给出将要执行的下一条指令在内存中的地址
- 整数寄存器:存储地址(指针)或者整数数据(参数和局部变量等)
- 条件寄存器:保存最近执行的算术或逻辑指令的状态信息
- 一组向量寄存器可以存放一个或多个整数或浮点数值
- 反汇编器可以查看机器代码文件的内容
- 在编译生成的汇编文件中,所有以‘.’开头的行都是指导汇编器和连接器工作的伪指令,他们告诉汇编器调整地址,以便在那儿产生代码或插入一些数据
- 条件数据传送
- 过程
- 栈向低地址的方向增长,即栈顶的地址最小
- 转移控制:将控制送函数P转移到函数Q只需要简单地把程序计数器设置为Q的代码的起始位置。不过,当稍后从Q返回的时候,处理器必须记录好它需要继续P的执行的代码位置。在x86_64机器中,这个信息是用指令call Q调用过程Q来记录的。该指令会把地址A压入栈中,并将PC设置为Q的起始地址。压入的地址A被称为返回地址
- 数据传送:当过程P调用过程Q时,P的代码必须首先把参数复制到适当的寄存器中。当Q返回到P时,P的代码可以访问寄存器中的返回值
- x86-64中,可以通过寄存器最多传递6个整型(例如整数和指针)参数。如果函数的参数大于6个,则1-6个参数在寄存器中,参数7-n放到栈上,而参数7位于栈顶
- 将指针从一种类型强制转换成另一种类型,只改变它的类型,而不改变它的值。如果p是一个char类型的指针,它的值为p,那么表达式(int)p+7计算为p+28,而(int*)(p+7)计算为p+7
- 缓冲区溢出:程序向缓冲区中写数据,但内容超过了程序员设定的缓冲区边界,从而覆盖了相邻的内存区域,造成覆盖程序中的其他变量甚至影响控制流的敏感数据,造成程序的非预期行为
- nop:对程序计数器加一
- 防止缓冲区溢出攻击的方法:
- 栈随机化技术:地址空间布局随机化(ASLR),每次运行时程序的不同部分,包括程序代码,库代码,栈,全局变量和堆数据,都会被加载到内存的不同区域
- 栈破坏检测:最近的GCC版本在产生的代码中加入了一种栈保护者机制,用来检测缓冲区越界,其思想是在栈中任何局部缓冲区与栈状态之间存储一个特殊的金丝雀值(canary)。这个金丝雀值是在程序每次运行时随机产生的,因此,攻击者没有简单的办法知道它是什么。在恢复寄存器状态和从函数返回之前,程序检查这个金丝雀值是否被该函数的某个操作或者函数调用的某个操作改变了。如果是,那么程序异常终止
- 限制可执行代码区域
第四章 处理器体系结构
- 硬件和操作系统软件联合起来将虚拟地址翻译成实际或物理地址,指明数据实际存在内存中哪个地方
- Stat描述程序执行的总体状态
- HDL与C的区别:
- 组合电路的输出持续随输入变化,而C语言只会在程序执行中被遇到时才求值
- C语言中0为false,其余都为true;而HDL只有0和1
- C语言表达式可能被部分求值(例如&&、||),而HDL没有这种规则,只是简单地相应输入的变化
第五章 优化程序性能
- 妨碍程序优化的因素
- 内存别名使用:对于指针的优化计算,需要考虑两个指针是否指向同一个内存位置
- *p+=q;p+=q;如果p和q指向不同的内存,则p+=2 q;如果p和q指向相同内存,那么前者起始是p增加4倍,而后者p增加了3倍
- x=1000;y=3000; *q=y; *p=x; t1=*q; 如果p和q指向不同内存,则t1=3000;如果指向相同内存t1=1000
- 内存别名使用:对于指针的优化计算,需要考虑两个指针是否指向同一个内存位置
- 同样的函数调用,如果在函数中修改了全局变量即count++,则2*fun();和fun()+fun()的结果不一样,虽然前者对函数的调用次数更少
- GCC编译器使用-Og可以优化程序性能
- 在循环中不断测试的判断语句可以提出来,消除循环的低效率,例如for(i=0;i<strlen(a);i++){…}中每次判断都会执行strlen函数,可以提前在循坏外做好
- 在循环中的*p = *p + num[i]; 这里需要对内存做两次读和一次写;而a = a + num[i]; 跳出循环后 *p = a;在循环中只需要对内存做一次读;
- 循环展开:通过增加每次迭代计算的元素的数量,减少循环的迭代次数。注意在做变换后边界值的判断
- 提高并行性
- 在循环中sum=(sum*num[i])num[i+1];和sum=sum(num[i]*num[i+1]);的区别是,前者第一次乘法需要等待累积值sum在进行第二次乘积,而后者的第一次乘法不需要等待前一次迭代的累积值就可以执行
- 将1,2,…,n-1,n的乘积分别计算序列中奇数和偶数的乘积,最后将两者相乘,可以改善程序性能。同时可以考虑分成更多的份数(k)
- 如果并行度p超过了可用的寄存器数量,那么编译器会出现寄存器溢出,将某些临时值放到内存中,反而增加了程序消耗
- GPROF工具可以对程序进行剖析,列出各个函数执行的时间等信息
- 优化方法总结:
- 选择适当的算法和数据结构,要提高警惕,避免渐近低效率
- 消除连续的函数调用,有可能的时候将计算移动到循环体外
- 消除不必要的存储器引用,引入临时变量保存中间值
- 展开循环,降低开销
- 提高并行,使用多个累积变量或者重新结合,用良好的风格重新条件操作
第六章 存储器层次结构
- CPU——高速缓存存储器——主存缓存
- 随机访问存储器:易失
- SRAM:静态随机访问存储器,用来作为高速缓存存储器,既可以在CPU芯片上,也可以在片下
- DRAM:动态随机访问存储器,用来作为主存以及图形系统的帧缓冲区,对干扰非常敏感,暴露在光线下会导致电容电压改变
- DDRn(DDR SDRAM),主存内存把数据排列后成nbit倍同步发送
- 读事务:movq A, %rax
- CPU将地址A放到总线接口→I/O桥→主存
- A的内容从主存→I/O桥→CPU总线接口
- CPU总线接口→寄存器
- 写事务:movq %rax, A
- CPU将地址A放到总线接口→I/O桥→主存
- CPU将%rax的数据放到总线接口→I/O桥→主
- 主存从内存总线读出数据,并且将这些位存储到DRAM
- 主机总线适配器将一个或多个磁盘连接到I/O总线,使用的是一个特别的主机总线接口定义的通信协议,最常用的磁盘接口是SCSI和SATA
- CPU对磁盘地址0xa0读取内容
- 发送三个存储指令
- 发送一个命令字,告诉磁盘发起一个读,同时还发送了其他参数
- 指明应该读的逻辑块号
- 指明应该存储磁盘扇区内容的主存地址
- 磁盘将逻辑块号翻译成一个扇区地址,读取对应内容后传送到内存。设备可以自己执行读或者二写总线事务而不需要CPU的干涉,称为直接内存访问(DMA)
- 磁盘给CPU发送一个中断信号通知CPU,CPU跳转到一个操作系统例程,这个程序记录I/O已完成,将控制返回到CPU被中断的地方
- 发送三个存储指令
- 局部性原理:引用邻近于其他最近引用过的数据项的数据项,或者最近引用过的数据项本身。
- 分为时间局部性和空间局部性
- 计算机——高速缓存存储器
- 一个连续向量中,每隔k个元素进行访问,就成为步长为k的引用模式。多维数组如果按列在最外循环,则引用模式为N;如果行在最外,引用模式为1
- C数组在内存中是按照行顺序来存放的
- 评价局部性的原则:
- 重复引用相同变量的程序有良好的时间局部性
- 具有步长为k的引用模式的程序,步长越小,空间局部性越好
- 对于取指令来说,循环有好的时间和空间局部性
- 存储器层次结构的中心思想是,对于每个k,位于k层的更快更小的存储设备作为位于k+1层的更大更慢的存储设备的缓存
- 高速缓存:每个存储地址有m位,共有2m个不同的地址,2s个高速缓存组,每组有E行,每行有一个2^b字节的数据块组成,容量C=SBE
- 高速缓存抽取出被请求的字的过程(即读):组选择;行匹配;字抽取
- 抖动:高速缓存反复地加载和驱逐相同的高速缓存块的组
- 直接映射高速缓存:每组只有一行,组号为索引
- 组相联高速缓存:每组多行,组号为索引了,行匹配和字选择:key为标记和有效位,value为块的内容
- 全相联高速缓存:一个组,行匹配和字选择:key为标记和有效位,value为块的内容
- 高速缓存写:
- 直写:立即将高速缓存写回低一层(非写分配,避开高速缓存,直接写回低一层)
- 写回:推迟更新,直到需要被驱逐这个更新过的块时,才写到低一层(写分配,加载低一层的块到高速缓存,更新这个高速缓存)
- 程序运行方式总结:
- 将注意力集中在内循环上,大部分计算的存储器访问都集中在这里
- 按照数据实际在存储器中存放的顺序,以步长为1来访问,空间局部性最优
- 一旦从一个存储器中读了一个数据出来,就尽可能多的利用他(kij版本)
第七章 链接
- 链接器必须完成的两个任务
- 符号解析:将每个符号引用正好和一个符号定义关联起来
- 重定位:将每个符号与一个内存文职关联起来
- 目标文件的三种形式:
- 可重定位目标文件,可重定位目标文件可以与其他重定位目标文件合并起来,创建一个可执行目标文件。
- 可执行目标文件,包含二进制代码和数据,可以直接被复制到内存中执行
- 共享目标文件,可以在加载或者运行中被动态地加载进内存并链接
- ELF可重定位目标文件
- ELF头:16字节的数组描述了生成该文件的系统的字的大小和字节顺序。结构体内其余信息描述了目标文件的信息,链接器的语法分析等
- .text:已编译程序的机器代码
- .rodata:只读数据,比如printf语句中的各式串和开关语句的跳转表
- .data:已初始化的全局变量和静态变量。局部变量在运行时被保存在栈中,不在data也不在bss
- .bss:未初始化的全局和静态变量,以及所有被初始化为0的全局或静态变量
- .symtab:符号表,存放在程序中定义和引用的函数和全局变量的信息(判断变量和函数的位置属性,例如extern等)
- .rel.text:.text节中位置的列表,当链接器把这个目标文件和其他文件组合时,需要修改这个位置
- .rel.data:被模块引用或定义的所有全局变量的重定位信息
- .debug:调试符号表
- .line:原始C程序中的行号和.text节中机器指令之间的映射
- .strtab:字符串表,包括.symtab和.debug中的符号表,以及节头部中的节名字
- symtab符号表包含三种符号
- 由模块m定义并能被其他模块引用的全局符号
- 由其他模块定义并被模块m引用的全局符号
- 只被模块m定义和引用的局部符号
- 本地函数中定义的static变量不在栈中管理,编译器在.data或.bss中为每个定义分配空间,并且在符号表中创建唯一名字的链接器符号,即使两个函数的static变量x是一样的,可以采用x.1,x.2
- 链接器具有解析多重定义的全局符号的规则,其中已初始化的函数和全局变量是强符号(包括初始化为0的),未初始化的全局变量是弱符号
- 不允许有多个同名的强符号
- 如果有一个强符号和多个弱符号同名,那么选择强符号
- 如果多个弱符号同名,随机选择
- 静态链接库与动态链接库
- 库打桩
第八章 异常控制流
- 在任何情况下,当处理器检测到有事件发生时,它就会通过一张叫做异常表的跳转表,进行一个间接过程调用,到一个专门设计用来处理这类事件的操作系统子程序(异常处理程序)。
- 异常表的起始地址放在一个叫异常表基址寄存器中
- 异常的类别
- 中断:异步,来自处理器外部I/O设备的信号的结果
- 陷阱:同步,执行当前指令的结果
- 陷阱最重要的用途是在用户程序和内核之间提供一个像过程一样的接口,叫做系统调用。
- 普通的函数运行在用户模式中,用户模式限制了函数可以执行的指令的类型,而且它们只能访问与调用函数相同的
- 系统调用运行在内核模式中,内核模式允许系统调用执行特权指令,并访问定义在内核中的栈
- 在x86_64系统中,系统调用是通过一条称为syscall的陷阱指令来提供的
- 陷阱最重要的用途是在用户程序和内核之间提供一个像过程一样的接口,叫做系统调用。
- 故障和终止:同步,执行当前指令的结果
- 故障无法修正的话,处理程序返回到内核中的abort例程,abort例程会终止引起故障的应用程序
- 进程是一个执行中程序的实例
- 进程的上下文包括存放在内存中的程序的代码和数据,它的栈、通用目的寄存器的内容、程序计数器、环境变量以及打开文件描述符的集合
- 逻辑控制流:程序计数器对进程控制的序列
- 并发流:一个逻辑流的执行在时间上与另一个流重叠,两个流互相并发运行
- 进程的地址空间底部保留给用户程序,依次包括代码,数据,堆和栈段。代码段总是从地址0x400000开始
- 处理器通过控制寄存器中的一个模式位来控制一个应用可以执行的指令和它可以访问的地址空间范围,设置了模式位后,进程运行在内核模式,否则运行在用户模式
- 通过上下文切换机制实现系统进程调度
- 保存当前进程的上下文
- 恢复某个先前被抢占的进程被保存的上下文
- 将控制传递给这个新恢复的进程
- 进程控制
- getpid()返回调用进程的PID
- getppid()返回它的父进程的PID
- exit(int status)以status退出状态来中终止进程
- 父进程通过调用fork函数创建一个新的运行的子进程
- 一个终止了但还未被回收的进程称为僵死进程
- 当一个进程终止时,内核并不是立即把它从系统中清除,而是处于已终止的状态,等待被它的父进程回收。如果父进程终止了,init进程会去回收它们
- 进程可以调用waitpid()函数等待它的子进程终止,wait()是它的简化版本
- sleep()函数让进程挂起一段时间,pause()函数让进程休眠直到该进程收到一个信号
- main函数中的argv
- fork和execve
- 信号
- 允许进程和内核中断其他进程的消息,通知进程系统中发生了一个某种类型的事件
- 发送进程的原因:
- 内核检测到一个系统事件
- 一个进程调用的kill函数,请求内核发送一个信号给目的进程
- 接收信号:信号处理程序
- 待处理信号:发出但没有被接收的信号;如果进程有一个待处理信号,则同类型的信号都不会排队等待,而是直接被丢弃
- kill()函数发送信号给其他进程
- alarm()向自己发送SIGALRM信号,取消任何待处理闹钟,并且返回任何待处理的闹钟在被发送前还剩下的秒数;如果没有待处理闹钟,就返回0
- 阻塞:
- 隐式阻塞:在运行信号处理程序时来了新的信号,则该信号变成待处理而没有被接收
- 显式阻塞:调用sigprocmask函数
- 信号处理程序的要求
- 尽可能简单
- 只调用异步信号安全的函数:可重入;不能被信号处理程序中断
- 保存和恢复errno
- 阻塞所有的信号,保护对共享全局数据的访问
- 用volatile声明全局变量
- 用sig_atomic_t声明标志
- 每种类型最多只能有一个未处理的信号
- 非本地跳转:setjmp,longjmp
第九章 虚拟内存
- 虚拟内存的作用
- 将主存看成是一个存储在磁盘上的地址空间的高速缓存,在主存中只保存活动区域,并根据需要在磁盘和主存之间来回传送数据
- 为每个进程提供了一致的地址空间,简化了内存管理
- 保护了每个进程的地址空间,不被其他进程破坏
- CPU通过生成一个虚拟地址来访问主存,这个虚拟地址在被送到内存之前先转换成适当的物理地址。
- 将一个虚拟地址转换为物理地址的任务叫做地址翻译,这需要CPU硬件和操作系统之间的紧密合作
- CPU芯片上的内存管理单元,利用存放在主存中的查询表来动态翻译虚拟地址,该表的内容由操作系统管理
- 一个包含N=2^n个地址的虚拟地址空间就叫做一个n位地址空间
- 主存中的每个字节都有一个选自虚拟地址空间的虚拟地址和一个选自物理地址空间的物理地址
- 虚拟内存被分割成虚拟页,物理内存被分割成物理页,而虚拟页面的集合被分为三个不相交的子集:
- 未分配的页
- 已缓存在物理内存的已分配页
- 未缓存在物理内存中的已分配页
- 页表将虚拟页映射到物理页,每次地址翻译硬件将一个虚拟地址转换为物理地址时,都会读取页表
- DRAM缓存不命中,称为缺页
- 多个虚拟页面可以映射到同一个共享的物理页面上(联想到fork函数)
- 将一组连续的虚拟页映射到任意一个文件中的任意位置的表示法称作内存映射
- 操作系统将不同进程中适当的虚拟页面映射到相同的物理页面,从而安排多个进程共享这部分代码的一个副本,而不是在每个进程中都包括单独的内核和C标准库的副本
- Linux虚拟内存系统
- Linux将虚拟内存组织成一些区域(也叫做段)的集合。代码段、数据段、堆、共享库段,以及用户栈都是不同的区域。每个存在的虚拟页面都保存在某个区域中,而不属于某个区域的虚拟页是不存在的,并且不能被进程引用。
- 进程中的task_struct包含或指向内核该进程所需要的所有信息。其中一个条目指向mm_struct,它描述了虚拟内存的当前状态。具体内容见linux进程管理之概念
- 缺页处理:
-段错误:访问一个不存在的页面
保护异常:例如,写一个只读的页面
正常缺页
- 内存映射
- Linux通过将一个虚拟内存区域与一个磁盘上的对象关联起来,以初始化这个虚拟内存区域的内容,这个过程称为内存映射
- 虚拟内存可以映射到两种类型的对象:
- linux文件系统中的普通文件
- 匿名文件
- 一个进程对共享区域进行操作,对于那些也把这个共享对象映射到它们虚拟内存的其他进程而言,也是可见的。并且这些变化也会反映在磁盘上的原始对象中。
- 对于一个映射到私有对象的区域做的改变,对其他进程来说不可见,并且所做的任何写操作也不会反映在磁盘的对象上。
- 写时拷贝
- fork()函数
- execve()加载并运行新程序的四个步骤:
- 删除已存在的用户区域
- 映射私有区域:写时复制
- 映射共享区域:共享库
- 设置程序计数器
- mmap()创建新的虚拟内存区域。munmap()删除虚拟内存区域
- 动态分配内存
- 显示分配:C:malloc和free;C++:new和delete
- 隐式分配:检测一个已分配块何时不再使用,进而释放这个块
- 显示分配的要求:
- 处理任意请求序列,不可以假设分配和释放的顺序
- 立即响应请求
- 只使用堆
- 对齐
- 不修改已分配的块
- 显示分配的目标:
- 最大化吞吐率:每个单位时间内完成的请求数,包含分配和释放请求
- 最大化内存利用率
- C语言中常见的与内存有关的错误
- 间接应用坏指针:scanf()函数中对变量不加&,则有可能将内容写到对应以变量值为地址的位置
- 读取未初始化的内存:在malloc内存后,不对其进行初始化,就对其进行读操作
- 允许栈缓冲区溢出:程序不检查输入串的大小就写入栈中的目标缓冲区(如定长数组),如果输入串比缓冲区大的话,可能发生溢出,fgets比gets能够限制串的大小
- 假设指针和它们指向的对象是相同大小的,例如分配一个int类型的数组,如果放入的数据类型大于int,则可能写入数据超出数组的内存大小
- 造成错位错误:申请内存为n的数组,但是初始化n+1的数组,则在结尾超出
- 引用指针,而不是它所指向的对象:*p–的意思是减少指针自己的值,(*p)–的意思是减少指针指向的整数的值
- 误解指针运算:指针的算术操作是以它们指向的对象的大小为单位来进行的,而这种大小单位并不一定是字节,int* p,p–其实减去的是四个字节的长度
- 引用不存在的变量:在函数中返回一个局部变量的地址,因为局部变量在函数结束后已经被释放,所以返回的只是地址,但是已经不是合法的
- 引用空闲堆块中的数据:引用一个已经被释放的内存的数据
- 引起内存泄漏:申请了内存,但是在不用时没有释放
第十章 系统级I/O
- 绝对路径和相对路径
- open、read和write函数,lseek
- RIO包
- fstat函数,检索到关于文件的信息
- opendir,readdir和closedir读取文件目录信息
- 共享文件
- 描述符表:每个进程都有它独立的描述符表,它的表项是由进程打开的文件描述符来索引的,每个打开的描述符表项指向文件中的一个表项
- 文件表:打开文件的集合是由一张文件表来表示的,所有的进程共享这张表。内含文件位置、引用计数(当前指向该表项的描述符表项数)等
- I/O重定向的一种工作方式是使用dup2(oldfd, newfd)
- 复制描述符表项oldfd到描述符表项newfd,覆盖描述符表表项newfd以前的内容,如果newfd已经打开了,dup2会在复制oldfd之前关闭newfd
- 标准I/O库将一个打开的文件模型化为一个流,可以认为一个流就是一个指向FLIE类型的指针:fopen,fclose,fread,fwrite,fgets,fputs,scanf,printf
第十一章 网络编程
- 客户端和服务器是进程,而不是常提到的机器或主机
- 从网络上接收到的数据从适配器经过I/O和内存总线复制到内存,通常是通过DMA传送
- 网络是一个按照地理远近组成的层次系统。最底层是LAN(local area network,局域网)
- 多个不兼容的居于杨可以通过路由器连接起来,形成互联网,也叫WAN(wide-area nerwork,广域网)
- 网络字节顺序为大端模式,主机序按主机所处的模式来规定,可以是大端也可以是小端
- htonl,htons,分别将32,16字节的网络字节序、无符号整数转换为主机序
- ntohl,ntohs,分别将32,16字节的主机序转换为网络字节序
- inet_pton函数将一个点分十进制串转换为一个二进制的网络字节顺序的IP地址
- inet_ntop函数将一个二进制的网络字节顺序的IP地址转换为它所对应的点分十进制
- 域名的等级
- 套接字结构体sockaddr_in后面的_in是互联网(Internet)的缩写
- socket、connect、bind、listen、accept函数
第十二章 并发编程
- 构造并发程序的方法:进程,I/O多路复用,线程
- 基于进程的并发编程
- 服务器作为父进程,每当一个客户端请求链接时,服务器就创建一个子进程来与之通信
- 优点:共享文件表,但是不共享用户地址空间。也就是说,一个进程不可能不小心覆盖另一个进程的虚拟内存
- 缺点:进程间通信相对困难,进程的控制和进程间通信开销很高
- 基于I/O多路复用的并发编程
- 服务器 select函数
- 事件驱动,当每个已连接描述符准备好可读时,服务器就为相应的状态机执行转移。
- 基于线程的并发编程
- 每个线程都有它自己的线程上下文,包括一个唯一的整数线程ID(TID)、栈、栈指针、程序计数器、通用目的寄存器和条件码
- 所有的运行在一个进程里的线程共享该进程的整个虚拟地址空间
- 每个进程开始生命周期都是单一,这个线程称为主线程。某一时刻主线程创建一个对等线程,两个线程就并发运行。
- 主线程执行一个慢速系统调用,例如read或者sleep,或者因为被系统的间隔计时器中断,控制就会通过上下文切换传递到对等线程。对等线程会执行一段时间,然后控制传递回主线程,依次类推。
- 线程的上下文比进程的上下文小得多,线程的上下文切换也比进程的上下文切换快得多
- 线程不像进程那样,严格按照父子层次来组织。主线程和其他线程的区别仅在于它总是进程中第一个运行的线程。
- 对等线程池的概念主要影响是,一个线程可以杀死它的任何对等线程,或者等待它的任意对等线程终止。
- pthread_create创建线程
- 线程的终止
- pthread_exit显式终止线程。如果主线程调用这个函数,它会等待其他所有对等线程终止,然后在终止主线程和整个进程
- 当顶层的线程例程返回时,线程会隐式的终止
- 线程调用exit函数,该函数终止进程以及所有与该进程相关的线程
- 另一个对等线程通过以当前线程ID作为参数调用pthread_cancel函数来终止当前线程
- pthread_join函数等待其他线程终止,调用后进入阻塞状态,直到线程tid终止,最终回收已终止进程占用的所有内存资源
- 可结合的线程能够被其他线程收回和杀死,在被收回之前,它的内存资源是不释放的。
- 分离的线程是不能被其他线程回收或杀死,它的内存资源在它终止时由系统自动释放。pthread_detach函数分离可结合线程tid。线程自己可以通过调用以pthread_self()为参数的pthread_detach函数来分离自己
- pthread_once初始化与线程相关的状态,例如初始化多个线程共享的全局变量
- 将变量映射到内存
- 全局变量:虚拟内存的读/写区域只包含每个全局变量的一个实例,任何线程都可以引用
- 本地自动变量:每个线程的栈都包含它自己的所有本地自动变量的实例,即使多个线程执行同一个线程代码也是如此
- 本地静态变量:虚拟内存的读/写区域只包含在程序中声明的每个本地静态变量的一个实例
- 临界区,互斥
- 信号量, sem_init,sem_wait,sem_post
- 线程不安全函数的分类:
- 不保护共享变量的函数,利用信号量解决
- 保持跨越多个调用的状态的函数:当前函数调用的结果依赖于前次调用的中间结果,即某个函数的入参依赖于另一个函数
该入参的处理,修改程序代码解决
返回指向静态变量的指针的函数,利用互斥锁解决
调用线程不安全函数的函数
- 可重入性:当他们被多个线程调用时,不会引用任何共享数据
- 竞争与死锁