第一章 计算机系统漫游
预处理->编译->汇编->链接
// hello.cpp
#include <cstdio>
#define PI 3.14
int main()
{
// 1111
//
printf("%f\n", PI);
printf("hello, world!\n");
return 0;
}
预处理
宏替换和注释消失 但是空行还在g++ -E hello.cpp -o hello.i
tail -9 hello.i
# 5 "hello.cpp"
int main()
{
printf("%f\n", 3.14);
printf("hello, world!\n");
return 0;
}
编译 生成汇编g++ -S hello.i
.file "hello.cpp"
.text
.section .rodata
.LC1:
.string "%f\n"
.LC2:
.string "hello, world!"
.text
.globl main
.type main, @function
main:
.LFB0:
.cfi_startproc
pushq %rbp
.cfi_def_cfa_offset 16
.cfi_offset 6, -16
movq %rsp, %rbp
.cfi_def_cfa_register 6
subq $16, %rsp
movq .LC0(%rip), %rax
movq %rax, -8(%rbp)
movsd -8(%rbp), %xmm0
leaq .LC1(%rip), %rdi
movl $1, %eax
call printf@PLT
leaq .LC2(%rip), %rdi
call puts@PLT
movl $0, %eax
leave
.cfi_def_cfa 7, 8
ret
.cfi_endproc
.LFE0:
.size main, .-main
.section .rodata
.align 8
.LC0:
.long 1374389535
.long 1074339512
.ident "GCC: (Ubuntu 7.5.0-3ubuntu1~18.04) 7.5.0"
.section .note.GNU-stack,"",@progbits
汇编 生成 可重定位二进制目标程序
gcc编译器 -c参数 起到的作用是编译和汇编 -S参数 仅仅是编译
g++ -c hello.s
链接 生成执行文件
g++ hello.o -o hello
在不同进程间切换 交错执行的执行-上下文切换
操作系统保持进程运行所需的所有状态信息 这种状态-上下文
线程共享代码和全局数据
第一部分 程序结构和执行
第二章 信息存储
孤立地讲,单个位不是非常有用. 然而, 当把位组合在一起, 再加上某种解释,即赋予不同可能位组合不同的含义, 我们就能表示任何有限集合的元素.
第五章 优化程序性能
编写高效的程序
- 选择一组适当的算法和数据结构
- 编写出编译器能够有效优化以转变为高效的可执行程序的源代码
- 大项目的并行计算
整数运算使用乘法的结合律和交换律, 当溢出的时候, 结果依然是一致的.
但是浮点数使用结合律和交换律的时候, 在溢出时, 结果却不是一致的.
整数的表示虽然只能编码一个相对较小的数值范围, 但是这种标识是精确地
浮点数能编码一个较大的数值范围, 但是这种表示只是近似的.
第二部分 在系统上运行程序
第三部分 程序间的交流和通信
第十章 系统级I/O
现在有一个读操作,当前文件的位置是K,然后读取了n个字节。如果k+n >= 文件大小m 则会触发end-of-file(EOF)的条件,应用程序会检测到这个条件。在文件的结束尾部没有明确的EOF符号
目录是包含一组链接(link)的文件,每个链接都将一个文件名映射到一个文件,这个文件可能是另一个目录。
每个目录至少含有两个条目,.
是到该目录自身的链接,..
是到目录层次结构中父目录的链接。
可以通过open函数打开一个已经存在的文件,或者创建一个新的文件
第十一章 网络编程
从网络上接收到的数据经过网络适配器 IO总线 内存总线复制到内存, 通常使用的DMA传送. 反过来就是内存到网络的方式
我们的实例抓住了互联网络思想的精髓, 封装是关键
应改为IP地址, 确切地说是Ipv4地址定义一个特殊的类型, 不过标准成立后已经太迟了. 不过看着ipv6的地址表示, 获取标准成功了? 提供了一个看起来特殊的类型
typedef uint32_t in_addr_t;
struct in_addr
{
in_addr_t s_addr;
};
struct in6_addr
{
union
{
uint8_t __u6_addr8[16];
uint16_t __u6_addr16[8];
uint32_t __u6_addr32[4];
} __in6_u;
};
socket函数返回的套接字被操作系统默认为是主动实体(客户端), 通过listen告知操作系统这是被动实体(服务器)
getaddrinfo函数
#include <netdb.h>
struct addrinfo
{
int ai_flags; // hints
int ai_family; // hints AF_INET 限制返回ipv4 AF_INET6限制返回ipv6 不指定则二者都有
int ai_socktype; // hints
int ai_protocol; // hints
socklen_t ai_addrlen;
struct sockaddr *ai_addr;
char *ai_canonname; // 只有设置了AI_CANONNAME 这里将保存host的官方名字
struct addrinfo *ai_next; // 链表
};
// node: 可以是域名 点分十进制ip nullptr
// AI_PASSIVE 告知返回的套接字地址可能用于listen套接字 此时应该设置为nullptr
// service: 端口号, 服务名http
// AI_NUMERICSERV 强制此参数为端口号
// addrinfo 设置后可以控制返回的result
// AI_ADDRINFO 只有当本机设置了ipv4才查询ipv4地址 ipv6亦然
int getaddrinfo(const char *node, const char *service,
const struct addrinfo *hints,
struct addrinfo **result);
void freeaddrinfo(struct addrinfo *res);
const char *gai_strerror(int errcode);
调用getaddrinfo
生成result链表后, 遍历链表直到socket和bind调用成功(socket和connect调用成功)
getnameinfo函数
// host 存储主机名 IP地址
// serv 存储端口号 服务名
// NI_NUMERICHOST 直接返回IP地址 不解析, NI_NUMERICSERV 字节返回端口号 不解析服务名
int getnameinfo(const struct sockaddr *addr, socklen_t addrlen,
char *host, socklen_t hostlen,
char *serv, socklen_t servlen, int flags);
EOF并不存在对应的字符, 其是由内核检测到的一种条件
第十二章 并发编程
现代操作系统提供了三种基本的构造并发程序的方法
- 进程
- 线程
- I/O多路复用
每个线程有自己的线程上下文, 线程id 栈 栈指针 程序计数器(存放下一条指令) 通用目的寄存器和条件码(CPU根据运算结果由硬件设置, 标识正负或者溢出等)
在任何一个时间点上线程是可结合的(joinable)或者是可分离的(detached). 必须二选一来收回线程资源
前者可以能够被其他线程收回和杀死, 后者则不可被其他线程回收或杀死, 它占用的资源在其终止时由系统自动释放
pthread_detach 分离线程, 可以通过pthread_self来分离自己
pthread_join 结合线程 只能等待一个指定的线程终止, 没有办法让其等待任意一个线程终止
为了理解程序中一个变量是否是共享的有一些基本问题要解答
- 线程的基础内存模型是什么
- 根据这个模型,
变量实例
如何映射到内存的 - 有多少线程引用这些
实例
线程内存模型
每个线程有自己独立的线程上下文包括线程id, 栈, 栈指针, 程序计数器, 通用目的寄存器和条件码 信号屏蔽字 线程优先级
线程之间共享剩余部分, 包括整个用户虚拟地址空间(由只读文本(代码), 读/写数据(全局变量, 本地静态变量), 堆, 共享库代码和数据区域)
共享相同的打开文件集合
线程之间栈相互独立, 基本自己访问自己的栈. 然而如果一个线程得到了另一个线程的栈指针 就可以读写这个栈的任何部分
进度图