南京大学操作系统学习笔记
1、绪论
只需要记住操作系统是为了实现多个程序看起来独占运行
2、应用眼中的操作系统
1:46 什么是应用程序
以elf格式存储 头部+其他节
23:33 应用程序怎么调用操作系统
31:29 工具观察程序的执行 --调试器 gdb命令 layout asm指令调出
gdb a.out
layout asm
si //单步执行
44:33 objdump命令
52:20 main()之前发生了那些操作系统API 调用
工具strace
59:58 demo1:gcc调用的操作系统的API(生成其他进程)
strace -f gcc a.c 2>&1 | grep execve
65:50 demo2:图形界面xedit调用的系统API strace xedit(read 和write数据)
3、多处理器编程
1:55 并发与并行的概念
操作系统可以同时加载多个程序:每个程序都有独立的内存空间,互不干扰;每隔一段时间,切换到另一进程执行(看起来同时运行)
6:08 并发性的来源
进程调用操作系统的API
10:19 典型的并发系统
主要关注***多处理器、共享内存***情况
11:50 线程定义
并行执行、共享内存、执行顺序不确定
15:00 线程间什么共享、什么不共享
资源包括:
- 代码资源(指令序列)
- 寄存器资源(rip,rsp,rax等等)
各个寄存的功能介绍x86寄存器问题 - 局部变量所在的堆栈、全局变量
其中,代码资源、全局变量数据共享,每个线程的堆栈和寄存器都是独享
20:16 POSIX的线程库
查看线程库手册
man 7 pthreads
28:44 线程创建 threads.h的实现
#include <stdlib.h>
#include <stdio.h>
#include <assert.h>
#include <pthread.h>
struct thread{ //链表
int id;//线程id 线程号 从1开始
pthread_t thread;//POSIX线程库规定的线程号
void (*entry)(int); //线程的入口地址
struct thread *next;//指向下一个线程
};
struct thread *threads; //单恋表头
void (*join_fn)();
__attribute__((destructor)) static void join_all(){ //main函数结束之后调用
for(struct thread *next;threads;threads=next){ //遍历整个进程链表
pthread_join(threads->thread,NULL);
next=threads->next;
free(threads);
}
join_fn?join_fn():(void)0;
}
static inline void *entry_all(void *arg){//符合pthreads_create要求
struct thread *thread =(struct thread *)arg;//取出线程对象指针cur
thread->entry(thread->id);//调用该线程的entry,对线程入口赋值
return NULL;//
}
//线程创建
static inline void create(void *fn){
struct thread *cur=(struct thread *)malloc(sizeof(struct thread));//分配内存
assert(cur);//假设内存分配成功
cur->id=threads?threads->id+1:1;//当钱线程的线程号
cur->next=threads;
cur->entry=(void (*)(int))fn;
threads =cur;
pthread_create(&cur->thread,NULL,entry_all,cur);
}
static inline void join(void (*fn)()){
join_fn=fn; //将函数指针赋值给全局变量
}
28:57两个多线程程序测试:
1、创建两个线程,分别打印a和b a.c
编译命令 gcc b.c -I. -lpthread^C
#include "threads.h"
void a() {while(1) printf("a");}
void b() {while(1) printf("b");}
int main(){
setbuf(stdout,NULL);
create(a);
create(b);
}
2、创建1000个线程,证明全局变量共享
#include "threads.h"
void f(){
static int x=0; //共享x
printf("hello from thread #%d\n",x);
while(1);
}
int main(){
for (int i=0;i<1000;i++)
create(f);
join(NULL);
}
35:00 用代码证明了pthread给每个线程分配了8M的堆栈。没有导致内存不够是因为内存虽然分配,但是只是在线程的地址空间标记了一下,没有真正分配。
4、多处理器编程的困难
41:30 共享资源
并发/并行执行的线程可能对资源内存进行争抢
示例1:输出的并不是2n,这就是因为sum++可以分解为三条指令,交替执行
#include "threads.h"
long sum =0 ;
void do_sum(){
for(int i=0;i<100000000;i++)
sum++;
//printf("sum=%ld\n",sum);
}
void print(){
printf("2sum=%ld\n",sum);
}
int main(){
create(do_sum);
create(do_sum);
join(print);
}
示例2:不同的编译优化等级输出不同
71:00 保证原子性(本课程重点)
理解并发程序执行的工具
1、串行程序的状态机模型
A、状态机的概念
B、程序=有限状态机
寄存器看成状态机的节点;程序指令的执行看成状态机的边
11:34 不确定的指令可能有多个后续状态
不确定性的来源比如:
19:33 X86-64的寄存器
这些寄存器的状态可以被gdb观察到,gdb中运行下面的命令:
info registers
查看进程的进程号:info inferiors
22:32 状态机模型的应用
A、超标量处理器,允许在状态机跳跃
B、程序分析技术
gdb step过头了,但是记录了状态机,就可以跳转到之前的状态
C、记录和重放
对于确定性的指令,只需要记录初始状态和执行步骤,就可以实现回放;
对于不确定的指令(syscall) 还需要记录这条指令的结果,重放时将记录的结果当做syscall的返回值
37:36 并发程序的状态机模型
在每一个时刻,都可以选择任何一个线程程序向前执行一步,导致并发程序的状态机很大
45:00 理解并发程序的执行
peterSon算法,直接理解代码:
构造状态机:(PC表示指令指针,PC1=1表示第一个线程执行到指令1)
那么判断一个并发程序正确,有两个条件:
- 不存在错误状态(这里是PC13^PC22)
- 不存在无穷路径(状态图有环)