pstree

1.解析参数

2.创建根节点

3.遍历进程文件建树,用read_proc_info读取父进程,创建节点

1
2
3
4
5
6
7
8
9
10
11
12
char name[100] = "\0"; 
pid_t p = 0;
pid_t pp = 0;
getPPid(stat_detail, &p, name, &pp);

ProcessNode* node = creatnode(name, p, pp);
ProcessNode* proc = process_table[pp];


if(pp == 0)continue;//加上这一段就只能打印init

proc -> children[proc -> childrencount++] = node;

本意是想取掉一个多余的0号进程,不打印

调试发现是出现了段错误,把print_tree改成打印table[0]就可以了

具体错误有待查询

打印节点

加前缀

1
2
3
4
5
6
7
8
9
10
11
init(Ubuntu-22.
├──── init
├──── SessionLeader
│ └──── Relay(17)
│ ├──── zsh
│ │ └──── pstree-64
│ ├──── zsh
│ │ └──── gitstatusd-linu
│ ├──── zsh
│ └──── zsh
└──── SessionLeader

分三种情况

1.没有父进程,不加前缀

2.有父进程,但没有兄弟进程或者是兄弟进程中的最后一个└────

3.有父进程,有兄弟进程,且不是最后一个├────

根据深度,加|或者空格

可以选择打印传入参数的子进程

修改printf函数之后就可以了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
void print_tree(ProcessNode* node, int level,char* prefix) {
if (node -> childrencount == 0) return;

for(int i = 0;i < node -> childrencount;i++)
{

printf("%s", prefix);//打印前缀
assert(node -> children[i] != NULL);
//printf("%s(%d)\n", node->children[i] -> name,node->children[i] -> pid);
if (level > 0) //如果不是根节点,若是最后一个孩子,则打印└────,否则打印├────
printf(node -> childrencount == (i + 1) ? "└──── " : "├──── ");

//参数为true时,打印pid
if (showpid == 1)
{
printf("%s(%d)\n", node->children[i] -> name,node->children[i] -> pid);
}
else
{
printf("%s\n", node->children[i] -> name);
}

char new_prefix[MAX_SIZE];
strncpy(new_prefix, prefix, sizeof(new_prefix) - 1);
if (level > 0)//如果不是根节点,若是最后一个孩子,则前缀加空格,否则加竖线
{
strcat(new_prefix, node->childrencount == (i + 1) ? " " : "│ ");
}

print_tree(node->children[i], level + 1, new_prefix);//递归打印孩子的孩子节点
}

}

vptr和tbl,typedef

vptr和tbl,typedef

动态绑定

函数指针

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
#include <iostream>
using namespace std;
class base{
int a;
public:
base(int x):a(x){}
base(){}
~base(){}

virtual void print(){cout << "base print" << endl;}
};

class derived: public base{
public:
derived(int x):base(x){}
~derived(){}
virtual void print(){cout << "derived print" << endl;}
};
int main(void)
{
derived d(1);

base* p = new base(1);
p->print();

base c ;
typedef void (*Fun)(void);
Fun pFun = NULL;

cout << "虚函数表地址" << (int*)(&c) << endl;
// 其实就是虚指针vbtr的值,因为vbtr的地址和对象的地址相同

cout << "虚函数表: 第一个函数地址" << (int*)*(int*)(&c) << endl;

pFun = (Fun)*((int*)*(int*)(&c));
pFun();

return 0;
}
1
2
3
Fun pFun = NULL;:这里,pFun是一个Fun类型的指针,也就是一个函数指针。它被初始化为NULL,表示它不指向任何函数。

pFun = (Fun)*((int*)*(int*)(&c));:这里,pFun被赋值为c对象的虚函数表中的第一个函数的地址。这行代码首先获取c的地址&c,然后将其转换为int*类型,解引用得到虚函数表的地址,再次将其转换为int*类型,解引用得到第一个虚函数的地址,最后将其转换为Fun类型。

typedef

C/C++ typedef用法详解(真的很详细)-CSDN博客

1
2
type (*)(....)函数指针 
type (*)[]数组指针
1
2
3
4
5
6
理解复杂声明可用的“右左法则”:
从变量名看起,先往右,再往左,碰到一个圆括号就调转阅读的方向;括号内分析完就跳出括号,还是按先右后左的顺序,如此循环,直到整个声明分析完。举例:
int (*func)(int *p);
首 先找到变量名func,外面有一对圆括号,而且左边是一个*号,这说明func是一个指针;然后跳出这个圆括号,先看右边,又遇到圆括号,这说明 (*func)是一个函数,所以func是一个指向这类函数的指针,即函数指针,这类函数具有int*类型的形参,返回值类型是int
int (*func[5])(int *);
func 右边是一个[]运算符,说明func是具有5个元素的数组;func的左边有一个*,说明func的元素是指针(注意这里的*不是修饰func,而是修饰 func[5]的,原因是[]运算符优先级比*高,func先跟[]结合)。跳出这个括号,看右边,又遇到圆括号,说明func数组的元素是函数类型的指 针,它指向的函数具有int*类型的形参,返回值类型为int
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
1. 原声明:int *(*a[5])(int, char*);
变量名为a,直接用一个新别名pFun替换a就可以了:
typedef int *(*pFun)(int, char*);
原声明的最简化版:
pFun a[5];

2. 原声明:void (*b[10]) (void (*)());
变量名为b,先替换右边部分括号里的,pFunParam为别名一:
typedef void (*pFunParam)();
再替换左边的变量b,pFunx为别名二:
typedef void (*pFunx)(pFunParam);
原声明的最简化版:
pFunx b[10];

3. 原声明:doube(*)() (*e)[9];
变量名为e,先替换左边部分,pFuny为别名一:
typedef double(*pFuny)();
再替换右边的变量e,pFunParamy为别名二
typedef pFuny (*pFunParamy)[9];
原声明的最简化版:
pFunParamy e;

互斥

实现原子指令用到了什么,一小段的不可被打断的指令

自旋锁,把1交换出去,其他的线程只能交换出0,并不断循环交换,临界区结束了之后把1还回去

另一个线程就把锁换过来

如果lock unlock函数加上参数,就相当于可以设多个锁,试图得到同一把锁的线程就实现了互斥

lock(&status)

1
2
3
4
5
6
bool holding(spinlock_t *lk) {//当前是有锁状态,且锁的拥有者是当前cpu
return (
lk->status == LOCKED &&
lk->cpu == &cpus[cpu_current()]
);
}

要正确使用锁很难,要经常使用断言,检查中断是否符合预测assert,可以读读xv6源码

在用户态

用户程序sum++拥有锁,被操作系统中断了,切换到了其他程序,

其他各个线程都无法获得锁,要等操作系统切回去

在操作系统中

会中断来实现锁

在操作系统内核:

连续上两次锁,中断一次再上锁,无法获得锁,就发生死锁

正确性准则

单处理器上锁解锁前后,中断状态不可改变,原来是中断还是中断,原来不中断还是不中断

多处理器,

使用栈保存中断状态

为了实现自旋一定要中断吗?

在用户态

因为自旋锁资源浪费严重

互斥锁(mutex)的实现使用了syscall,具有较好的scalability

futex如果没有锁直接访问,fast path

如何唤醒被syscall挂起的线程?

06-并发控制:互斥 (1)

自己思考一下,想各种情况,修改后可以立即用model checker来验证

互斥 2)

关中断+自旋实现互斥

保存锁前中断状态

多线程

多线程

独立的栈,共享的内存空间

阅读thread.h源码

在thread-qa中

将thread.h移入文件夹,执行make报错找不到库

解决方法:设置TLIB_PATH路径为thread.h所在的文件夹

1
CFLAGS := -O1 -g -I$(TLIB_PATH)

解释

  1. -I$(TLIB_PATH)
    • 这个选项指定了一个包含路径,该路径由 $(TLIB_PATH) 变量定义。
    • $(TLIB_PATH) 可能在 Makefile 的其他地方定义,或者在运行 make 命令时通过环境变量传递。
    • 编译器会在 $(TLIB_PATH) 指定的目录中查找头文件。
  2. -I.
    • 这个选项指定当前目录(.)作为包含路径。
    • 编译器会在当前目录中查找头文件。

多个进程读取写入

支付宝,第一个减去100,第二个在该进程没有减去的时候进行条件判断,也减去100,由于是unsigned long 结果变成很大的数

copilot解释

竞态条件的发生

  1. 共享变量balance 是一个全局变量,多个线程可以同时访问和修改它。
  2. 线程创建main 函数中创建了两个线程,分别执行 T_alipay 函数。
  3. 函数调用:每个线程调用 Alipay_withdraw(100),尝试从 balance 中扣除 100。

竞态条件的具体过程

  1. 线程1和线程2同时检查 balance

    • 线程1检查 balance 是否大于等于 100,结果为真。
    • 线程2也检查 balance 是否大于等于 100,结果也为真。
  2. 线程1和线程2同时进入 if

    • 线程1进入 if 块并调用 usleep(1),暂时让出CPU。
    • 线程2也进入 if 块并调用 usleep(1),暂时让出CPU。
  3. 线程1和线程2同时修改 balance

    • 线程1从 balance 中减去 100,balance 变为 0。
    • 线程2也从 balance 中减去 100,balance 无符号整数会溢出

如果是两个进程都循环加1加到10000,结果不会是20000

原代码反汇编

1
2
3
4
13ea:       48 8b 05 4f 2c 00 00    mov    0x2c4f(%rip),%rax       
# 4040 <sum>
13f1: 48 83 c0 01 add $0x1,%rax
13f5: 48 89 05 44 2c 00 00 mov %rax,0x2c44(%rip)

修改成一条汇编指令

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
2
3
load(sum + N)
;循环N次
store(num)

O2优化sum=200000000

1
2
3
0000000000001260 <T_sum>:
1264: 48 81 05 d1 2d 00 00 addq $0x5f5e100,0x2dd1(%rip)
;改成了一条指令,两个进程碰在一起的概率很低

处理器也是编译器,所以单线程的处理器可能会优化,调换程序执行的顺序(在结果不变的情况下)

也是状态机,流水线,读写不冲突就能同时执行

所以……相对论?

共享内存只是一个简化的假象

mem-modle

一个是写Y读X,一个是写X读Y,得按特定顺序才能输出1,1,所以很少

arm与x86的内存模型不同,对于多线程的程序模拟难度大

嵌套类

Club嵌套Coach

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Coach{
string name;
int winRate;
public:
Coach(string n, int wr){
name=n; winRate=wr;
}
void show();
};
class Club{
string name;
Coach c;
int year;
public:
Club(string n1, int y, string n2, int wr);
void show();
};

Club的初始化和show函数要利用Club类中的东西

1
2
3
4
5
6
7
8
Club::Club(string n1,int y,string n2,int wr):c(n2,wr){
name = n1;
year = y;
}
void Club::show(){
cout << name << " " << year << "\n";
c.show();
}

同样,在下面Circle调用内部Point类型的变量,要有Point中的get函数配合才能取出相应的x,y

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#include<bits/stdc++.h>
using namespace std;
class Point{
private:
double x,y;
public:
Point(int n,int m):x(n),y(m){}
double get_x(){return x;}
double get_y(){return y;}
};
class Circle{
private:
Point o;
double r;
public:
Circle(Point p,double x):o(p){r = x;}
bool isPointIn(Point p){
double a = o.get_x();
double b = o.get_y();
double c = p.get_x();
double d = p.get_y();
double x = sqrt((a - c) * (a - c) + (b - d) * (b - d));
if(x < r)return true;
return false;
}
};

实现库函数printf

实现库函数printf

1
int    printf    (const char *format, ...);

这个程序除了调用的库函数不同 (例如没有 stdio.h;多了 am.h) 之外,它就是一个完全符合 C 标准的普通程序,但因为没有操作系统和标准库的支持,我们需要编写所有的库函数。例如,printf 也来自我们的代码,它调用了 AbstractMachine 提供的 putch API:

并发问题

该例子是一个生产者一个消费者,缓冲区是1,如果缓冲区很大这个问题就会被忽视了,如果生产者和消费者数量增加,那么死锁概率也会增加

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//错误的
void wait(struct condvar *cv, mutex_t *mutex){
mutex_lock(&cv -> lock);
cv -> nwait++;
mutex_unlock(&cv -> lock);
//理想状态是球在生产者和消费者之间传递
mutex_unlock(mutex);
//但是如果在这里broadcast抢占,先唤醒了,然后nwait=0了,
//就相当于唤醒了另一个同样是生产者或者是消费者线程甚至是自己把球抢走了,再检查条件再进入wait,但是没有线程再放球了就会死锁
P(&cv -> sleep);//这个睡眠和解锁顺序不能更改


mutex_lock(mutex);
}

问题是,为什么前一个线程执行完的broadcast后,consumer(12896)为什么没被唤醒,而后面的producer(12897)被唤醒了,而且例子中只有一个生产者和一个消费者,缓冲区大小为1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
------Consumer: begin, execution count: 12896------
Consumer: Waiting
------waiting------
in wait nwait: 1
waiting before sleep

------Producer: begin, execution count: 12896------
(
depth = 1
Producer: Broadcast
------broadcast_start------
start broadcast
int broadcast nwait: 1
------broadcast_end------
------Producer: end, execution count: 12896------

------Producer: begin, execution count: 12897------
Producer: Waiting
------waiting------
in wait nwait: 1
waiting before sleep
------waiting out------
Producer: Waiting
------waiting------
in wait nwait: 2
waiting before sleep

标准C++类stdstring的内存共享和Copy-On-Write技术

标准C++类std::string的内存共享和Copy-On-Write技术_std::string重新分配内存拷贝字符串-CSDN博客

string的懒拷贝

当函数返回一个静态字符串时,这个字符串存储在函数的静态存储区域中。这个静态存储区域在程序的整个生命周期内都存在,并且在程序加载时分配,在程序退出时释放。

在你描述的情况中,函数GetIPAddress返回一个静态字符串,它在动态链接库的地址空间中分配。当动态链接库被释放时,这个地址空间也被释放,导致返回的静态字符串指向的内存变得无效。但是,由于字符串对象的值是存储在这个无效内存中的,所以在后续程序中使用这个字符串时就会出现未定义行为,因为访问了无效的内存地址。

这种情况下,即使程序在后续没有使用到这个字符串,当程序退出时,会调用字符串对象的析构函数,尝试释放这个无效内存,进而导致内存访问异常,导致程序崩溃。

模板的全特化与偏特化

模板的全特化与偏特化

模板函数和模板类有的时候可能需要对传入的不同类型进行不同的处理,比如说有的模板传入int或double类型都可以处理,但是传入char型则会出错,这时就需要模板特化的方式。

类模板全特化:

全特化即将模板类型里的所有类型参数全部具体指明之后处理,如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
template<typename T,typename C>
struct A
{
A(){cout<<"泛化版本构造函数"<<endl;}
void func()
{
cout<<"泛化版本"<<endl;
}
};

template<>//把类型取出
struct A<int,int>
{
A(){cout<<"int,int特化版本构造函数"<<endl;}
void func()
{
cout<<"int,int特化版本"<<endl;
}
};

template<>中为空,代表所有类型都在下面特殊化处理,上面相当于对int,int进行了分别的处理,其他类型依然是泛化版本。

对类中的某个成员函数进行特化处理

还是以上面给的例子为基础,特化func()成员函数,当A的模板参数为<int,double>时,调用特化版的func()。

1
2
3
4
5
template<>
void A<int,double>::func
{
cout<<"int,double特化版本函数"<<endl;
}

类模板的偏特化

类模板偏特化(局部特化):顾名思义,只特殊化几个参数或者一定的参数范围

个数偏特化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
template<typename T,typename C,typename D>
struct A
{
void func()
{
cout << "泛化版本" << endl;
}
};

template<typename C>
struct A<int,C,int>
{
void func()
{
cout << "int,C,int偏特化版本" << endl;
}
};

template<>括号中存留的参数是依然可以任意填的参数。

STL中的一个个数偏特化例子:

1
2
3
//泛化
template <class T,class Alloc = alloc>
class vector{}
1
2
3
4
//特化
template <class Alloc>
class vector<bool, Alloc>
{};

范围偏特化

记住这种情况的template<>中还是要填上原有的大类型,且const T*属于T*不属于const T

注意范围二字,比如const int属于int的一个小范围,int *和const int*属于int的一个小范围,int&属于int的一个小范围,int&&属于int的一个小范围

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
template<typename T>
struct A
{
void func()
{
cout << "泛化版本" << endl;
}
};

template<typename T>
struct A<const T>
{
void func()
{
cout << "const T版本" << endl;
}
};

template<typename T>
struct A<T*>
{
void func()
{
cout << "T*版本" << endl;
}
};

STL中的一个范围偏特化例子:
在这里插入图片描述

调试

理解状态机执行:不是 “调试”,也是 “调试”

  • ssh:使用 -v 选项检查日志
  • gcc:使用 -v 选项打印各种过程
  • make:使用 -nB 选项查看完整命令历史

调试:不仅是 “调试器”

  • Profiler: perf - “采样” 状态机
  • Trace: strace - 追踪系统调用

将日志文件重定向到vim进行各种操作

1
strace -f g++ a.cc |& vim -

执行 grep 命令进行过滤,可以通过 :! 来运行外部命令并将结果显示在 vim 中。

步骤如下:

  1. vim 中输入:

    1
    :!grep "关键词" 文件名

    或者,你可以直接对 vim 缓冲区的内容使用管道进行过滤:

    1
    :%!grep "关键词"
    • :!grep "关键词":会在当前终端执行 grep,但不影响你在 vim 中的内容。
    • :%!grep "关键词":会将当前文件内容通过管道传递给 grep,然后将过滤结果替换当前文件的内容。
  2. 例如:

    1
    :%!grep "error"

    这会将所有包含 error 的行保留,并替换掉当前缓冲区的内容。

如果包含.h这种,使用转义字符

1
:%!grep "\.h"