1、fork和exec是怎么实现的?
fork 和 exec 是 Linux 中实现进程创建和执行新程序的两种系统调用。fork 会创建一个新的进程,这个进程是当前进程(父进程)的副本。新进程(子进程)会继承父进程的资源(如文件描述符、内存空间等)。调用 fork 后,父进程和子进程会并行执行,且它们会继续执行 fork 调用后的代码。
fork 会返回两次:
在父进程中,返回子进程的 PID。
在子进程中,返回 0。
pid_t pid = fork();
if (pid < 0) {
// fork失败的情况
fprintf(stderr, "Fork failed");
return 1;
} else if (pid == 0) {
// 子进程
printf("I am the child process with PID: %d\n", getpid());
} else {
// 父进程
printf("I am the parent process with PID: %d, and my child has PID: %d\n", getpid(), pid);
}
实现原理:操作系统内核通过分配一个新的进程 ID 给子进程,并复制父进程的内存空间(可以通过写时复制(Copy-on-Write)技术来优化)。
exec 会用指定的程序替换当前进程的映像,即加载并执行一个新的可执行文件。调用 exec 后,当前进程的地址空间、文件描述符等会被新程序的地址空间覆盖,原先的代码被替换,当前进程会开始执行新的程序。
实现原理:内核通过加载新程序的映像文件,并将其设置为进程的运行环境。exec 可能会在父进程调用 fork 后在子进程中执行,用于启动新的程序。
2、select、poll、epoll的区别?LT和ET的区别?
select 是一种同步 I/O 多路复用技术,可以监视多个文件描述符(socket、管道等),当这些文件描述符有可读、可写、异常等事件发生时,select 会返回,允许程序进行相应的处理。
缺点:支持的文件描述符数量有限(通常为 1024),每次需要对文件描述符数组重新赋值,每次都需要从用户态拷贝到内核态,需要遍历所有的文件描述符
poll 是 select 的增强版,解决了 select 文件描述符数量限制的问题。poll 使用链表来存储文件描述符,可以监视更多的文件描述符。
缺点:每次要从用户态拷贝到内核态,需要遍历所有的文件描述符,性能也没有得到根本改善。
epoll 是 Linux 特有的高效 I/O 多路复用机制,支持大规模并发连接。它通过事件驱动和回调机制,使得监控文件描述符变得更加高效。epoll 使用内核事件通知来减少了文件描述符集合的传递开销,支持 LT(水平触发)和 ET(边缘触发)两种触发模式。
epoll 适用于处理大量并发连接,尤其在高并发的服务器中非常常见。
LT (Level Triggered) 与 ET (Edge Triggered):
LT(水平触发): 当文件描述符的状态满足条件时(如可读、可写),会一直通知应用程序,直到应用程序处理完相关事件。适用于所有应用场景。
ET(边缘触发): 只有在状态变化时才会通知应用程序一次,适用于高效的、快速处理的应用。使用 ET 时需要注意确保每次 epoll_wait 都能够处理所有的数据。
3、何判断一个任务处理完成?
返回值:通过任务的返回值来判断,例如函数执行结束时返回特定值表示任务完成。
事件通知:通过事件、信号或回调函数来通知主程序任务完成。
状态标志:使用全局标志或状态变量来表示任务是否完成,线程或进程可以通过检查该标志来判断任务是否完成。
条件变量:在多线程程序中,常使用条件变量来阻塞线程直到任务完成。
4、进程和线程的区别?
进程是程序执行的基本单位,每个进程都有独立的内存空间、代码、数据和系统资源。
进程之间相互独立,一个进程崩溃不会直接影响其他进程。
进程之间通信比较复杂,通常通过管道、共享内存、消息队列等机制进行通信。
线程是进程内的执行单元,一个进程可以包含多个线程,它们共享进程的资源(如内存空间、文件描述符等)。
线程之间的通信更简单,直接通过共享内存即可通信。
线程相比进程更轻量级,创建和销毁的开销较小,但线程共享内存带来并发控制和线程安全的问题。
5、线程独有的资源是什么?
线程 ID:每个线程都有唯一的 ID。
寄存器状态:每个线程有独立的寄存器集。
栈空间:每个线程有独立的栈,用于保存局部变量和函数调用信息。
TLS(线程局部存储):用于在线程之间存储线程私有的数据。
6、读写锁什么时候上锁?
读写锁通常有两个操作:
读锁(共享锁):当线程获得读锁时,其他线程仍然可以获取读锁(并发读取),但是无法获得写锁。
写锁(独占锁):当线程获得写锁时,其他线程不能获得读锁或写锁(写锁是独占的)。
读写锁用于允许多个线程并发读取,而写操作需要独占访问。上锁时,线程请求读锁或写锁,系统根据当前锁的状态决定是否允许锁定。
7、什么是死锁?只有两个线程时候,会发生死锁吗?
死锁是指两个或多个线程在执行过程中因争夺资源而造成的一种相互等待的情况,导致这些线程无法继续执行。
死锁发生的四个必要条件是:
互斥:至少有一个资源是被排他性占用的。
占有且等待:线程持有至少一个资源,并等待其他资源。
非抢占:资源不能被其他线程强制抢占,只能由持有线程主动释放。
循环等待:存在一种线程资源的循环等待关系。
在只有两个线程的情况下,也可以发生死锁。例如,线程 A 拥有资源 R1 并等待资源 R2,而线程 B 拥有资源 R2 并等待资源 R1,这样就会形成一个死锁。
8、栈和堆的区别?
栈:
用于存储局部变量、函数调用的参数和返回地址等。
内存分配和回收由编译器自动管理,速度较快。
空间有限,通常大小受限(例如,4MB 或 8MB)。
栈内存由函数调用和返回自动管理,遵循先进后出的原则。
堆:
用于动态分配内存,需要程序员手动管理(如使用 new 或 malloc 分配内存,delete 或 free 回收内存)。
空间较大,通常由操作系统管理,大小只有操作系统限制。
内存分配和回收较慢,但可以动态分配和释放内存。
9、讲一下智能指针,各自的区别?
智能指针是 C++ 提供的一种管理动态内存的机制,能够自动管理内存的释放,避免内存泄漏。
std::unique_ptr:
独占式所有权,一个 unique_ptr 只能拥有一个对象的所有权。通过移动语义,可以将所有权转移给另一个 unique_ptr。
std::shared_ptr:
共享式所有权,多个 shared_ptr 可以共享同一个对象的所有权,引用计数决定对象何时被销毁。
std::weak_ptr:
弱引用,不控制对象生命周期,但可以观察对象是否已经被销毁。它常用于打破 shared_ptr 之间的循环引用。
10、弱引用计数的来源?
std::weak_ptr 和 std::shared_ptr 都依赖于一个共同的控制块(control block),这个控制块管理着对象的省命周期。控制块中包含两个计数器:
强引用计数:
这个计数器记录了有多少个 std::shared_ptr 持有该对象。
当强引用计数降为 0 时,表示没有任何 std::shared_ptr 持有该对象,此时对象会被销毁。
弱引用计数:
这个计数器记录了有多少个 std::weak_ptr 持有该对象。
弱引用计数还包括那些通过 std::shared_ptr 创建的 std::weak_ptr。
弱引用计数也降为 0 时,表示没有任何 std::weak_ptr 或 std::shared_ptr 持有该对象,此时控制块本身也会被销毁。
weak_ptr的弱引用计数来源于其他weak_ptr对象。当创建一个weak_ptr或者拷贝一个weak_ptr时,会增加弱引用计数。
制块的工作原理
创建 std::shared_ptr:
当创建一个新的 std::shared_ptr 时,会创建一个控制块,并将强引用计数初始化为 1。
弱引用计数初始化为 1(因为创建 std::shared_ptr 时,控制块本身就是一个弱引用)。
创建 std::weak_ptr:
当从一个 std::shared_ptr 创建 std::weak_ptr 时,std::weak_ptr 会增加控制块的弱引用计数。
std::weak_ptr 不会增加强引用计数,因此不会影响对象的生命周期。
销毁 std::shared_ptr:
当一个 std::shared_ptr 被销毁时,会减少控制块的强引用计数。
如果强引用计数降为 0,对象会被销毁,但控制块仍然存在,直到弱引用计数也降为 0。
销毁 std::weak_ptr:
当一个 std::weak_ptr 被销毁时,会减少控制块的弱引用计数。
如果弱引用计数也降为 0,控制块本身会被销毁。
使用 std::weak_ptr::lock():
当调用 std::weak_ptr::lock() 时,如果对象仍然存在(即强引用计数大于 0),则返回一个 std::shared_ptr,并增加强引用计数。
如果对象已经被销毁,则返回一个空的 std::shared_ptr。
11、了解make_shared吗?weak_ptr会导致线程不安全吗?
make_shared:
make_shared 是一种构造 shared_ptr 的方法,它通过一个单一的内存分配来同时分配 shared_ptr 和其管理的对象。这种方法比直接使用 new 和 shared_ptr 构造函数更加高效,因为它减少了内存分配的次数。
weak_ptr 的线程安全性:
weak_ptr 本身是线程安全的,但如果多个线程同时操作与其相关联的 shared_ptr 对象时,必须确保对 shared_ptr 的操作是线程安全的。weak_ptr 本身不会导致线程不安全,但对其相关的 shared_ptr 进行访问时需要注意同步问题。
12、讲一些stl中的容器和各自特点?
vector:
动态数组,支持随机访问,插入和删除效率较低(尤其是在数组中间操作)。
deque:
双端队列,支持在两端进行快速插入和删除,访问效率较低(相比 vector,但插入/删除更灵活)。
list:
双向链表,支持在任意位置快速插入和删除,但不支持随机访问。
set / map:
基于红黑树实现,保证元素的有序性,查找、插入和删除操作复杂度为 O(log n)。
unordered_set / unordered_map:
基于哈希表实现,查找、插入和删除操作平均复杂度为 O(1),但不保证有序性。
stack / queue:
分别基于栈和队列的数据结构实现,提供 LIFO 和 FIFO 操作。
13、讲一下deque的底层实现?
deque底层通常采用分段数组实现,它由多个小数组组成,每个块有固定的大小。这样可以在两端高效地进行插入和删除,同时避免了单一动态数组在两端操作时可能出现的频繁内存重新分配。
14、红黑树的特点?哈希表的特点?如何解决哈希冲突?
红黑树特点:
每个节点要么红要么黑
根节点是黑色
叶子节点(NIL)是黑色
红节点的子节点必须是黑色
从任一节点到其每个叶子节点的所有路径都包含相同数目的黑色节点
时间复杂度:O(logn)
哈希表特点:
直接寻址,查询效率高
插入删除快速
空间换时间
平均时间复杂度:O(1)
最坏时间复杂度:O(n)
解决哈希冲突的方法:
开放定址法:线性探测、二次探测、双重散列
链地址法:冲突的元素用链表存储
再哈希法:使用另一个哈希函数重新计算
15、讲一下右值引用?
右值引用(T&&)是 C++11 引入的,用于支持移动语义和完美转发。它允许把临时对象(右值)传递给函数,而不进行不必要的拷贝,从而提高效率。
右值是临时对象,例如:int &&x = 5; 中的 5 就是一个右值。
左值是有持久地址的对象,可以引用它,例如:int x = 5; 中的 x 是左值。
右值引用可以用来实现移动构造和移动赋值操作,从而避免不必要的对象拷贝。
16、什么时候使用移动语义?
当对象的拷贝代价较高时,使用移动语义可以避免不必要的复制操作,提高效率。尤其是对于包含动态分配内存、大量数据的类(如 std::vector 和 std::string)。
比如,在返回局部对象时,使用右值引用和 std::move
可以将局部对象的资源转移给返回值,而不是进行复制。
17、讲一下多态?怎么调用虚函数的?
多态是面向对象编程中的一个核心概念,允许通过基类指针或引用调用派生类的方法。多态分为:
编译时多态(静态多态):通过函数重载和模板实现。
运行时多态(动态多态):通过虚函数实现。
虚函数是在基类中声明并在派生类中重写的函数。通过基类指针或引用调用虚函数时,会动态绑定到实际的派生类函数。