深入理解计算机系统
1. 概论
1.1 程序的格式
- 预处理阶段:预处理器(
cpp
)将以#
开头的语句引入替换成相关文件内容或数据内容,变为文本文件 - 编译阶段: 编译器(
ccl
)将文本转化为汇编语言,也是文本文件 - 汇编阶段: 汇编器(
as
)将汇编语言转化机器指令,打包成可重定位目标程序(Relocatable Object Program
),变为二进制文件 - 链接阶段: 连接器(
ld
)将动态库或者静态库与上述形成的可重定位文件进行合并,得到可执行目标文件.
1.2 硬件系统
-
总线: 携带信息字节在各个组件间传递
-
I/O设备: 系统与外部世界的联系通道.每个I/O设备通过控制器(是I/O设备本身或者系统的主板上的芯片组)或适配器(插在主板插槽上的卡)与I/O总线相连,通过 PCI 总线
-
主存: 主存是一个临时存储设备,在处理器执行程序时,用来存放程序和程序处理的数据.
-
处理器: 即
CPU
,是解释(执行)主存中的指令的引擎,其核心是大小一个字的寄存器,称为程序计数器(PC)
.任何时刻,PC
都指向主存中的某条机器指令.CPU
的模型由指令架构集
决定.CPU
从PC
指向的内存中读取指令,解释其中的位,执行该指令指示的简单操作,随后更新PC
.这些简单操作主要围绕主存
,寄存器文件(Register File)
和算数/逻辑单元(Algorithm and Logic Unit)
.ALU
计算新的数据和地址值.- 加载: 从主存的某一地址处复制不大于一个字的数据到寄存器
- 存储: 从寄存器复制不大于一个字的数据到主存中的某一地址
- 操作: 把两个寄存器中的值复制到
ALU
,由ALU
对其进行计算,并将计算结果返回至一个寄存器中 - 跳转: 从指令中复制一个字到
PC
中,
处理器的组成分为:
- 处理器的指令集架构: 其描述每条机器指令的含义,各种汇编代码都有其一定的格式,
CPU
可以根据对应格式将二进制代码转化成汇编代码.即二者一一对应 - 处理器的微体系结构: 其描述处理器的具体实现
1.3 存储设备层次结构
1.4 操作系统管理硬件
所有程序对硬件的操作必须通过操作系统实现
操作系统的两个基本功能:
- 防止硬件被失控的进程滥用
- 提供简单一致的机制来控制复杂而又大相径庭的低级硬件设备
文件对I/O设备的抽象表示,虚拟内存是对主存和I/O设备的抽象表示,进程是对处理器,主存和I/O设备的抽象表示
1.4.1 进程
进程是操作系统对一个正在运行的程序的一种抽象,即,在一台系统上可以同时运行多个程序,每个程序都好像在独占硬件.
而并发运行
,则是说一个进程和另外一个进程交错执行.因此需要运行的进程数可以多于运行他们的CPU
数.这种交错执行的机制被称为上下文切换
(上下文即为进程运行所需的所有状态信息,如PC
,寄存器值以及主存的内容).
任何一个时刻,单核处理器只能运行一个进程的代码.当操作系统决定将控制权从一个进程切换到另个进程的时候,就会进行上下文切换
,即保存当前进程的上下文,恢复另一个进程的上下文,并将控制传递给另一个进程.新进程就会从上次停止的地方开始运行.
上下文切换
由操作系统内核(Kernel
)管理.
内核
是操作系统代码常驻内存的部分.
当某个进程需要操作系统的某些操作(例如读写操作)时,它就执行系统调用
(system call)指令,将控制权传递给内核.内核执行后得到结果再传递给进程.
内核
不是一个独立的进程,它是系统管理全部进程所用代码和数据结构的集合.
1.4.2 线程
现代系统中,一个进程可以由多个称为线程
的执行单元组成.每个线程都运行在进程的上下文中,并共享同样的代码和全局数据.
1.4.3 虚拟内存
虚拟内存
也是一种抽象,是对内存(后文也称为物理存储器)空间映射,使得每个进程都认为自己运行在独立的内存空间,且每个内存一致.
该图中的
内核虚拟内存
是为内核保留的,不运行进程读写该区域的内容或者直接调用内核代码定义的函数.进程只能调用内核来执行这些操作.
1.4.4 文件
文件
即字节序列,在Unix
系统中,任何设备都被看成文件,甚至包括网络.
1.5 总结
计算机系统是硬件与系统软件的集合
1.5.1 Amdahl定律
主要思想: 当我们对系统的某个部分加速时,其对系统整体的性能影响取决于该部分的重要性和加速程度.要想显著加速整个系统,必须提升全系统中相当大的部分的速度
设系统执行某应用程序所需的时间为
1.5.2 并发(concurrency)与并行(parallelism)
1.5.2.1 线程级并发
并发可由进程间的快速切换进行模拟
多核处理器将多个CPU
(称为核
)集成到一个集成电路芯片上
超线程
(hyperthreading
),有时也被称为同时多线程
(simultaneous multi-threading),是一项允许一个CPU
执行多个控制流的技术.即CPU
的某些硬件有多个备份,如PC
和寄存器文件,而其他硬件部分只有一份,如执行浮点算数运算的单元.常规处理器大约需要
1.5.2.2 指令级并发
在较低层次上,现代处理器可以同时执行多条指令的属性称为指令级并行
.流水线
(pipelining
)是它的一种实现机制.
流水线将执行一条执行所需要的活动划分成不同的步骤,将系统的硬件组织成一系列的阶段,每个阶段执行一个步骤,将处理器的硬件组织成一系列的阶段,每个阶段执行一个步骤.这些阶段可以并行地操作,用来处理不同指令的不同部分.
如果处理器可以达到比一个周期一个条指令更快的执行速率,就称之为超标量
(superscaler
)
1.5.2.3 单指令,多数据并行
在最低层次上,许多现代处理器拥有特殊的硬件,允许一条指令产生多个可以并行执行的操作,这种方式称为单指令、多数据,即SIMD并行
.如较新几代的
1.5.3 计算机系统中抽象的重要性
一、程序结构与执行
2. 信息的表示与处理
2.0 概论
计算机世界中的信息由以比特为单位的二进制数字组成,人们对其进行编码解释(interpretation)
,赋予不同的可能模式位不同的涵义
2.1 信息存储
机器级程序将内存看成一个巨大的字节数组,称为虚拟内存
(virtual memory),同时将动态随机访问存储器(DRAM),闪存,磁盘存储器,特殊硬件和操作系统软件相结合.内存中每一个字节都由唯一的数字标识,即为地址
.所有可能的地址集合即为虚拟地址空间
(virtual address space).
程序对象
(program object):即程序数据,指令和控制信息.
2.1.1 字数据大小
每台计算机都有一个字长
64位机器会向32位兼容,所以可以运行32位程序.我们将程序称为32位程序或64位程序时,区别在于该程序是如何编译的,而不是其运行的机器类型.
在编写编译程序之前,可以利用关键字sizeof()
获取相应类型数据的长度
2.1.2 寻址与字节排序
为多字节对象建立的规则:
- 对象的地址是什么:地址为对象在内存中最低位置
- 对象如何排列:与操作系统有关
小端模式
(little endian): 在地址中按照最低有效位到最高有效位的顺序存储大端模式
(big endian): 在地址中按照最高有效位到最低有效位的顺序存储
注意网络传输时因此而出现的问题
C语言中可以使用强转换
(cast)和联合
(union)进行来允许以一种数据类型引用一个对象,而这种数据类型与创建这个对象时定义的数据类型不同.其实只是告诉编译器换一种方式去理解这种类型,并没有改变对象的任何数值.因为底层数据是不分类型的.
2.1.3 表示字符串
ASCII
码由一个字节编码,所以不必考虑大小端问题,具有更强的平台移植性.
由于世界各地字符太多,一个字节无法全部编码,所以引入Unicode
标准
2.1.4 表示代码
不同处理器的指令集架构不同,所以编码而成的二进制字节也就不同.故而不同机器上的二进制代码难以兼容
2.1.5 布尔代数
围绕数值0和1的数学知识体系被称为布尔代数
上述运算可以被推广到位向量上
布尔运算的性质:
对 有分配律,即: 对 有分配律,即:- 布尔运算中的
加法逆元
(additive inverse)是其值本身,即: ^ .且其满足交换律,即: ^ ^ .
位向量
可以编码任何子集 .其中 ,当且仅当 .则使用这种编码集合的方法,布尔运算 , 和 ,分别对应于集合中的
2.1.6 C语言的位级运算
位级运算的一个常见用法就是实现掩码运算
2.1.7 C语言的逻辑运算
其逻辑运算符为
只有运算的参数为0或1时,位运算才和其对应的逻辑运算有相同的行为
对二元逻辑运算符来说,如果第一个参数求值就可以确定表达式的结果,他们就不会对第二个参数继续求值
2.1.8 C语言的移位运算
C语言的移位运算有三种:
- 左移运算:
,即 向左移动 位,丢弃最高的 位,并在右端补 个 - 逻辑右移运算:
,即在左端填充 个0.用于无符号数 - 算术右移运算:
,即在左端填充 个最高有效位的值.用于有符号数
C语言的移位运算是从左至右结合的,C语言的其他运算符的优先级可以参考这篇文章
C语言标准中,如果,则位移量为
2.2 整数表示
本节的一些术语如下图
2.2.1 整数数据类型
2.2.2 无符号数的编码
假设一个整数数据类型有
其中
函数
显然,无符号数编码具有唯一性,且函数
2.2.3 补码(two's-complement)编码
最常见的有符号数的计算机表示方式为补码形式.其将字的最高位解释为负权(negative weight).我们用
其中,最高有效位
显然,补码编码具有唯一性,且函数
由上可得补码和无符号编码的一些性质
上述的补码不对称的原因是负数占一半,非负数占一半,而
C语言标准并没有要求使用补码的形式,但是几乎所有机器使用补码形式.
C库文件<limits.h>
定义了一组常量,限定不同数据的取值范围,定义了INT_MAX,INT_MIN,UNIT_MAX等值.<stdin.h>中定义了intN_t和uintN_t,为了无歧义地声明位数据类型,其中
在网络中,最重要的是保持数据类型与协议的数据类型的兼容
有符号数还有两种表示方法:
- 反码(Ones' Complement): $$B2O_w\doteq-x_{w-1}(2{w-1}-1)+\sum\limits_{i=0}{\vec{x}_i2^i}$$
- 原码(Sign-Magnitude): $$B2S_w\doteq(-1){x_{w-1}}\bullet\sum\limits_{i=0}{\vec{x}_i2^i}$$
这两种编码方式对
注意术语Two's Complement表示的是:对于非负数
,用 (一个 )计算 的w位表示.而Ones' Complement用 (多个 )计算 的w位表示.
2.2.4 有符号数与无符号数的转换
C语言通过上述的类型强制转换,将有符号数转化成无符号数.注意,强制类型转换不改变数据位数值,只改变解释解释这些位模式的方法.于是我们可以获得其相互转化的函数:
- 补码转无符号
:
对满足 的 有: - 无符号转补码
对满足 的 有:
2.2.5 C 语言中的有符号数与无符号数
C语言中,常量都是采用补码表示的,可在数值后面加
当执行一个运算时,如果它的一个运算数是有符号的而另一个是无符号的,那么C语言会隐式地将有符号参数强制类型转换为无符号数
在<limits.h>中INT_MIN被定义成(-INT_MAX-1)
2.2.6 扩展一个数字的位表示
即从一个较小的数据类型转换到一个较大的类型,且保持数值不被改变.
- 无符号数的零扩展:
定义宽度为 的位向量 和宽度为 的位向量 ,其中 ,则 - 补码数的符号扩展:
如果是无符号数,则在这个过程中可以执行一个符号扩展
(sign extension),在位向量中填充最高有效位的值.
定义宽度为 的位向量 和宽度为 的位向量 ,其中 ,则
其证明为:
2.2.7 截断数字
即减少数值的位数
- 截断无符号数:
定义宽度为 的位向量 和宽度为 的位向量 .令 ,则 . - 截断补码数:
定义宽度为 的位向量 和宽度为 的位向量 .令 ,则 .
需要注意的是,在不同类型间的转换中,特别是当两种不同类型出现在同一个运算式中时,容易出现错误.
书中给出的建议是:绝不使用无符号数
2.3 整数运算
2.3.1 无符号加法
本文为参数
检测无符号加法中的溢出:
对于满足
模数加法形成了一种数学结构,称为阿贝尔群
(Abelian group).它是可交换的和可结合的.它有一个单位元
则可得无符号求反的定义:对满足
无符号数的逆元时通过加法溢出实现的
2.3.2 补码加法
定义
补码加法定义为:对于满足
原理:不管哪种类型数据相加,都是对其位向量进行操作,再将其转化为对应十进制数值.由于相加时存在溢出,所以需要截断.
其证明如下:
补码加法溢出的检测:
满足
- 当且仅当
,但 时,发生了正溢出 - 当且仅当
,但 时,发生了负溢出
对于补码来说,若
,则不管是否发生溢出,都有
2.2.3 补码的非
补码求反的定义:对满足
计算位级表示补码非的值的方式:
- 对位级取反再加
,即:
- 以最右边的
为界,在其左方取反
2.3.4 无符号乘法
因为
2.3.5 补码乘法
范围
其原理为:无符号和补码乘法的位级等价
给定长度
由无符号与反码的转换公式可知:
再联合上式可得:
2.3.6 乘以常数
整数乘法指令相当慢,一般需要十个时钟周期,而加法、减法、位级运算和移位只要一个时钟周期.因此,编译器会试着用加法和移位来替代常数因子的乘法(后两者需要截断处理).
- 乘以
的幂
即数的位模式左移 位,右方用 填充:
- 与
的幂相乘的无符号乘法
C变量x和k有无符号数值 和 ,且 ,则C表达式x<<k
产生数值 - 与
的幂相乘的补码乘法
C变量x和k有补码数值 和无符号数值 ,且 ,则C表达式x<<k
产生数值
对于常数K的表达式x*K
生成代码,首先将K转化成一组
(x<<n)+(x<<(n-1))+⋯+(x<<m)
(x<<(n+1))-(x<<m)
2.3.7 除以2的幂
整数除法要比整数乘法更慢,需要
整数除法会涉及小数,所以我们引入一些运算使其规整到零.
- 定义
为唯一的整数 使得 - 定义
为唯一的整数 使得
则对于下舍正,上舍负
.则有:
- 除以
的幂的无符号除法
C变量x和k有无符号数值 和 ,且 ,则C表达式x>>k
产生数值 .此为逻辑右移
其原理是 的低 位的无符号数值小于 - 除以 2 的幂的补码除法,向下舍入
C变量x和k有补码数值 和无符号数值 ,且 ,则C表达式x>>k
产生数值 .此为算术右移
当 时,向下舍入得到的数值,不等于相应位模式的补码表示.所以需要加入偏置
(biasing),使其向上舍入.如图,其中 ,则其整数表示应该为 ,即舍去小数部分,取整数部分.而其位模式经移位后得到的补码数值为 .
- 除以 2 的幂的补码除法,向上舍入
C变量x和k有补码数值 和无符号数值 ,且 ,则C表达式(x+(1<<k)-1)>>k
产生数值 .此为算术右移
此表达式中加入了偏置
其原理为:
2.4 浮点数
2.4.1 二进制小数
对于形如
显然,小数的有限位编码的二进制表示法只能表示能分解为
2.4.2 IEEE浮点数表示
IEEE浮点标准用
符号
(sign): 决定这个数是负数( )还是正数( ).数值 的符号位解释做特殊情况处理.其被一个单独的符号位 编码尾数
(significand): 是一个二进制小数,其范围是 ,或 . 位的小数字段 编码尾数 ,但是编码的值取决于阶码字段的值是否是 .阶码
(exponent): 的作用是对浮点数加权,权重是 的 次幂(可能是负数). 位的阶码字段 对 进行编码
被编码的值主要有 种情况
- 规格化的值:
即 的位模式即不全为 又不全为 .这种情况下,阶码字段被解释为以偏置
(biased)形式表示的有符号整数.所以阶码的值为 ,其中 即为 的无符号数表示, .
小数字段 被解释为小数位 ,其中 ,其二进制表示为 .尾数定义为 ,这种方式也叫隐含的以1开头的
(implied leading 1)表示.我们可以调整阶码 使得尾数 始终在范围 中 - 非规格化的值:
阶码域全为 时,所表示的数时非规格化
形式.在此种情况下,阶码值为 (注意,他是一个定值,用于使非规格化值平滑地转换到规格化值),而尾数 ,也就是小数字段的值,不包括隐含的开头的
其用于表示 : 的浮点表示的位模式为全 ,当符号位为 时,其他字段全 ,得到 .
也可以表示非常接近于 的数.他们提供了一种属性,称为逐渐溢出
(gradual underflow),其中,可能的数值分布均匀地接近 - 特殊值:
阶码全为 的情况:- 当小数域全为
时,表示无穷. 是 , 是 ,其用来表示两个大数相乘得到的溢出
的结果. - 当小数域非
时,结果值表示为 ,即Not a Number
.一些运算的结果不能是实数或者无穷,就会返回这样的 值.也可用于表示非初始化的值.
- 当小数域全为
2.4.3 数字示例
从上图可以看出,最大非规格数
可得到整数数值以浮点数的的存储形式.如下例,将整数
2.4.4 舍入
由于表示方法限制了浮点数的范围与精度.因此,对于值最接近的
可以用浮点形式表示的匹配值舍入
(rounding)的任务.
IEEE规定了四种舍入方式
,默认的方法是找到对接近它的值,其他的三种是寻找它的上界与下界.
向偶数舍入
(round-to-even)或向最近的数舍入
(round-to-nearest): 它将数字向上或向下舍入,使得结果的最低有效位数字为偶数.所以这种方法将 和 都舍入成 .其作用为避免统计偏差,因为它既可向上舍入,也可向下舍入.- 其他三种产生实际值的确界(guaranteed bound).
向偶数舍入也可用于二进制小数中.将最低位 看作偶数, 看成奇数.例如考虑舍入值到小数点右边 位,即四分之一: 向下舍入到 . 向上舍入到 .
注意到此例中的最低有效位是 ,这是因为这些值不是可能值的中间值,所以舍入到最近的.如果这些值是可能值得中间值,则选择最低有效位为 的值,如: ,向上舍入到 ,向下舍入到 .
2.4.5 浮点运算
IEEE标准制定一个了规则:把浮点数
虽然实数的加法也形成了阿贝尔群,但是舍入导致了浮点数的运算不可交换(此特性可能会造成一些意想不到的漏洞).如:
作为阿贝尔群,大多数值在浮点加法下都有逆元,即
浮点数的加法满足了单调性属性,即:若
无符号和补码加法无单调性属性
浮点乘法被定义为
2.4.6 C语言中的浮点数
因为C语言标准不要求机器使用IEEE浮点标准,所以没有标准的方法改变舍入方式或者得到诸如
#define _GNU_SOURCE 1
#include <math.h>
在int,float,double格式进行强制类型转换的时候,程序改变数值和位模式的原则如下(假设int为
- int→float,数字不会溢出,但出现舍入
- int\float→double,能保留精确的数值
- double→float,可能溢出成
,也可能出现舍入 - float\double→int,值会向零舍入.也可能出现溢出,C语言对此没有指定固定的结果.与Intel兼容的微处理器指定位模式
(字长 的 )为整数不确定
(integer indefinite)值.一个浮点数到整数的转换,如果不能找一个合理的近似值,就会产生这样一个值.如 会得到 (即 ),即从一个正值变成一个负值.
3. 程序的机器级表示
3.0 概论
计算机执行机器代码
,用字节序列编码低级的操作,包括处理数据,管理内存,读写存储设备上的数据,以及利用网络通信.编译器基于编程语言的规则,目标机器的指令集和操作系统遵循的惯例,经过一系列的阶段生成机器代码.
GCC C语言汇编器以汇编代码
的形式产生输出,汇编代码是机器指令的文本表示,给出程序的每一条指令.然后GCC调用汇编器
和链接器
,根据汇编代码生成可执行的机器代码.其中,汇编代码由CPU
的指令集架构决定.
本章对源代码和对应汇编代码进行研究,这是一种逆向工程
(reverse engineering):通过研究系统和逆向工作,来试图了解系统的创建过程
IA32是x86-64的32位前身,Intel在1985年提出
3.1 历史观点
3.2 程序编码
3.3 数据格式
3.4 访问信息
3.5 算数和逻辑操作
3.6 控制
3.7 过程
3.8 数组分配和访问
3.9 异数的数据结构
3.10 在机器级程序中将控制和数据结合起来
3.11 浮点代码
4. 处理器体系结构
5. 优化程序性能:永远的好兄弟,局部性!!!
6. 存储器层次结构:永远的好兄弟,局部性!!!
问题:
- 存储器的发展,种类和结构,由此解决的问题和新的问题
- SRAM:最快,强抗干扰
- DRAM:较快,访问需两次,参数为
,主要瓶颈 硬件本身架构 以及 缓存不命中的存在- FPM DRAM:缓存不立即擦除
- EDO DRAM:减少 RAS/CAS 间隔
- SDRAM:同步,根据时钟上升沿
- DDR DRAM:两个时钟沿, 参考计算机是如何工作的?探索主内存,以DDR5为例
- VRAM:处理图片,通过对缓存的偏移
- ROM
- FLASH
- SSD:Page in Block in Flash in SSD
- 磁盘
- 主要瓶颈:寻道
- 结构:CHS 和 LBA,实际 LBA 会被驱动器转换为 CHS
- 存储器与 CPU 的通信
- CPU 通过 I/O 桥转发到 存储器,除了 Ln Cache
- 存储器操作完毕后,直接发送到 内存,通过 DMA 传输
- 发送信号告诉 CPU 操作完成
- 存储器与存储器之间的关系与通信
- k + 1 层向 k 层提供一块原始数据,块的大小始终不变,由二者决定,几个块组成一个区域
- 读
- 若 k 有,则直接读,是为缓存命中
- 若 k 没有,则向下读,k + 1 层向 k 层提供对应数据,是为缓存不命中
- k 层什么都没有,是为冷不命中,需要使用放置策略,向下读,但是由于上层比下层小,会将块的编号进行 模 处理,之后防止在对应快上(这种策略也提供了映射机制),会发生冲突,此为冲突不命中,类似于 hash 碰撞,此时需要调整策略
- 高级存储器和低级存储器之间的文件映射实现
- 猜测是由多级表项实现,譬如,内存中有块区域叫做 Swap Buffer,用来存储磁盘的满足局部性的数据,可以在此处放置映射表项,类似于虚拟地址和物理地址的转换,结合目录项,获得磁盘地址,表项对应地映射到物理地址
- 譬如:32 位地址的
- 因为 永远的好兄弟,局部性 的存在能优化访问速度,所以会被存储器大量运用,但若有朝一日它抛弃了我们怎么办?即缓存的命中问题
- 高速缓存器 Cache(读 cashing),参考理解计算机Cache(一):从块到缓存结构,以及逐步推出映射策略
- 主要参数
, 组,每组 行,一行有 个字节加上 的标志位,一个有效位, 表示的是寻址空间总大小。 - 注意:寻址可能是按字寻址,而非按字节寻址,字的大小一般与 CPU 相关,64位,应该是一字等于八字节
:直接映射 :组相联映射 :全相联映射
- 主要参数
二、系统中运行程序
7. 链接
8. 异常控制流
9. 虚拟存储器
问题:
- 什么是虚拟存储器,它为何出现
- 它与物理存储器的关系与转换
- 它的异常策略:缺页、盈满、碎片管理
三、程序间的交互与通信
10. 系统级I/O
I/O
是在主存和外部设备拷贝数据的过程
10.1 Unix I/O
Unix文件
是一个m个字节的序列.所有的I/O
设备,如网络,磁盘和终端,都被模型化成为一个个文件,所有的输入和输出即为读和写:
- 程序打开文件时,内核会创建一个非负整数,叫
描述符
,用于唯一标识所打开的文件.每个进程开始时都会有三个打开的文件,为:标准输入(描述符0),标准输出(描述符1),标准错误(描述符2).在<unistd.h>中定义了成了常量STDIN_FILENO
,STDOUT_FILENO
和STDERR_FILENO
. - 内核中保持着一个文件
当前位置k
,其初始为0,意义为字节偏移量 - 读文件或者说输入,即使从文件拷贝相应个数的的字节到内存,一直到遇到
EOF
时结束 - 关闭文件时,内核会根据相应的
描述符
释放打开文件时创建的数据结构,并将该描述符
恢复到可用描述符池
中
10.2 打开和关闭文件
open
函数来打开或者创建文件
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int open(char *filename, int flags, mode_t mode)
其返回值为文件描述符,且为当前进程中没有打开的最小的描述符
filename
为文件名,flags
为访问形式,mode
为新文件的访问权限位.通过每个进程都有的unmask
函数进行设置
11. 网络编程
socket相当于定义了一个文件的类型,告诉系统这是个网络类型的文件,其中的类型信息包含了协议类型.随后的 connect 或者 bind 用于产生连接,即相当于手动或者系统自动的分配"存储地址".
该文件的地址相当于被扯成了两半, 一半在服务端手中, 一半在客户端手中, 两者结合才能访问文件内容
/*************************************
文件名: server.c
linux 下socket网络编程简例 - 服务端程序
服务器端口设为 0x8888 (端口和地址可根据实际情况更改,或者使用参数传入)
服务器地址设为 192.168.1.104
*/
#include <stdlib.h>
#include <sys/types.h>
#include <stdio.h>
#include <sys/socket.h>
#include <linux/in.h>
#include <string.h>
int main()
{
int sfp,nfp; /* 定义两个描述符 */
struct sockaddr_in s_add,c_add;
int sin_size;
unsigned short portnum=0x8888; /* 服务端使用端口 */
printf("Hello,welcome to my server !\r\n");
sfp = socket(AF_INET, SOCK_STREAM, 0);
if(-1 == sfp)
{
printf("socket fail ! \r\n");
return -1;
}
printf("socket ok !\r\n");
/* 填充服务器端口地址信息,以便下面使用此地址和端口监听 */
bzero(&s_add,sizeof(struct sockaddr_in));
s_add.sin_family=AF_INET;
s_add.sin_addr.s_addr=htonl(INADDR_ANY); /* 这里地址使用全0,即所有 */
s_add.sin_port=htons(portnum);
/* 使用bind进行绑定端口 */
if(-1 == bind(sfp,(struct sockaddr *)(&s_add), sizeof(struct sockaddr)))
{
printf("bind fail !\r\n");
return -1;
}
printf("bind ok !\r\n");
/* 开始监听相应的端口 */
if(-1 == listen(sfp,5))
{
printf("listen fail !\r\n");
return -1;
}
printf("listen ok\r\n");
while(1)
{
sin_size = sizeof(struct sockaddr_in);
/* accept服务端使用函数,调用时即进入阻塞状态,等待用户进行连接,在没有客户端进行连接时,程序停止在此处,
不会看到后面的打印,当有客户端进行连接时,程序马上执行一次,然后再次循环到此处继续等待。
此处accept的第二个参数用于获取客户端的端口和地址信息。
*/
nfp = accept(sfp, (struct sockaddr *)(&c_add), &sin_size);
if(-1 == nfp)
{
printf("accept fail !\r\n");
return -1;
}
printf("accept ok!\r\nServer start get connect from %#x : %#x\r\n",ntohl(c_add.sin_addr.s_addr),ntohs(c_add.sin_port));
/* 这里使用write向客户端发送信息,也可以尝试使用其他函数实现 */
if(-1 == write(nfp,"hello,welcome to my server \r\n",32))
{
printf("write fail!\r\n");
return -1;
}
printf("write ok!\r\n");
close(nfp);
}
close(sfp);
return 0;
}
/*************************************
文件名: client.c
linux 下socket网络编程简例 - 客户端程序
服务器端口设为 0x8888 (端口和地址可根据实际情况更改,或者使用参数传入)
服务器地址设为 192.168.1.104
*/
#include <stdlib.h>
#include <sys/types.h>
#include <stdio.h>
#include <sys/socket.h>
#include <linux/in.h>
#include <string.h>
int main()
{
int cfd; /* 文件描述符 */
int recbytes;
int sin_size;
char buffer[1024]={0}; /* 接受缓冲区 */
struct sockaddr_in s_add,c_add; /* 存储服务端和本端的ip、端口等信息结构体 */
unsigned short portnum=9875; /* 服务端使用的通信端口,可以更改,需和服务端相同 */
printf("Hello,welcome to client !\r\n");
/* 建立socket 使用因特网,TCP流传输 */
cfd = socket(AF_INET, SOCK_STREAM, 0);
if(-1 == cfd)
{
printf("socket fail ! \r\n");
return -1;
}
printf("socket ok !\r\n");
/* 构造服务器端的ip和端口信息,具体结构体可以查资料 */
bzero(&s_add,sizeof(struct sockaddr_in));
s_add.sin_family=AF_INET;
s_add.sin_addr.s_addr= inet_addr("43.248.128.155"); /* ip转换为4字节整形,使用时需要根据服务端ip进行更改 */
s_add.sin_port=htons(portnum); /* 这里htons是将short型数据字节序由主机型转换为网络型,其实就是将2字节数据的前后两个字节倒换,和对应的ntohs效果、实质相同,只不过名字不同。htonl和ntohl是操作的4字节整形。将0x12345678变为0x78563412,名字不同,内容两两相同,一般情况下网络为大端,PPC的cpu为大端,x86的cpu为小端,arm的可以配置大小端,需要保证接收时字节序正确。
*/
printf("s_addr = %#x ,port : %#x\r\n",s_add.sin_addr.s_addr,s_add.sin_port);
/* 这里打印出的是小端和我们平时看到的是相反的。 */
/* 客户端连接服务器,参数依次为socket文件描述符,地址信息,地址结构大小 */
if(-1 == connect(cfd,(struct sockaddr *)(&s_add), sizeof(struct sockaddr)))
{
printf("connect fail !\r\n");
return -1;
}
printf("connect ok !\r\n");
/*连接成功,从服务端接收字符*/
if(-1 == (recbytes = read(cfd,buffer,1024)))
{
printf("read data fail !\r\n");
return -1;
}
printf("read ok\r\nREC:\r\n");
buffer[recbytes]='\0';
printf("%s\r\n",buffer);
getchar(); /* 此句为使程序暂停在此处,可以使用netstat查看当前的连接 */
close(cfd); /* 关闭连接,本次通信完成 */
return 0;
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 全程不用写代码,我用AI程序员写了一个飞机大战
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 记一次.NET内存居高不下排查解决与启示
· DeepSeek 开源周回顾「GitHub 热点速览」
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了