yter1  

c++语言篇

C++11有什么新特性?

  • auto 用法语言类型自动推导; 迭代器循环遍历
    for(auto num:nums){}
  • lamada表达式
    auto dfs = [&]( this auto&& dfs, int i, int j )->void{}
    auto compare = [&](pair<int,int> x,pair<int,int> y)->int
  • 列表初始化,默认初始化为0
  • 迭代器 容器
    std::vector::iterator iter = vec.begin();
    std::list::iterator iter = lst.begin();
    std::deque::iterator iter = deq.begin();
    std::forward_list::iterator iter = flst.begin();
    std::unordered_map<int,int>::iterator iter = hash_table.begin();
  • const contexpr
    delctypedecltype 通过分析表达式来推导类型,而不是通过变量的初始值。这使得 decltype 在某些情况下比 auto 更适用,尤其是在泛型编程中。
    noexpect(异常抛出,减少异常捕捉的代码)
    const int* a;//变量存储的是常量
    int* const a;//变量存储的是常量指针,不能改变指向的,必须初始化
  • c++11 alignas alignof
    struct A {
    int a;
    virtual funcA(){}
    }

面向对象语言的三大特性:封装、继承、多态。

经典面试题:如何使用C语言实现C++的三大特性(封装、继承和多态)?

封装性

  • 封装性是通过将属性与方法捆绑在一起,然后隐藏内部实现的细节,外部只能通过内部暴露的接口进行访问。
  • C++语言中支持数据封装,类是支持数据封装的工具,对象是数据封装的实现。c语言中结构体是支持数据封装的工具,对象是数据封装的实现。在封装中,还提供一种对数据访问的控制机制,使得一些数据被隐藏在封装体内,因此具有隐藏性。封装体与外界进行信息交换是通过操作接口进行的。
  • 封装性实际上是由编译器去识别关键字public、private和protected来实现的,体现在类的成员可以有公有成员(public),私有成员(private),保护成员(protected)。
  • 私有成员是在封装体内被隐藏的部分,只有类体内说明的函数(类的成员函数)才可以访问私有成员,而在类体外的函数时不能访问的,公有成员是封装体与外界的一个接口,类体外的函数可以访问公有成员,保护成员是只有该类的成员函数和该类的派生类才可以访问的。
c 封装性
int add(int x, int y)// 定义函数指针与属性绑定在一起
{
    return x+y;
}

// 定义函数指针
int(*padd)(int , int ); //函数指针,指针变量的名字是padd
padd = add;
*padd(3,4)(c调用写法)  pdd(3,4)(c++调用写法)

// 用typedef重命名
typedef int(*PADD)(int , int ); //注意typedef后要加分好,#define宏定义后没有分号
PADD padd;
padd = add;

struct A{
	int x,y;
	PADD padd;
};
struct B{
	int z;
	PADD padd;
};

继承性

  • 继承性是指子类允许继承父类的属性与方法,提高代码的复用性,减少冗余,使得编译期速度提高。
  • C++语言允许单继承和多继承。继承是面向对象语言的重要特性,是实现抽象和共享的一种机制。

多态性

  • 多态性是指同一个方法或属性在不同的封装体中的表现不一样。它依赖于继承,包括运算符重载,函数重载。
  • C++中多态特性的工作依赖虚函数的定义,在需要解决多态问题的重载成员函数前,加上virtual关键字,那么该成员函数就变成了虚函数,从上例代码运行的结果看,系统成功的分辨出了对象的真实类型,成功的调用了各自的重载成员函数。如果在基类某些函数前面声明为virtual,则C++编译器会在内存对象模型前面插入一个虚函数指针,该指针指向一个虚函数表,正是因为我们可以改变虚函数指针所指函数表地址,从而实现其多态操作。
  • C要自己实现,要自己对函数指针成员变量重新赋值,而且有多少个多态函数,就得在构造函数初始化多少个函数指针地址。都做到了构造时才确定 指定变量名A.func 调用哪个函数。
C语言模拟C++的继承与多态
//C语言模拟C++的继承与多态
typedef void (*FUN)();      //定义一个函数指针来实现对成员函数的继承
struct _A       //父类
{
    FUN _fun;   //由于C语言中结构体不能包含函数,故只能用函数指针在外面实现
    int _a;
};
struct _B         //子类
{
    _A _a_;     //在子类中定义一个基类的对象即可实现对父类的继承
    int _b;
};
void _fA()       //父类的同名函数
{
    printf("_A:_fun()\n");
}
void _fB()       //子类的同名函数
{
    printf("_B:_fun()\n");
}

经典面试题:如何使用lua脚本语言实现c++的面向对象的三大特性?

  • lua本身就是由一张大表构成,所以table的概念本身就满足封装性,将属性和方法封装在一个表中,通过方法进行访问。
  • 继承可以通过元表(metatable)和 __index 字段来实现:
  • 多态可以通过在派生类中重写基类的方法来实现,通过setmatetable去实现,设置好元方法以及元函数,当子表找不到成员函数时候,就先去子表设置的__index中查找获取途径,获取途径索引到父表,然后去把父表中的成员函数function拷贝到子表当中,这样就实现了多态。当然还得考虑多重继承的多态,递归的层数也得考虑。
lua面向对象实现

内存管理

内存分区与进程内存分区

0xFFFFFFFFFFFF (128TB) ┌───────────────────────┐
                        │ Kernel Space            │
                        ├───────────────────────┤ ← 用户空间与内核空间分界
                        │ Stack (向下增长)        │
                        ├───────────────────────┤
                        │ Memory Mapping Segment │
                        │ (mmap区域:共享库、文件映射等)│
                        ├───────────────────────┤
                        │ Heap (向上增长)         │
                        ├───────────────────────┤ ← brk/sbrk控制堆顶
                        │ .bss (未初始化全局变量)  │
                        ├───────────────────────┤
                        │ .data (已初始化全局变量) │
                        ├───────────────────────┤
                        │ .rodata (只读常量)      │
                        ├───────────────────────┤
                        │ .text (代码段)         │
0x400000              └───────────────────────┘
程序的运行分区 pro.c
#include "stdio.h"
int a = 0;//全局初始化区
char *p1;//   全局未初始化区
void main(void)
{
  int   b;//   栈
  char   s[]   =   "abc";//   栈
  char   *p2;//   栈
  char   *p3   =   "123456";//   123456/0在常量区,p3在栈上。
  static   int   c   =0;//   全局(静态)初始化区
  p1   =   (char   *)malloc(10);
  p2   =   (char   *)malloc(20);
  //分配得来得10和20字节的区域就在堆区。
  strcpy(p1,   "123456");//   123456/0放在常量区,编译器可能会将它与 
  //p3所指向的"123456" 优化成一个地方。
}
//ps -ef | grep pro
//pmap -p pro_pid
//gdb pro
//

内核区(ecc机制失效导致高位01反转,换内存即可)
栈区(8M)自动变量、函数调用帧 0x7ffd... ~ 高地址
共享库、文件映射、虚拟内存空间 同堆区
堆区(TB) new/malloc动态分配 brk指针动态扩展 内存池解决碎片化问题(从内存中的拿大块内存,并统一时间归还,申请的大块内存由程序员去操作,如果按8字节对齐,申请了12字节,则有4字节的浪费))
.bss(better save space)(全局未声明) 0x601000~0x602000
.rdata(常量,虚函数表) 0x400000~0x401000
.data(全局已声明) 0x601000~0x602000
.txt(二进制代码)

一些关键字:
mutable:类成员经常变得量
volatile:重新去内存中读取,经常变的量
const:常量
CAS(compare and swap):使用一个变量统计网站的访问量; Atomic类操作; 数据库乐观锁更新。

constexpr

final
override

程序从代码到二进制可执行文件的过程

阶段概括版本

预处理阶段(.cpp->.ii or .c->.i 本质上还是高级语言)把#define #include 直接插入到代码引用处,属于超级文本替换阶段,宏展开错误、头文件重复包含(#define替换,头文件插入,条件编译、注释删除等)
|
编译阶段(.ii - > .s) 预处理后的代码进行类型检查,语法树转换为抽象语法树((类型检查,确认值推导,死代码消除等)),形成汇编语言,属于语法类型检查(语法错误、类型不匹配),代码优化,比如之前说的内存对齐,死代码消除
|
汇编阶段(.s->.o) as汇编器,(指令编码错误)执行汇编指令,在寄存器存储,在堆栈区中,常量区,全局区中拿数据,然后进行计算,最终形成比特流,也就是二进制代码
|
链接重定向阶段(.o+.a -> 可执行程序) ld链接器,将所有.o文件,以及静态库 libc.a 库中的,我们所使用到的那部分,比如print.o拿出来,进行代码区链接,虚拟内存整合)(进行未定义引用检查,代码段,数据区合并,静态库复制到可执行文件中)

链接阶段是指把所有的目标文件和库文件,合并成1个可执行文件。其中要做以下处理,符号解析,我们定义的所有函数,变量,可能位于目标文件外部,要找到他们的实际地址并关联;重定位,每个目标文件都有自己的地址,要在最终的可执行文件中,重定位一下;每个目标文件有自己的分段,要将分段合并;

重定向阶段 (可重定位目标文件就是自己写的代码.o 可执行目标文件 共享目标文件就是在应用程序执行前使用加载器进行libxx.so加载)

运行时加载动态库(使用dlopen,dlsym,dlclose,dlerror 需要一点系统调用的性能开销就能实现)
虚拟内存
二进制代码区也就是.txt区(存放着已经编译好的程序代码)
.bss(better save space)区存放的是全局和静态变量(静态变量默认为0,并且是在被加载进内存的时候才会分配值为0,等到实际定义的时候,加载进内存,这个0值才会被覆盖为有效数据)
.data区是存放着已经初始化的变量,在磁盘是占地址空间的,.bss只是个占位符
.rodata区存放的是只读数据,比如printf

处理目标文件的一些常见命令
反汇编objdump
编译as
gstack
gcore
pmap

编译预处理期

由程序员编写的代码属于高级语言,首先经过编译器进行编译预处理,概括的说是处理所有括号内的东西,对括号内的引用进行插入和高级文本替换,这个过程是不涉及类型检查的。所以编译预处理函数、编译预处理的变量替换、头文件的引用等等,都是直接暴力插入到我们编写的代码中,此时经过编译预处理后的产物仍然是属于我们的高级语言的范畴,由.c -> .i or .cc -> .ii

编译期

此时将编译预处理的产物进行编译器正式编译,编译时是不分配内存的。此时只是根据声明时的类型进行占位,到以后cpu执行程序时,才正确分配内存,才有我们所讨论的那些进程内存分区。
const 修饰的变量在编译期间只是去做一个代码折叠操作,const int a = 1;从而优化代码的长度,只有全局的const变量才可能给它占位赋值。对于局部的还得在运行期才会分配内存赋值,const 分配内存,宏定义是不分配内存的,是编译期间直接替换插入,局部变量是在运行期加载进的,全局const则是在放在全局常量区。
constexpr修饰的变量或者函数,是指在编译期就对它进行事先占位,在运行期显示使用时候才会动态分配内存赋值。constexpr体现的是编译时期的“常量性”, 如不能作用于virtual的成员函数,就会使用constexpr修饰。
内存对齐也是在编译期进行的,未声明要求就按照成员变量中最大字节的数据类型进行对齐。对于struct来说,每个成员变量和函数都拥有自己的内存对齐的方式,union则是按最大的成员变量的内存进行倍数对齐。[C/C++计算类/结构体和联合体(union)所占内存大小(内存对齐问题)](https://blog.csdn.net/luolaihua2018/article/details/115372273)
宏定义和函数,宏定义是发生在编译时被替换,函数是在运行时查找具体的函数代码,函数内的局部变量也是运行时加载到栈中,宏定义不做类型检查,函数是会做类型检查的
宏定义和typedef,宏定义是编译时被替换,typedef是编译的一部分,也需要做类型检查
量的声明与定义的区别,变量可以多处地方声明(extern),因为声明是不分配内存的,但是一旦在运行过程中定义,分配内存就只有一处定义,它会覆盖占位符字
内联函数是在编译期进行代码插入,省去栈对函数以及参数的调用开销。

汇编期

可以使用pmap -p pid 可查看到进程的进程的内存映射空间分布
anon 就是匿名堆区,stack就是栈区,然后就是一堆动态库的链接了

内存碎片的管理与内存分配

image

内存池(_pool 和 allcaota)
自定义的内存分配池
依赖于RAII的智能指针

堆区栈区
栈与堆的动态管理对比
特性 栈区 堆区
分配方式 编译器自动管理(push/pop指令) 程序员手动控制(new/delete)
增长方向 高地址→低地址(x86架构) 低地址→高地址
速度 纳秒级(寄存器操作) 微秒级(涉及系统调用和内存管理)
容量限制 默认8MB(可通过ulimit -s调整) 受虚拟内存大小限制(TB级)
碎片问题 无 需通过内存池或GC解决

new/delete 与malloc/free异同
new 和 delete的实现
delete 和 delete[] 区别,对象析构次数不同
malloc 和 new 的区别
realloc 和 calloc(分配且清零)
注意,realloc() 函数调整先前分配的内存块大小,可能会原地调整大小,也可能会重新分配一块新的堆内存。当重新分配新的内存块时,realloc() 函数会将旧块的内容复制到新位置,然后将旧块内存释放掉。这种情况下,指针 ptr 仍指向原来的旧内存块,继续使用该指针可能会导致未定义的行为。
内部碎片与外部碎片https://www.zhihu.com/question/559199744/answer/3457814196

指针与引用的区别,右值引用与左值引用的区别?
初始化考虑,指针可不被初始化,但是会产生野指针造成段错误,使用dgb bt可查看报错的函数栈顶位置。
指针是指变量存储的是一个内存地址,引用是变量的别名,这也就是最基础的右值引用了
sizeof(指针)是指指针的大小,比如int*,在32位就是4字节,64位就是8字节
const char str[] = "hello,world!";
sizeof(str) = 13
strlen(str) = 12

const char* str = "hello,world!";
sizeof(str) = 4(32位) 8(64位)
sizeof(*str) = 14
strlen(str) = 13

const char* str[] = {"hello,world!"};
sizeof(str) = 4 or 8
strlen(str) = 12

指针类型辨别:
int p[10] 指针数组,存了10个指针
int (
p)[10] 数组指针,指向一个int数组
int p(int) 函数声明,返回值是int参是int
int (*p)(int)函数指针,返回值和参都是int
主要讨论引用与指针是在传参方面:
引用传参可以避免临时对象的拷贝,引用的是原数据地址,实参形参不是副本,指针则是拷贝副本。
对于栈敏感的就使用栈,因为栈内存小,大概只有4M。而堆的内存就大得多了,1-4G。
在对于类构造函数中,默认使用的都是引用传参的方法,因为类内存如果拷贝一个临时副本,效率高内存小,尽量使用零拷贝的思想和技术。
右值语义是由移动语言move实现的,主要是指对象控制权的转发,和指针,引用进行对比的话,我们常常在拷贝构造函数中去使用右值语义,指针适合不影响原数据时候使用,但是又带来了指针的释放和临时拷贝对象数据的构造和析构问题,不合适;引用可以改变原数据,而且也不用担心指针的释放问题和临时对象构造与析构问题,适合传参。
深浅拷贝指的是值拷贝和内存拷贝的意思,前者是只拷贝一份值副本,后者是直接把指向内存的指针都拷贝到来,而且可以改变原数据。

指针与内存的高能关系:
(1)内存分配失败

coredump
void GetMemory(char *p)
{
	p = (char *)malloc(100);
}
void Test(void)
{
	char *str = NULL;
	GetMemory(str);
	strcpy(str, "hello world");
	printf(str);
}

GetMemory()栈帧被释放了,栈内函数生命周期已经结束,str内存分配失败,简单判断一下就好了,要么使用引用传参或者说二级指针传参,要么改变栈内函数块内的数据的生命周期,比如说使用全局函数或者全局指针,改变数据或者函数的生命周期。

(2)栈内存已经释放,访问了一块已经释放了的内存区域

点击查看代码
char *GetMemory(void)
{
	char p[] = "hello world";
	return p;
}
 
void Test(void)
{
	char *str = NULL;
	str = GetMemory();
	printf(str);
}

输出乱码(多线程下,原内存被覆盖)
程序崩溃(访问非法内存地址)
偶尔正确输出(内存未被立即覆盖,依赖编译器实现)

(3)内存泄漏了啊
void GetMemory(char **p, int num)
{
*p = (char *)malloc(num);
}
void Test(void)
{
char *str = NULL;
GetMemory(&str, 100);
strcpy(str, "hello");
printf(str);
}
在堆区(Heap)中malloc()会动态分配内存,用完要free,否则造成内存泄漏。
很经典的运行中,在栈中调用堆去分配内存,栈函数的生命周期结束了,并不会对堆分配的内存进行管理,所以需要程序员手动去释放内存。

(4)出现野指针

点击查看代码
void Test(void)
{
	char *str = (char *)malloc(100);
	strcpy(str, "hello");
	free(str);
	if (str != NULL)
	{
		strcpy(str, "world");
		printf(str);
	}
}
free(str)后,没有将str指向NULL类型,将出现野指针

关键字static的作用
作用域和访问权限
● 静态局部变量:在函数中声明,生命周期延续至程序结束,作用域为函数内。
● 静态全局变量:在文件作用域声明,仅在声明的文件内可见。
● 静态成员变量:在类中声明,所有对象共享,属于类本身。
● 静态成员函数:在类中声明,不依赖于对象,可以在不创建对象的情况下调用。
● 默认初始化为0.
静态函数没有this指针的原因与内存分区无关,主要与其设计特性和作用域有关。以下是详细解释:
静态函数的设计特性:
静态成员函数属于类而非对象实例,没有隐含的this指针参数
普通成员函数编译时会自动添加this指针作为第一个参数(如void func(MyClass* this))
静态函数无法访问非静态成员变量/函数,因为缺少this指针的上下文
内存分区的误解:
函数代码(无论静态与否)都存储在.text段(代码段)
.bss段用于未初始化的静态/全局变量,.data段用于已初始化的静态/全局变量
静态成员变量确实存储在.bss/data段,但静态函数的代码仍和其他函数一样在.text段

访问机制的真相:
所有函数的地址在编译链接时就已经确定,与是否静态无关
运行时通过函数指针直接访问,不需要动态查找地址
虚函数通过虚函数表(vtable)实现动态绑定,但静态函数不涉及虚函数机制

静态单例懒汉模式 和 饿汉模式的区别在于单例对象创建的时机,饿汉是静态初始化就有,懒汉就是使用时候才创建,在使用懒汉时候,得加分布式锁访问,才能是线程安全,符合并发使用。

构造函数有:默认构造函数、初始化构造函数、析构函数、拷贝构造函数、移动构造函数(move和右值语义)、转换构造函数(隐式转换)
默认构造函数是使用参数初始化列表进行初始化操作或直接初始化操作
拷贝构造函数是通过生成临时对象进行拷贝初始化
所以可以在构造函数中使用explict关键字修饰,可以避免隐式转换。

一般网络通信传输就是使用progrm pack(1) 好处就是保证通信对等方在同一通信规则下进行消息传递,协议的定义就包括这种规则的定义,协议是服务的载体,涉及好协议,就能更好的实现服务,实现逻辑

纯虚函数和虚函数的作用和区别:
● 都是为了实现多态性,表现上的区别是,纯虚函数需要加上=0
● 虚函数允许在基类中定义默认行为,并可以在派生类中重写,从而实现运行时的多态性。
● 纯虚函数用于定义接口或抽象基类,强制派生类实现这些函数,适合用来设计框架和接口。
虚函数是声明时不分配内存,定义时分配内存,分配在二进制代码区。是通过一个ptr指针指向内核虚函数表进行重载的。
final 和 override关键字
重载,隐藏(重定义),覆盖(重写)
String 中的final char* str[]

内存泄漏、野指针、coredump、指针悬挂

STL

STL是算法、迭代器、容器
迭代器++i 与 i++区别
先加后 返回i 则 int& operator++() return *it
先取i再加 则 int opertor++(int) int temp = *this;++temp return *this
传参了就会有拷贝
STL hashtable 的实现
hashtable节点,对于冲突key使用链表,也就是拉链法去解决哈希冲突
STL vector 的resize() 与 reserve() 有了解嘛?扩容机制呢?
vector 底层实现是可以无限扩容的机制,顺序容器有vector 还有deque ;reserve api就是问内核拿100字节的数据,程序员分配50个int数据后,再次使用push_back 尾插元素,vector会采用自动扩容机制,一般是以 1.5-2倍扩容。这个时候就会涉及到一个频繁刺激扩容机制,所以在effective c++ 中提到,最好是提前reserve 预留200字节的内存,然后再使用resize分配出50个int数据的空间,这样既能避免vector扩容机制的频繁触发导致的上下文切换频繁造成额外消耗,同时也提前预防了后续的扩容考虑,哪怕是到达了200字节都用完了,再去触发扩容机制也会很好。
STL的内存缩小机制使用shrink,当然vector也可以使用swap
迭代器的实现了解嘛?
迭代器的移动语义有了解嘛?

STL并非“重复实现”内存管理,而是在操作系统提供的基础设施之上,构建了专用于高效数据结构的轻量级抽象层。这种分层设计既利用了操作系统的底层能力,又通过针对性优化满足了应用程序的性能需求,体现了“抽象隔离关注点”的软件工程原则。

内存对齐
c++11涉及2个api

进程与多线程、并发编程

程序由多个进程组成,文件是对IO设置的抽象表示,虚拟内存是对主存和磁盘设备的抽象表示,进程则是对处理器、主存、IO设备的抽象表示。进行也可以说是对于正在运行中的系统资源的抽象的表示。

僵尸进程、孤儿进程

孤儿进程和僵尸进程都是由于子进程的资源未正确被父进程调用wait或waitpid进行获取子进程退出的状态,导致子进程残留资源(PCB)存放于内核中。
区别在于僵尸进程是还没来得及获取,子进程就先于父进程退出了;而孤儿进程是父进程未获取到子进程的退出状态。

僵尸进程:子进程先于父进程退出,但是父进程还未苏醒,不能够及时调用wait或waitpid去回收子进程的资源
pstree -psn 查看进程图谱
ps -ef | grep defunct
ps -A -ostat,pid,ppid | grep -e '[zZ]'
逐个kill -9 pid 就能够清理zombie进程 SIGTERM信号

zombie process
#include<stdio.h>
int main()
{
	pid_t pid = fork();
	if(pid == 0){
		sleep(3);
	}else if(pid > 0){
		sleep(5);
	}else{
		perror("fork error")
		return -1;
	}
	return 0;
}

孤儿进程:父进程未调用wait或者waitpid去等待子进程资源回收,此时子进程就变成了无父进程的孤儿,最后由init进程去管理孤儿进程的资源回收。
守护进程:
守护进程的创建

进程内存空间查看

pmap -p pid
cat /proc/pid/maps
image

CAS

cas (compare and swap)可以使得多线程环境下进行无锁访问临界资源,与此同时高竞争下,也会使得线程自旋,增加cpu开销。
悲观锁和乐观锁就是这样的
数据库中的行锁,表锁,读锁(共享锁),写锁(排他锁),以及syncronized实现的锁均为悲观锁
悲观并发控制实际上是“先取锁再访问”的保守策略,为数据处理的安全提供了保证,
乐观锁(Optimistic Lock): 就是很乐观,每次去拿数据的时候都认为别人不会修改。所以不会上锁,但是如果想要更新数据,则会在更新前检查在读取至更新这段时间别人有没有修改过这个数据。

typedef struct{
    int value;
    Node* next;
}Node,*nodePtr;
class ConcurrentLinkList 
{
    ConcurrentLinkList(){list_head = nullptr;}
    ~ConcurrentLinkList(){if(list_head) list_head = nullptr;}
    static void append(int val)
    {
        nodePtr old_head = list_head.load();
        nodePtr new_node = new Node{val,old_head};

        while(list_head.compare_exchange_weak(old_head,new_node))
        {
            new_node->next = old_head;
        }
    }
    static void clean()
    {
        nodePtr it;
        while(it != list_head)
        {
            list_head = it->next;
            delete it;
        }
    }

public:
    std::atomic<Node *> list_head;
};

template<class T>
class optimsm_lock
{
    //加锁
    read();
    //放锁
};
class positive_lock
{
    positive_lock();
    save版本号();
    if(read() != 期望数据)

}

锁类别:

智能指针的底层实现:
指针变量在做拷贝临时对象时候,值是不会变的,也就是指向的那块地址是不会变的,加上std::atomic _refCnt;(c++20),智能指针shared_ptr 就是这样实现的底层。

锁同步虽好,但是不能滥用,很容易引起锁竞争,导致死锁,一般都是在读写场景(临界资源的)使用加锁获取。
比如说我们在使用单例锁时候,我当时考虑说直接复用线程的锁,这样就很容易导致锁卡在这边,那边占用资源率高。不行的,都应该自己实现并管理自己的锁。如果实在想要复用的话就使用类去实现,去包装锁,然后再利用RAII的思想。

单核不允许使用同步机制,线程是否会发送数据读取错误,会的,因为内存中的数据同步不是立刻同步的,所以线程读取内存的数据也不是及时更新的,不是最新的。

内核强行干预用户空间中的指令,属于被动干预,所以任何运行在用户空间的指令都是可能被随时软硬中断的,内核会随时保存用户空间执行指令的上下文环境,待干预后返回。
信号机制是内核提供的一种完整保存上下文的强行干预机制,当然是异步的,随时可以被中断,用户态接收到了信号-》内核接管操作,将PC设置为信号处理函数的地址-》返回用户态sigreturn
用户空间的低级语言层的汇编层的jump指令使得PC(无条件跳转)、call指令(弹出返回地址ret,继续call下一条执行)
用户空间的高级语言层的提供的c语言库函数longjump和setjump,setjump设置跳转点,longjump跳转过去setjump,这些都是用户层面的上下文环境保存(堆栈指针和寄存器),需要手动去实现堆栈的资源管理。这种的应用主要是try catch异常捕捉啊,协程实现啊等等。

进程管理,线程与协程 其实就是一个c结构

进程与线程的区别?

进程是资源分配的基本单位,进程控制块的资源包括:进程id号,组id号,文件描述符表,物理地址的切换(涉及到快表的刷新),堆栈空间,寄存器空间,也有自己的代码区执行地址等等。
线程是执行的基本单位,他除了共享堆空间,全局数据和非全局数据,常量这些外,他拥有自己独立的线程栈空间

追求安全,隔离就用多进程
追求高性能,共享内存就多线程

结论一:父子进程 间 遵循 读时共享写实复制 机制,提升效率,互不影响
结论二:一旦有父进程或者子进程改动了值,那么父子进程就会各自维护一份,全局变量a=1,父进程改了a=2,那么子进程还是读取的a=1。
结论三:父子进程共享的的部分,最重要的是 文件描述符,mmap建立的映射区
结论四:fork之后,父进程先执行还是子进程先执行不确定。

父子进程fork时候,父子进程遵循读时共享(mmap系统调用,构造内存映射),写时复制
如果父进程修改了全局变量A =100为200,因为父子进程各自维护的是自己那份数据A,

进程空间是相互独立的,同个进程内的线程是共享内存空间的。
进程内存空间是采取读时共享,写时复制原则:
读时共享:使用mmap系统调用函数去分配一大块内存,映射到堆栈中间的内存区,父子进程读都是指向这片内存。
写时复制:fork并不会拷贝父进程的数据副本,只有当读的那刻才会拷贝父进程的数据到子进程副本,所以称之为进程空间独立,互不影响。
接下来想要同步进程间数据,就是进程间通信方式了。使用的是系统原语:条件变量,信号量等。

孤儿进程和僵尸进程
孤儿进程是父进程还没有来的及等到调用wait 或者 waitpid回收子进程资源,就先于子进程回收资源了,这样子进程没了父进程,就变孤儿了,由init进程代为管理资源的回收。
僵尸进程是指子进程的退出和资源的回收没有告知到父进程,也就是父进程根本没有调用wait或者waitpid去回收子进程的资源,这样子进程其实已经退出了,但是资源未被回收,过多的僵尸进程会造成资源的浪费;
野指针是指声明了指针,但是未初始化,,会造成段错误的报错。
内存泄漏是指丢失了内存的控制器,没有调用delete free等内存资源回收的系统调用。

进程的是资源分配的基础单位,线程是cpu调度的最小单位。
我们会把c语言中的 setjmp/longjmp 与 goto 语句进行功能上的比较;与 fork 函数从返回值上进行类比;与 Python/Lua 语言中的协程进行使用场景上的比较。
c语言底层库函数
协程在系统内核是没有这个概念的,它是由c语言实现的,所以对于go语言、lua语言、python语言。但是在汇编指令层是存在jump指令和call指令的,前者是无条件跳转指令,并立刻去执行新的任务,后者是保存当前上下文环境。
信号

go语言很重要的一个概念就是协程,因为go中的协程相较于线程上下文切换更小的消耗,很适合并发高的环境下使用。c语言上下文切换可能要执行系统调用,然后中断处理,再返回。
在c语言中,也有2个指令,longjump 和 setjump指令,可以保存当前上下文环境变量值,和fork类比有返回值,并立刻去执行另外一个任务。(改变了代码的执行顺序)
lamada表达式其实就是一个 匿名函数 ,共享外部变量,代码的执行顺序并没有被改变。

软硬件中断(c结构 和 屏蔽字)

硬中断和软中断

posted on 2025-04-16 10:03  花生米与花生酱  阅读(31)  评论(0)    收藏  举报