探索C++底层机制-第一天

C++ 引用的本质是什么?

①C++中的引用本质上是 一种被限制的指针(类似于线性表和栈,栈是被限制的线性表,底层实现相同,只不过逻辑上的用法不同而已)。

②由于引用是被限制的指针,所以引用是占据内存的。

③在使用高级语言的层面上,是没有提供访问引用的方法的。并且引用创建时必需初始化,创建后还不能修改。

gdb 的原理

gdb 主要通过系统调用 ptrace 实现. ptrace 可以将 gdb attach 到指定线程, 这有两种方式:

在 gdb 中运行一个进程, 再对其进行调试. 首先利用 fork 创建该进程, 再在子进程中调用 ptrace,

并将第一个参数设为 PTRACE_TRACEME, 表示此进程将被父进程跟踪, 然后执行 exec 函数.

将 gdb attach 到一个已存在的进程. 指定进程的 pid, gdb 调用 ptrace, 将第一个参数设为 PTRACE_ATTACH,

将 pid 作为函数参数. 这样, gdb 就成为该进程的父进程, 并跟踪该进程.

通过这两种方式, gdb 就和被调试进程建立了联系, 即成为其父进程, 该进程被父进程跟踪.

此时任何传递给该进程的信号(除了SIGKILL)都将通过 wait 方法阻塞该进程, 并将信号转交给父进程.

并且, 该进程如果调用 exec 函数, 都会接收到一个 SIGTRAP 信号, 使得父进程(gdb)可以在被跟踪进程执行第一条指令前就可以做一些需要的工作.

这也是第一种实现方式的原理.

断点*是通过内核信号实现的. 增加断点时, 实际是将指定位置写入指令 INT 3. 运行到此指令时,

会触发 SIGTRAP 信号, 从而被跟踪进程会被暂停, gdb可以捕获到此信号. gdb将断点组织为一个链表,

此时就可以查询此链表, 检查是否有匹配的断点记录, 当存在时就发生断点命中, 就允许用户做一些调试操作.

否则继续执行命令.


编译器会为const引用创建临时变量

将常引用绑定到临时数据时,


const int &A;

==编译器会为临时数据创建一个新的、无名的临时变量,并将临时数据放入临时变量中,然后再将引用绑定到临时变量。==临时变量也是变量,所有的变量都会被分配内存。


常引用和普通引用不一样。


因为临时数据无法寻址,不能写入。而引用是绑定到一份数据时,就可以通过引用对数据进行读取和修改。即使为临时数据创建一个临时变量的时候,修改的也是临时变量的数据,而不是源数据。这样引用所绑定的数据和源数据不能同步更新,失去了操作数据的作用。


以代码为例


void swap(int &r1, int &r2){

int temp = r1;

r1 = r2;

r2 = temp;

}

如果编译器为r1和r2创建了临时变量,那么r1和r2的值怎样都不会发生交换。


常引用只能通过const引用读取数据的值,而不能修改数据的值。所以不用考虑数据更新的问题,也不会产生两份数据。


以代码为例


//该函数用来判断数字是否为奇数

bool isOdd(const int &n){ //改为常引用

if(n/2 == 0){

return false;

}else{

return true;

}

}


int main(){

int a = 100;//

isOdd(a); //正确

isOdd(a + 9); //正确

isOdd(27); //正确

isOdd(23 + 55); //正确

}

对于第12行的代码,编译器不会创建临时变量,引用n会直接绑定a,而对于13~15行代码,编译器会创建临时变量来存储临时数据。即编译器只有在必要的时候才会创建临时变量。


IO复用的实现原理

IO设备的驱动程序中有一个等待队列, 可以通过驱动程序提供的 poll 接口来获取IO设备是否就绪, 也可以将进程加入到等待对应的等待队列中.
当IO设备就绪时, 就可以通知等待队列中的进程, 将其从睡眠中唤醒.
select, poll 的实现是:

扫描所有 fd, 利用 poll 接口检测对应的IO是否就绪, 并将进程加入到等待队列.

如果存在就绪的 fd, 就在遍历后返回就绪 fd 的数量.

如果没有就绪的 fd, 就睡眠一段时间.

如果休眠结束或被IO驱动程序唤醒, 继续循环上面的过程. 如果是后者, 就会检测到就绪的 fd, 从而可以在第二步返回.

epoll 流程也类似上面, 只有下面几点不同:

epoll 在创建句柄时将 fd 集合拷贝到内核, epoll_wait 时不需要再拷贝 fd 集合, 这样对同一集合多次调用 epoll_wait 时就只需要拷贝一次.

epoll 的句柄通过一个红黑树来维护 fd 集合, 每次加入时会先在红黑树中查找是否已经保存了该 fd,


时间复杂度是 log(N)log(N).

epoll 第一次也要遍历 fd, 并将进程加入到等待队列, 但是同时为每个 fd 指定了回调函数, 当 fd 就绪时,
设备驱动程序会唤醒进程, 并调用此回调函数. 这个回调函数的作用是将这个就绪的 fd 加入到就绪链表.
因此, 当进程从等待中被唤醒时, 就可以直接通过检查这个就绪链表是否为空来判断是否有 fd 就绪,
而不需要像 select/poll 一样再次遍历 fd 集合.

调用 epoll_wait 时, 会把就绪的 fd 拷贝到用户态内存, 然后清空就绪链表, 最后再检查这些 fd.
在水平触发模式下, 如果检查到这些 fd 上还有未处理的事件, 会将这些 fd 放回就绪链表中, 保证事件得到正确处理.

对于 fd 集合大小的限制, epoll 是进程可以打开的最大文件数目, 这个值保存在 /proc/sys/fs/file-max.


malloc的原理,另外brk系统调用和mmap系统调用的作用分别是什么?

参考回答:
Malloc函数用于动态分配内存。为了减少内存碎片和系统调用的开销,malloc其采用内存池的方式,先申请大块内存作为堆区,然后将堆区分为多个内存块,以块作为内存管理的基本单位。当用户申请内存时,直接从堆区分配一块合适的空闲块。Malloc采用隐式链表结构将堆区分成连续的、大小不一的块,包含已分配块和未分配块;同时malloc采用显示链表结构来管理所有的空闲块,即使用一个双向链表将空闲块连接起来,每一个空闲块记录了一个连续的、未分配的地址。


当进行内存分配时,Malloc会通过隐式链表遍历所有的空闲块,选择满足要求的块进行分配;当进行内存合并时,malloc采用边界标记法,根据每个块的前后块是否已经分配来决定是否进行块合并。

Malloc在申请内存时,一般会通过brk或者mmap系统调用进行申请。其中当申请内存小于128K时,会使用系统函数brk在堆区中分配;而当申请内存大于128K时,会使用系统函数mmap在映射区分配。

请你说一说C++的内存管理是怎样的?

参考回答:
在C++中,虚拟内存分为代码段、数据段、BSS段、堆区、文件映射区以及栈区六部分。
代码段:包括只读存储区和文本区,其中只读存储区存储字符串常量,文本区存储程序的机器代码。


分享到:


相關文章: