collect采用gpt生成,适用于mosaic结果的分析
1 |
|
collect采用gpt生成,适用于mosaic结果的分析
1 |
|
让gdb启动时就打开tui
编写gdbinit文件

所以先在home目录下设置gdbinit
然后执行
1 | source ~/.gdbinit |
hanoi
每个Frame(栈帧)都有自己的变量,pc是相互独立的,指示自身栈帧下一步要执行什么
每次call,入栈,把pc置为0,在while(1)开头再指向top
每次执行完case语句,都把pc加1,不一定是栈顶的
把递归的分解为6步,逐步执行,case3移动一个不需要保存,在case6加1就行
1 | int hanoi(int n, char from, char to, char via) { |
存在自旋,但是自旋仅在guard保护修改flag和队列中,比用户的临界区短
1 | 1 typedef struct __lock_t { |
guard的作用是保护flag和队列的修改,flag保护临界区
在unlock中,唤醒等待线程后,不需要设置flag=1,这样直接将锁从解锁的线程传给了被唤醒的线程。
是的,在原文的代码实现中,线程被唤醒后就直接进入临界区
存在的问题就是唤醒的丢失,因为A线程加入队列后如果切换到释放锁的线程B,线程B唤醒A后,A继续执行然后睡眠,但是此时等待队列已经没有A了,所以会造成死锁
使用setpark,如果setpark后切换到另一个线程,调用unpark释放后,返回lock函数,pack会直接返回而不再睡眠
1 | void lock(lock_t *m) { |
1.解析参数
2.创建根节点
3.遍历进程文件建树,用read_proc_info读取父进程,创建节点
1 | char name[100] = "\0"; |
本意是想取掉一个多余的0号进程,不打印
调试发现是出现了段错误,把print_tree改成打印table[0]就可以了
具体错误有待查询
打印节点
加前缀
1 | init(Ubuntu-22. |
分三种情况
1.没有父进程,不加前缀
2.有父进程,但没有兄弟进程或者是兄弟进程中的最后一个└────
3.有父进程,有兄弟进程,且不是最后一个├────
根据深度,加|或者空格
可以选择打印传入参数的子进程
修改printf函数之后就可以了
1 | void print_tree(ProcessNode* node, int level,char* prefix) { |
vptr和tbl,typedef
动态绑定
函数指针
1 |
|
1 | Fun pFun = NULL;:这里,pFun是一个Fun类型的指针,也就是一个函数指针。它被初始化为NULL,表示它不指向任何函数。 |
typedef
C/C++ typedef用法详解(真的很详细)-CSDN博客
1 | type (*)(....)函数指针 |
1 | 理解复杂声明可用的“右左法则”: |
1 | 1. 原声明:int *(*a[5])(int, char*); |
实现原子指令用到了什么,一小段的不可被打断的指令
自旋锁,把1交换出去,其他的线程只能交换出0,并不断循环交换,临界区结束了之后把1还回去
另一个线程就把锁换过来
如果lock unlock函数加上参数,就相当于可以设多个锁,试图得到同一把锁的线程就实现了互斥
lock(&status)
1 | bool holding(spinlock_t *lk) {//当前是有锁状态,且锁的拥有者是当前cpu |
要正确使用锁很难,要经常使用断言,检查中断是否符合预测assert,可以读读xv6源码
在用户态
用户程序sum++拥有锁,被操作系统中断了,切换到了其他程序,
其他各个线程都无法获得锁,要等操作系统切回去
在操作系统中
会中断来实现锁
在操作系统内核:
连续上两次锁,中断一次再上锁,无法获得锁,就发生死锁
正确性准则
单处理器上锁解锁前后,中断状态不可改变,原来是中断还是中断,原来不中断还是不中断
多处理器,
使用栈保存中断状态
为了实现自旋一定要中断吗?
在用户态
因为自旋锁资源浪费严重
互斥锁(mutex)的实现使用了syscall,具有较好的scalability
futex如果没有锁直接访问,fast path
如何唤醒被syscall挂起的线程?
自己思考一下,想各种情况,修改后可以立即用model checker来验证
关中断+自旋实现互斥
保存锁前中断状态
多线程
独立的栈,共享的内存空间
阅读thread.h源码
在thread-qa中
将thread.h移入文件夹,执行make报错找不到库
解决方法:设置TLIB_PATH路径为thread.h所在的文件夹
1 | CFLAGS := -O1 -g -I$(TLIB_PATH) |
解释
-I$(TLIB_PATH):$(TLIB_PATH) 变量定义。$(TLIB_PATH) 可能在 Makefile 的其他地方定义,或者在运行 make 命令时通过环境变量传递。$(TLIB_PATH) 指定的目录中查找头文件。-I.:.)作为包含路径。多个进程读取写入
支付宝,第一个减去100,第二个在该进程没有减去的时候进行条件判断,也减去100,由于是unsigned long 结果变成很大的数
balance 是一个全局变量,多个线程可以同时访问和修改它。main 函数中创建了两个线程,分别执行 T_alipay 函数。Alipay_withdraw(100),尝试从 balance 中扣除 100。线程1和线程2同时检查 balance:
balance 是否大于等于 100,结果为真。balance 是否大于等于 100,结果也为真。线程1和线程2同时进入 if 块:
if 块并调用 usleep(1),暂时让出CPU。if 块并调用 usleep(1),暂时让出CPU。线程1和线程2同时修改 balance:
balance 中减去 100,balance 变为 0。balance 中减去 100,balance 无符号整数会溢出如果是两个进程都循环加1加到10000,结果不会是20000
原代码反汇编
1 | 13ea: 48 8b 05 4f 2c 00 00 mov 0x2c4f(%rip),%rax |
修改成一条汇编指令
1 | 13ea: 48 ff 05 4f 2c 00 00 incq 0x2c4f(%rip) |
改成一条指令,如果在一个处理器上还能正确,但是在多处理器上还会错误。
看着是一条指令,实际上不是原子指令。
printf是线程安全的
因为汇编指令取值,在中间寄存器加1后可能会中断,再放入变量对应地址中,结果可能就只是加一个1,而不是两个1
最小可以小于10000,可以改成汇编指令
为什么是2?
每个进程有一个n次循环,n个进程
在关键进程中,最后一步store,之前,已经循环了n-1次了,这两次的最小值为1(进程A第一个循环store()时,关键进程第n-1个循环正好结束,进程A,store后sum=1)关键进程执行前两步,然后关键进程等待其他进程结束后执行store(2,sum)
编译器优化,可能会隐藏并发的bug(都假设状态迁移是原子性,顺序执行)
O1优化sum=100000000
1 | load(sum + N) |
O2优化sum=200000000
1 | 0000000000001260 <T_sum>: |
处理器也是编译器,所以单线程的处理器可能会优化,调换程序执行的顺序(在结果不变的情况下)
也是状态机,流水线,读写不冲突就能同时执行
所以……相对论?
共享内存只是一个简化的假象
mem-modle
一个是写Y读X,一个是写X读Y,得按特定顺序才能输出1,1,所以很少
arm与x86的内存模型不同,对于多线程的程序模拟难度大
实现库函数printf
1 | int printf (const char *format, ...); |
这个程序除了调用的库函数不同 (例如没有 stdio.h;多了 am.h) 之外,它就是一个完全符合 C 标准的普通程序,但因为没有操作系统和标准库的支持,我们需要编写所有的库函数。例如,printf 也来自我们的代码,它调用了 AbstractMachine 提供的 putch API:
Club嵌套Coach
1 | class Coach{ |
Club的初始化和show函数要利用Club类中的东西
1 | Club::Club(string n1,int y,string n2,int wr):c(n2,wr){ |
同样,在下面Circle调用内部Point类型的变量,要有Point中的get函数配合才能取出相应的x,y
1 |
|