本篇为收集的技术面的面经以及自己经历的补充
共分为两部分
- 第一部分,网络上汇总的面经.(已标明出处)
- 第二部分,鄙人亲自经历的面经汇总.(持续更新)
1.1 进程线程的基本概念
进程是操作系统进行资源分配和调度的基本单位.一个进程包括:
线程是进程中的一个执行单元,是CPU调度和分派的基本单位.它属于进程且共享进程的地址空间.
一个进程可以包含多个线程,线程间可以并发执行.线程有以下主要特点:
1.1.1 什么是进程,线程,彼此有什么区别⭐⭐⭐⭐⭐
进程和线程都是操作系统进行运算调度的单位,但二者有以下几个主要区别:
进程:
关于线程共享进程的地址空间的解释
线程属于进程,一个进程可以包含多个线程.而进程有自己独立的地址空间,所以线程作为进程的组成部分,自然也共享这个地址空间.
这意味着:
相比之下,进程有自己独立的地址空间,所以:
1.1.2多进程、多线程的优缺点⭐⭐⭐⭐
多进程和多线程都可以实现并发执行,但各有优缺点:
多进程:
优点:
优点:
1.1.3什么时候用进程,什么时候用线程⭐⭐⭐
选择多进程还是多线程,需要根据实际应用中的需求和资源进行权衡:
如果需要系统级并发和高安全性,选择多进程;
如果需要大量的轻量级并发和高效通信,选择多线程;
也可以两者结合使用,获得各自的优势.
1.1.4多进程、多线程同步(通讯)的方法⭐⭐⭐⭐⭐
多进程通讯:
IPC是Inter-Process Communication的缩写,意为进程间通信.它是指两个或多个进程之间进行数据交换和资源共享的机制.
IPC常见的方式有:
多线程同步:
1.1.5进程的空间模型⭐⭐⭐⭐
进程的地址空间通常采用以下几种模型:
1.1.6进程线程的状态转换图 什么时候阻塞,什么时候就绪⭐⭐⭐
进程/线程会在阻塞和就绪状态之间转换:
就绪(Ready):正在等待获得CPU使用权,随时可执行.
阻塞(Blocked):无法执行,需要等待某事件发生后才能转为就绪状态.
进程/线程会在以下情况下转入阻塞状态:
进程/线程会在以下情况下转为就绪状态:
1.1.7父进程、子进程的关系以及区别⭐⭐⭐⭐
父进程和子进程是相对的概念:
父进程:正在执行的进程,调用了fork()系统调用产生子进程.
子进程:由父进程调用fork()产生的新进程.
当一个进程调用fork()时,操作系统会创建一个子进程,这个子进程拷贝父进程的地址空间,并在子进程中继续执行fork()的下一条指令.
所以,父进程和子进程有以下关系:
1.1.8什么是进程上下文、中断上下文⭐⭐
进程上下文:
当 CPU 调度进程执行时,会为其建立一个执行环境,包括程序计数器、栈指针、通用寄存器等,这些统称为进程上下文.
当进程被抢占时,操作系统会将其上下文保存,以便其下次调度时恢复执行.进程上下文切换涉及保存和恢复现场,其开销较大.
中断上下文:
当 CPU接收到中断时,当前执行的进程会被暂停,转入中断服务例程执行.
中断服务例程有自己的上下文,包括堆栈指针、程序计数器等.在中断处理完后,CPU 会恢复到中断发生前的进程,继续执行.
与进程上下文切换相比,中断上下文切换开销较小,只需要简单保存和恢复少数寄存器即可.
所以,进程上下文和中断上下文之间的主要区别在于:
1.1.9一个进程可以创建多少线程,和什么有关⭐⭐
1.2 并发,同步,异步,互斥,阻塞,非阻塞的理解
并发:同一时间间隔内,多个任务都在执行(执行的交替进行).并发程序设计的目的是利用多核 CPU 或多线程提高程序的执行效率.
同步:一个任务在执行某个操作时,需要等待其他任务的信息或信号才能继续执行.同步处理通常使用互斥锁,条件变量等机制实现.
异步:一个任务在执行某个操作时,不需要等待其他任务的信息或信号就继续执行.异步通常使用回调函数,事件等机制实现.
互斥:多个任务同一时间只能有一个任务进入临界区,其他任务必须等待.互斥的实现使用的是互斥锁(Mutex).
阻塞:一个任务在等待某个事件(如 I/O 操作或互斥锁)发生时,被暂停执行.阻塞通常用于同步处理,以协调多个任务的执行.
非阻塞:一个任务在等待某个事件(如 I/O 操作)时,不暂停执行.非阻塞通常用于异步处理,提高程序的响应性.
所以,这些概念之间的关系是:
并发提供了同时执行多个任务的环境;
同步和异步是任务之间的一种协作方式;
互斥是一种实现同步的机制;
阻塞和非阻塞是任务在等待事件时的两种不同状态.
1.2.1什么是线程同步和互斥⭐⭐⭐⭐⭐
线程同步:
是指多个线程协调其执行顺序和相互通信的机制.因为线程运行并行,所以需要同步来避免对共享资源的非法访问和不一致的执行结果.
常用的线程同步方法有:
1.2.2线程同步与阻塞的关系?同步一定阻塞吗?阻塞一定同步吗?⭐⭐⭐⭐
线程同步与阻塞是两个不同的概念,但存在一定的关系:
线程同步:是指多个线程协调其执行顺序和相互通信的机制,用于避免对共享资源的非法访问和不一致的执行结果.常用方法有互斥锁、条件变量、读写锁、信号量等.
线程阻塞:是指线程在等待某个事件(如 I/O 操作或互斥锁)发生时,被操作系统挂起执行的状态.阻塞状态的线程不会被 CPU 调度执行,直到其等待的事件发生,其状态才会变为可执行状态.
那么,线程同步与阻塞的关系是:
1.3 孤儿进程、僵尸进程、守护进程的概念
孤儿进程:其父进程终止,但该进程还在运行的进程.孤儿进程会被 init 进程采集,并由 init 进程成为其父进程.
僵尸进程:其子进程已经终止,但父进程还没有调用 wait() 系统调用来获取子进程的终止信息,所以子进程的进程描述符仍然保留的进程.僵尸进程只占用进程描述符,不占用任何系统资源.
守护进程:是运行在后台并且一直运行的进程.守护进程不受控制终端的影响,在系统引导时启动,并一直运行等待处理事件.常见的守护进程有系统日志进程 syslogd、web 服务器进程 httpd 等.
所以,这几种进程的主要特征是:
孤儿进程:失去父进程,被 init 进程采集.
僵尸进程:子进程终止但父进程未回收,占用进程描述符.
守护进程:在后台持续运行,不受控制终端影响.
其中,僵尸进程和守护进程都是由我们的程序所产生,需要在设计中妥善处理:
僵尸进程:父进程要及时调用 wait() 获取子进程信息,避免产生僵尸进程.
守护进程:设置进程组,与控制终端分离,忽略终端信号等.
1.3.1如何创建守护进程:⭐⭐
创建守护进程通常需要以下几个步骤:
#include
#include
#include
int main()
{
pid_t pid;
// 产生子进程
pid = fork();
if (pid < 0)
{
exit(1); // fork 失败,退出
}
else if (pid > 0)
{
exit(0); // 这是父进程,退出
}
// 设置进程组ID,与控制终端分离
setsid();
// 忽略SIGHUP信号
signal(SIGHUP, SIG_IGN);
// 设置工作目录
chdir("/");
// 关闭文件描述符
close(STDIN_FILENO);
close(STDOUT_FILENO);
close(STDERR_FILENO);
// 循环直到接收退出信号
while (1)
{
// daemon 进程执行内容
}
}
1.3.2正确处理僵尸进程的方法⭐⭐⭐⭐
僵尸进程是父进程创建子进程后,子进程终止但父进程还未调用wait()
系统调用获取子进程信息而产生的.僵尸进程只占用进程描述符,不占用任何系统资源.
要正确处理僵尸进程,主要有以下两种方法:
wait()
系统调用,获取子进程信息.pid_t pid;
int status;
pid = fork();
if (pid == 0) {
// 子进程代码
} else {
// 父进程代码
wait(&status); // 等待子进程退出,获取退出状态
}
2. 将子进程设置为守护进程,与父进程的生命周期分离.
这种方法不需要父进程调用wait()获取子进程信息,代码如下:
c
pid_t pid;
pid = fork();
if (pid == 0) {
// 子进程成为守护进程
setsid();
} else {
// 父进程代码
}
setsid()
系统调用将子进程分离成新的会话和进程组,与控制终端分离,实现了与父进程生命周期的分离.
所以,正确处理僵尸进程的两个方法是:
wait()
系统调用,获取子进程信息;2.1 c和c++区别、概念相关面试题
2.1.1 new和malloc的区别⭐⭐⭐⭐⭐
new 和 malloc 都是用来动态分配内存的操作,但二者有以下主要区别:
// 使用 new
Person *p = new Person;
delete p;
// 使用 malloc
void *mem = malloc(sizeof(Person));
Person *p = (Person*)mem;
free(mem);
理解 new 和 malloc 的区别,可以帮助我们在 C 和 C++ 程序中选择适合的方式动态分配内存.通常来说:
2.1.2 malloc的底层实现⭐⭐⭐⭐
malloc()函数是C语言中用于动态内存分配的标准库函数.它的主要作用是从堆区分配一块连续的内存空间,并返回该空间的首地址.
malloc()的主要底层实现步骤如下:
2.1.3在1G内存的计算机中能否malloc(1.2G)?为什么?⭐⭐
在1G内存的计算机中调用malloc(1.2G)无法成功分配1.2G的内存空间.这是因为malloc()函数分配的内存空间是在进程私有的堆区,堆区的大小是有限的,不可能超过系统的物理内存.
当调用malloc(1.2G)时,会发生以下情况:
2.1.4指针与引用的相同和区别;如何相互转换?⭐⭐⭐⭐⭐
2.1.5 C语言检索内存情况 内存分配的方式⭐⭐⭐
检索内存情况:
2.1.6 extern”C” 的作用⭐⭐⭐
extern “C” 的作用是告诉编译器,引用的外部符号遵循C语言的命名规则.
在C++中,extern “C” 通常用于:
// 调用C语言的printf函数
extern "C" {
int printf(const char* format, ...);
}
// 定义供C语言调用的函数
extern "C" void func() {
// ...
}
// 定义供C语言使用的全局变量
extern "C" int count;
所以,总结来说,extern “C” 的主要作用是:
使C++符号(主要是函数和全局变量)遵循C语言的命名和链接规则,以便C语言解析和调用;
调用C语言提供的库函数和全局符号.
2.1.7头文件声明时加extern,而在定义时不要加⭐⭐⭐⭐
因为extern可以多次声明,但只有一个定义
2.1.8函数参数压栈顺序,即关于__stdcall和__cdecl调用方式的理解⭐⭐⭐
在C/C++中,函数参数的压栈顺序分为__stdcall和__cdecl两种方式:
__stdcall:从右到左压栈.返回值由调用函数弹出.主要用于Windows API.
__cdecl:从右到左压栈.返回值由被调用函数弹出.C/C++的默认方式.
以计算两个整数和为例:
__stdcall:
int __stdcall add(int a, int b) {
return a + b;
}
int main() {
int sum = add(1, 2); // sum = 3
}
__cdecl:
int __cdecl add(int a, int b) {
return a + b;
}
int main() {
int sum = add(1, 2); // sum = 3
}
在汇编层面,函数调用会将参数压入栈中.以上两个函数的压栈顺序为:
__stdcall:
向下增长的栈:
b (2)
a (1)
__cdecl:
向下增长的栈:
b (2)
a (1)
可以看出,两种方式参数的压栈顺序相同,都是从右到左.区别在于:
__stdcall:返回值由调用函数弹出; 比如说main(调用函数)
__cdecl:返回值由被调用函数弹出; 比如说 add(被调用函数)
当弹出返回值时,被调用函数的栈帧可能被清空,也可能未被清空,这取决于所使用的函数调用约定。
主要有两种情况:
2.1.9重写memcpy()函数需要注意哪些问题⭐⭐
重写memcpy()函数需要注意以下几点:
void *my_memcpy(void *dst, const void *src, size_t n)
{
// 参数检查
if (!dst || !src || !n) {
return NULL;
}
// 获取指针
char *d = (char*)dst;
const char *s = (const char*)src;
// 检查重叠
if (dst > src && dst < src + n) {
s += n;
d += n;
}
// 拷贝
while (n--) {
*d++ = *s++;
}
return dst;
}
2.1.10数组到底存放在哪里⭐⭐⭐
数组存放在栈区或堆区,取决于数组的定义方式:
int arr[5]; // 数组大小必须是常量
int *arr = malloc(5 * sizeof(int));
2.1.11 struct和class的区别 ⭐⭐⭐⭐⭐
struct和class在C++中本质上都是类(class),都可以用作基类或派生类.
区别在于默认成员访问权限不同:
struct:默认public继承,用于定义数据结构;
class:默认private继承,更适用于定义抽象数据类型.
但通过显式指定访问权限,struct和class可以互相替换,没有本质区别.
比如结构体显示指定访问权限
struct A {
int a; // 默认为public
A(int x) :a(x) {
}
};
struct C : protected A { // OK,A可以是C的基类
int c; // 默认继承A的public权限
C(int x) :c(x), A(x)
{
}
};
2.1.12 char和int之间的转换;⭐⭐⭐
char c = 'A'; // c = 65
int i = c; // i = 65
int i = 65;
char c = (char)i; // c = 'A'
char str[10] = "Hello";
char* p = str; // p = str
int* q = p; // q = p,类型提升为int*
int arr[10] = {1, 2, 3};
int* p = arr;
char* q = (char*)p; // 显示类型转换为char*
2.1.13 static的用法(定义和用途)⭐⭐⭐⭐⭐
static关键字的主要用途有:
// file1.c
static int x = 0;
// file2.c
extern int x;
int main() {
x = 1; // x = 1
}
void func() {
static int x = 0;
x++; // 第1次x=1;第2次x=2;...
printf("%d ", x);
}
int main() {
func(); // 1
func(); // 2
func(); // 3
}
static void hello() {
printf("Hello!");
}
int main() {
hello(); // 正确,可以调用
}
// 其他文件中无法调用hello()
static const int SIZE = 5;
所以,总结来说,static的主要用途为:
2.1.14const常量和#define的区别(编译阶段、安全性、内存占用等) ⭐⭐⭐⭐
const常量和#define的主要区别有:
const int a = 5; // int a = 5; // 编译阶段分配内存
#define b 5 // 预处理阶段进行宏替换
2.1.15 volatile作用和用法 ⭐⭐⭐⭐⭐
volatile关键字的主要作用是:防止变量被错误优化.
在C/C++中,编译器会对代码进行优化,以提高程序的运行效率.但这可能导致volatile变量产生未定义行为,因此需要使用volatile关键字进行标识,告诉编译器不要对其进行优化.
volatile有以下两种主要用法:
volatile int cnt = 0;
void increment() {
cnt++; // 读取cnt的值,进行修改,再写入
}
int main() {
// 线程1
increment();
// 线程2
increment();
}
这里使用volatile确保cnt不会被优化为寄存器变量,这样每个线程都能读取到最新值.
2. 访问硬件寄存器:
当我们从寄存器中读取值或写入值时,使用volatile可以防止编译器对这些代码进行优化,确保每次都真正读取/写入硬件寄存器.例如:
volatile unsigned char *pIoAddr;
pIoAddr = (unsigned char *)0x1000;
// 读取寄存器值
data = *pIoAddr;
// 写入寄存器值
*pIoAddr = 0x05;
这里使用volatile来保证对寄存器的每次访问都是真实的读/写操作.
所以,总结来说,volatile的主要作用是:
2.1.16有常量指针 指针常量 常量引用 没有引用常量,解释以下这是为什么?⭐⭐⭐
int a = 5;
const int* p = &a; // 常量指针p 或写成 int const * p 只要const在*前面都行
a = 10; // 可以修改
p = &b; // 可以修改
*p =21; //不能修改 报错
const int b = 10;
int* const p = &b; // 指针常量p
b = 15; // 编译错误,b是常量
p = &a; // 不能修改
*p =21; //可以修改
int c = 20;
const int& ref = c; // 常量引用ref
c = 25; // 可以修改
ref = 30; // 编译错误,ref是一个常量引用
2.1.17没有指向引用的指针.⭐⭐⭐
因为引用是没有地址的,但是有指针的引用
2.1.18c/c++中变量的作用域⭐⭐⭐⭐⭐
在C/C++中,变量的作用域主要有以下几种:
void func() {
int a = 5; // 局部变量a
}
int main() {
func();
printf("%d", a); // 错误,a的作用域只在func()内
}
void add(int a, int b) { // a和b为函数参数
int sum = a + b;
}
int main() {
add(1, 2);
printf("%d", a); // 错误,a的作用域只在add()内
}
int g = 10; // 全局变量g
void func1() {
printf("%d", g); // 正确,可以使用g
}
void func2() {
printf("%d", g); // 正确,可以使用g
}
namespace ns1 {
int a = 5;
}
namespace ns2 {
printf("%d", a); // 错误,a的作用域在ns1内
}
ns1::printf("%d", a); // 正确,通过作用域解析运算符::使用ns1内的a
2.1.29 c++中类型转换机制?各适用什么环境?dynamic_cast转换失败时,会出现什么情况?⭐⭐⭐
在C++中,有以下几种类型转换机制:
静态类型转换:包括隐式转换和强制类型转换.
const_cast:去除 Const-volatile 类型的常量性和易变性,常用于常量数据的写入.
比如:
const int a = 5;
const int* p = &a;
int* q = const_cast
*q = 10; // 现在可以修改a的值
dynamic_cast:用于执行多态类型之间的没有阶层关系的转换,执行过程中会检查转换的安全性,如果没有继承或派生关系则会返回Null.
reinterpret_cast:可以在任何两种类型直接转换,二进制数据直接复制,不会检查转换的安全性.
static_cast:指明必须存在继承关系的显示类型转换,同时检查是否违反 const-volatile 属性.
这几种类型转换的使用场景如下:
2.2 继承、多态相关面试题 ⭐⭐⭐⭐⭐
2.2.1继承和虚继承 ⭐⭐⭐⭐⭐
继承和虚继承是面向对象编程中的两个重要概念.
继承:
class Base
{
public:
void fun() { ... }
};
class Derived1 : virtual public Base
{
public:
...
};
class Derived2 : virtual public Base
{
public:
...
};
class Derived3 : public Derived1, public Derived2
{
public:
...
};
这里,Derived3同时继承Derived1和Derived2,而Derived1和Derived2又虚继承自同一个基类Base.所以Derived3只会有一个Base,避免了重复继承同一个基类带来的问题.
2.2.2多态的类,内存布局是怎么样的 ⭐⭐⭐⭐⭐
多态类的内存布局与普通类不同.理解多态类的内存布局对学习C++面向对象编程很重要.
普通类的内存布局:
class Base
{
public:
virtual void fun1() { ... }
virtual void fun2() { ... }
};
class Derived : public Base
{
public:
virtual void fun1() { ... } // 重写基类方法
virtual void fun3() { ... } // 新增方法
};
int main()
{
Derived d;
Base* b = &d;
b->fun1(); // 调用Derived::fun1(),使用派生类vtable
b->fun2(); // 调用Base::fun2(),使用基类vtable
d.fun1(); // 调用Derived::fun1(),使用派生类vtable
d.fun3(); // 派生类独有方法,使用派生类vtable
}
可以看出,多态类的内存布局与调用机制实现了动态绑定的效果,这是面向对象编程和C++的核心特征之一.
2.2.3被隐藏的基类函数如何调用或者子类调用父类的同名函数和父类成员变量 ⭐⭐⭐⭐⭐
在继承关系中,子类可以调用被隐藏的基类函数或父类的同名函数/成员变量的几种方式:
1. 使用作用域解析运算符::显式调用基类函数:
class Base
{
public:
void fun() { ... }
};
class Derived extends Base
{
public:
void fun() { ... }
void callBaseFun()
{
Base::fun(); // 调用基类fun()
}
};
1. 使用基类指针或引用调用:
cpp
void Derived::callBaseFun()
{
Base* b = this;
b->fun(); // 调用基类fun()
}
cpp
void Derived::callBaseFun()
{
Base& b = *this;
b.fun(); // 调用基类fun()
}
1. 使用using声明来重命名基类函数:
cpp
class Derived extends Base
{
public:
using Base::fun; // 使用using重命名基类fun为fun
void fun() { ... }
};
现在fun()会调用派生类的fun(),而Base::fun()可以调用基类版本.
Derived::Derived() : Base() { ... }
这会调用Base()构造函数,用于初始化基类子对象.
5. 使用基类的同名非静态成员直接访问:
class Base
{
public:
int val;
};
class Derived extends Base
{
public:
void accessBaseVal()
{
val = 1; // 访问基类val
Base::val = 2; // 也可以使用作用域解析运算符访问
}
};
2.2.4多态实现的三个条件、实现的原理 ⭐⭐⭐⭐⭐
多态性是C++面向对象编程的一个关键特征.实现多态性需要满足三个条件:
class Base
{
public:
virtual void fun() { ... }
};
class Derived extends Base
{
public:
virtual void fun() { ... } // 重写基类虚函数
};
int main()
{
Base* b = new Derived();
b->fun(); // 调用Derived::fun(),动态绑定
}
这里,虽然 b 是Base类型,但因为它指向Derived对象,所以会调用Derived::fun(),实现动态绑定.
总结一下,多态性实现需要:
2.2.5对拷贝构造函数 深浅拷贝 的理解 拷贝构造函数作用及用途?什么时候需要自定义拷贝构造函数?⭐⭐⭐
拷贝构造函数的作用是:
使用另一个同类型对象初始化一个新对象.
拷贝构造函数的语法是:
ClassName (const ClassName &obj) {
// 拷贝构造函数体
}
它与普通构造函数的区别是:参数是相同类型的引用.
拷贝构造函数需要在以下情况下自定义:
举个简单示例:
class Person {
public:
Person() {
name = new string;
*name = "Default Name";
}
Person(const Person &p) { // 自定义拷贝构造函数
name = new string;
*name = *p.name; // 深拷贝,分配新的内存
}
private:
string *name;
};
int main() {
Person p1;
Person p2 = p1; // 使用拷贝构造函数
cout << *p1.name << endl; // Default Name
cout << *p2.name << endl; // Default Name
}
2.2.6析构函数可以抛出异常吗?为什么不能抛出异常?除了资源泄露,还有其他需考虑的因素吗?⭐⭐⭐
析构函数不能抛出异常.这是因为:
2.2.7什么情况下会调用拷贝构造函数(三种情况)⭐⭐⭐
Person p1;
Person p2 = p1; // 调用拷贝构造函数
这里使用p1初始化p2,会调用Person类的拷贝构造函数.
void func(Person p) { ... }
Person p;
func(p); // 调用拷贝构造函数
在将p作为实参传给func()时,会调用拷贝构造函数生成p的一个副本.
2. 以值作为函数的返回值返回对象时
例如:
Person func() { ... }
Person p = func(); // 调用拷贝构造函数
func()的返回值是Person对象,这需要调用拷贝构造函数生成一个临时对象,然后将其返回.
所以总结来说,当我们使用另一个同类型对象初始化一个新对象,或者对象以值作为函数参数或返回值时,都会调用其拷贝构造函数.
拷贝构造函数相比于普通构造函数,增加了一个同类型对象作为参数.它的主要作用是:
将一个对象初始化为另一个对象的副本.
如果类中没有定义拷贝构造函数,编译器会自动生成一个浅拷贝的版本.所以当需要深拷贝或资源管理时,必须自定义拷贝构造函数.
2.2.8析构函数一般写成虚函数的原因⭐⭐⭐⭐⭐
class Base {
public:
virtual ~Base() {} // 虚析构函数
};
class Derived extends Base {
public:
~Derived() {}
};
int main() {
Base *b = new Derived();
delete b; // 调用Derived::~Derived()
}
这里我们对基类指针b调用delete,如果Base()不是虚函数,会直接调用Base::Base(),无法正确释放Derived对象所占用的资源.定义为虚函数后,可以调用正确的析构函数Derived::~Derived().
2.2.9构造函数为什么一般不定义为虚函数⭐⭐⭐⭐⭐
构造函数用于对象的初始化,它创建对象并设置初始状态.虚函数的调用依赖于对象的实际类型,需要对象完全创建并且事先知晓对象的实际类型,这与构造函数的语义不符.此外,定义为虚函数会产生额外空间开销.所以,构造函数一般不定义为虚函数.
2.2.10什么是纯虚函数⭐⭐⭐⭐⭐
纯虚函数是虚函数的一种,它只有函数声明而没有函数体.纯虚函数的定义使用“= 0”syntax,如:
cpp
virtual void func() = 0; // 纯虚函数
包含纯虚函数的类是抽象类,无法实例化.它强制要求所有派生类实现该虚函数,用于定义接口.
2.2.11静态绑定和动态绑定的介绍⭐⭐⭐⭐
静态绑定(Static Binding): 函数调用在编译时决定,这要求在编译时就能确定函数的调用对象.仅普通成员函数才有静态绑定.
动态绑定(Dynamic Binding): 函数调用在运行时(根据对象的真实类型)决定,这需要虚函数和智能指针或引用.动态绑定实现了运行时多态,是C++的一大特点.
2.2.12 C++所有的构造函数 ⭐⭐⭐
C++有5种构造函数:
2.2.13重写、重载、覆盖的区别⭐⭐⭐⭐⭐
重写(Override): 子类中与父类虚函数具有相同名称、参数列表和返回值类型的函数.用于实现动态绑定和运行时多态.
重载(Overload): 同一个类中,多个函数具有相同名称但参数列表不同.用于处理不同的参数类型和个数.
覆盖(Overide): 与C++中的重写概念相同,子类中与父类虚函数有相同的函数签名,替换父类的虚函数实现.
2.2.14成员初始化列表的概念,为什么用成员初始化列表会快一些(性能优势)?⭐⭐⭐⭐
成员初始化列表出现在构造函数参数列表后的冒号后面,用于初始化类的成员变量.
使用成员初始化列表而不是在构造函数体内初始化成员变量有以下优势:
2.2.15如何避免编译器进行的隐式类型转换;(explicit)⭐⭐⭐⭐
在C++中,如果构造函数只有一个参数,编译器会自动进行隐式类型转换,这会引起一些问题.
可以使用explicit关键字将其定义为显式构造函数,防止编译器进行隐式类型转换:
class Person {
public:
explicit Person(int age) { ... } // 显式构造函数
};
int main() {
Person p = 10; // 错误,无法隐式转换
Person p(25)
}
3.1 TCP UDP
3.1.1 TCP、UDP的区别 ⭐⭐⭐⭐⭐
TCP是连接的,UDP是无连接的.
TCP保证可靠发送,UDP不保证.
TCP面向字节流,UDP面向报文.
TCP首部开销大,UDP首部开销小.
TCP传输速度慢,UDP传输速度快.
3.1.2 TCP、UDP的优缺点⭐⭐⭐
TCP 优点:
3.1.3 TCP UDP适用场景⭐⭐⭐
TCP适用场景:
3.1.4 TCP为什么是可靠连接⭐⭐⭐⭐
TCP有以下特征使其成为可靠连接:
3.1.5典型网络模型,简单说说有哪些;⭐⭐⭐
常见的网络模型有:
3.1.6 Http1.1和Http1.0的区别⭐⭐⭐
Http1.1与Http1.0的主要区别:
3.1.7 URI(统一资源标识符)和URL(统一资源定位符)之间的区别⭐⭐
URI和URL的主要区别:
URI是URL的超集,URL是一种具体的URI.
URI用于唯一标识一个资源,URL提供了找到该资源的方式.
URI只定义了资源的标识,URL在URI的基础上添加了定位该资源的信息,如主机名、路径等.
所有URL都是URI,但不所有URI都是URL.
3.2 三次握手、四次挥手
3.2.1什么是三次握手⭐⭐⭐⭐⭐
三次握手是TCP连接建立的过程.它分为三个步骤:
3.2.2为什么三次握手中客户端还要发送一次确认呢?可以二次握手吗?⭐⭐⭐⭐
三次握手需要客户端再次发送ACK报文的原因是:
3.2.3为什么服务端易受到SYN攻击?⭐⭐⭐⭐
服务端易受到SYN攻击的原因是:
3.2.4什么是四次挥手⭐⭐⭐⭐⭐
3.2.5为什么客户端最后还要等待2MSL?⭐⭐⭐⭐
MSL(Maximum Segment Lifetime)是报文段最大生存时间.2MSL时间内,报文段有可能被重传.
客户端等待2MSL的原因是:
3.2.6为什么建立连接是三次握手,关闭连接确是四次挥手呢?⭐⭐⭐⭐
三次握手和四次挥手之所以采用不同的步骤数,原因是:
建立连接时,客户端和服务器都是从初始状态开始的,都可以主动发起连接请求.所以采用三步握手,客户端首先发起SYN请求,服务器回复ACK确认,客户端再次确认ACK.
关闭连接时,由于连接已建立,其中一方要主动发起关闭请求.此时另一方可能还在发送数据,所以需要四步挥手来协调连接的释放:
4.1 排序算法
4.1.1各种排序算法的时间空间复杂度、稳定性⭐⭐⭐⭐⭐
算法 | 最好 | 最坏 | 平均 | 空间 | 稳定性 |
---|---|---|---|---|---|
冒泡排序 | O(n) | O(n^2) | O(n^2) | O(1) | 稳定 |
选择排序 | O(n^2) | O(n^2) | O(n^2) | O(1) | 不稳定 |
插入排序 | O(n) | O(n^2) | O(n^2) | O(1) | 稳定 |
希尔排序 | O(nlogn) | O(nlog^2n) | O(nlogn) | O(1) | 不稳定 |
归并排序 | O(nlogn) | O(nlogn) | O(nlogn) | O(n) | 稳定 |
快速排序 | O(nlogn) | O(n^2) | O(nlogn) | O(logn) | 不稳定 |
堆排序 | O(nlogn) | O(nlogn) | O(nlogn) | O(1) | 不稳定 |
4.1.2各种排序算法什么时候有最好情况、最坏情况(尤其是快排) ⭐⭐⭐⭐
最好情况:数据已经排序,选取的划分元素恰好是中间元素
平均情况:选取的划分元素接近中间
最坏情况:数组中只有两个不同数值,选取的划分元素总是极端元素.
例如快速排序:
最好情况:O(nlogn),当数组已经排序,每次选取的划分元素恰好是中间元素时
平均情况:O(nlogn),选取的划分元素随机接近中间
最坏情况:O(n^2),当数组中只有两个不同数值,每次选取的划分元素总是极端元素时,会退化为冒泡排序.
4.1.3 快速排序
void quick_sort(int q[], int l, int r)
{
if (l >= r) return;//结束条件
int i = l - 1, j = r + 1, x = q[l + r >> 1];
while (i < j)
{
do i ++ ; while (q[i] < x);
do j -- ; while (q[j] > x);
if (i < j) swap(q[i], q[j]);
}
quick_sort(q, l, j), quick_sort(q, j + 1, r);
}
4.1.4 归并排序
void merge_sort(int q[], int l, int r)
{
if (l >= r) return;
int mid = l + r >> 1;
merge_sort(q, l, mid);
merge_sort(q, mid + 1, r);
int k = 0, i = l, j = mid + 1;
while (i <= mid && j <= r)
if (q[i] <= q[j]) tmp[k ++ ] = q[i ++ ];
else tmp[k ++ ] = q[j ++ ];
while (i <= mid) tmp[k ++ ] = q[i ++ ];
while (j <= r) tmp[k ++ ] = q[j ++ ];
for (i = l, j = 0; i <= r; i ++, j ++ ) q[i] = tmp[j];
}
4.2 STL库相关
4.2.1 vector list异同⭐⭐⭐⭐⭐
相同点:
4.2.2 vector内存是怎么增长的vector的底层实现⭐⭐⭐⭐
vector的内存空间是连续的,在创建时根据初始大小分配内存.当元素增加超过当前容量时,vector会重新分配内存,新空间的大小为当前空间的2倍.
vector的底层结构如下:
struct vector {
char* start; //首元素地址
char* end; //尾元素的下一个位置
char* endOfStorage; //当前空间的尾部
//...
}
插入元素时,如果超出endOfStorage,则会分配新空间,将原数据复制过来,释放旧空间.
4.2.3 vector和deque的比较⭐⭐⭐⭐
相同点:
4.2.4为什么stl里面有sort函数list里面还要再定义一个sort⭐⭐⭐
STL中sort函数使用的排序算法是比较排序,它要求被排序的容器必须支持随机访问迭代器.
list是以双向链表实现的,只支持双向迭代器,不支持随机访问.所以list不能直接使用通用的sort算法.
为了给list也提供排序功能,STL在list中另外实现了一个sort成员函数,该函数使用归并排序算法,它只需要双向迭代器就可以工作,所以可以用于list.
所以,原因是算法要求的迭代器种类不同导致的.STL为了各容器都提供排序功能,在list中重载了sort成员函数.
4.2.5 STL底层数据结构实现⭐⭐⭐⭐
vector: 数组
list: 双向链表
deque: 双端队列
stack: 以deque或list实现
queue: 以deque或list实现
priority_queue: 以vector和堆结构实现
set/multiset: 红黑树
map/multimap: 红黑树
hash_set/hash_map: 哈希表
4.2.6利用迭代器删除元素会发生什么?⭐⭐⭐⭐
使用迭代器删除STL容器中的元素会发生以下情况:
4.2.7 map是如何实现的,查找效率是多少⭐⭐⭐⭐⭐
map是基于二叉树(红黑树)实现的. map的查找时间复杂度为O(logN).
map的底层结构为二叉搜索树,它具有以下特点:
4.2.8几种模板插入的时间复杂度 ⭐⭐⭐⭐⭐
常见的STL容器插入元素的时间复杂度:
vector:
vector的底层是数组,插入元素需要移动元素,时间复杂度O(N).
但vector在元素少时(不触发扩容)是O(1).
list:
list的底层是双向链表,插入元素只需要调整指针,时间复杂度O(1).
deque:
deque的插入取决于插入的位置,在头尾插入是O(1),在中间插入需要移动元素,是O(N).
set/map:
set和map的底层是红黑树,插入元素需要维护二叉搜索树,时间复杂度O(logN).
unordered_set/unordered_map:
unordered_set和unordered_map的底层是哈希表,插入元素需要 rehash,时间复杂度期望是O(1).
所以,总结下来,常见容器的插入时间复杂度为:
vector:O(N)/O(1)
list:O(1)
deque:O(1)/O(N)
set/map:O(logN)
unordered_set/unordered_map:O(1)
5.1 Linux内核相关
5.1.1 Linux内核的组成⭐⭐
什么搞清楚什么是内核?内核就是软件层面的核心系统程序.
5.1.2用户空间与内核通信方式有哪些?⭐⭐⭐⭐⭐
用户空间和内核空间的通信方式主要有:
5.1.3系统调用read()/write(),内核具体做了哪些事情⭐⭐
系统调用read()和write()在内核空间中会做以下事情:
read():
5.1.4系统调用的作用⭐⭐⭐⭐⭐
系统调用的主要作用是:
5.1.5内核态,用户态的区别⭐⭐⭐⭐⭐
内核态和用户态的主要区别如下:
5.1.6 bootloader内核 根文件的关系⭐⭐⭐⭐
Bootloader的启动过程通常分为两阶段:
第一阶段:主引导记录(MBR)加载与执行
5.1.7 Bootloader多数有两个阶段的启动过程:⭐⭐⭐
是的,Linux的内核是由bootloader装载到内存中的.
Linux的启动过程如下:
5.1.8 linux的内核是由bootloader装载到内存中的?⭐⭐⭐
是的,Linux的内核是由bootloader装载到内存中的.
Linux的启动过程如下:
5.1.9为什么需要BootLoader⭐⭐⭐⭐
需要BootLoader的主要原因是:
5.1.10 Linux内核同步方式总结⭐⭐⭐⭐
Linux内核中的同步方式主要有:
5.1.11为什么自旋锁不能睡眠 而在拥有信号量时就可以?⭐⭐⭐⭐
这是因为自旋锁和信号量的机制不同:
自旋锁:
5.1.12 linux下检查内存状态的命令⭐⭐⭐
linux下常用的检查内存状态的命令有:
free:显示当前内存使用总量和空闲内存量.
bash
free -h # -h 人性化显示
vmstat:详细显示内存相关统计信息.
bash
vmstat 2 3 # 每2秒获取一次信息,共3次
pidstat:统计每个进程的内存使用情况.
bash
pidstat -r 2 3 # 每2秒获取一次信息,共3次
sar:历史记录和monitoring系统活动情况.
bash
sar -r 2 10 # 每2秒检查一次,共10次,监控内存相关信息
top:动态监控进程状态,其中包括内存统计信息.
bash
top -b -n 2 # 批处理模式共2次,查看内存信息
这些命令可以监控内存使用量、使用比例、分配情况、进程内存使用等信息.
通过这些信息可以了解系统内存的负载、分配和潜在瓶颈,也是检测内存泄漏和优化的重要手段.
5.1.13 linux的软件中断⭐⭐⭐
软件中断. 软件中断和我们常说的中断(硬件中断) 不同之处在于, 它是通
过软件指令触发而并非外设引发的中断, 也就是说, 又是编程人员开发出的一种异
常(该异常为正常的异常) . 操作系统一般是通过软件中断从用户态切换到内核态
5.2 其他操作系统常见面试题
5.2.1大小端的区别以及各自的优点,哪种时候用⭐⭐⭐⭐⭐
大小端分为大端(Big Endian)和小端(Little Endian)两种:
大端:高位字节存储在低地址,低位字节存储在高地址.
小端:低位字节存储在低地址,高位字节存储在高地址.
大小端的优缺点:
大端:
优点:地址加1,值也加1,计算方便.
缺点:不容易理解,两台系统间数据交互困难.
小端:
优点:数据在内存的表示更自然,两台不同系统间数据交互方便.
缺点:地址加1,值不一定加1,计算稍复杂.
选择的时候需要考虑:
5.2.2 一个程序从开始运行到结束的完整过程(四个过程)⭐⭐⭐⭐⭐
一个程序从开始运行到结束的完整过程通常分为四个阶段:
5.2.3什么是堆,栈,内存泄漏和内存溢出?⭐⭐⭐⭐
堆:动态分配内存,可以任意增长和释放,由程序员管理.
栈:函数参数、局部变量等自动分配和释放,由编译器管理.
内存泄漏:程序未释放已经不再使用的堆内存,导致内存资源浪费.
内存溢出:程序尝试访问超出可用内存范围的地址,导致程序崩溃.
具体来说:
堆:
5.2.4堆和栈的区别⭐⭐⭐⭐⭐
堆和栈的主要区别如下:
5.2.5死锁的原因、条件 创建一个死锁,以及如何预防⭐⭐⭐⭐⭐
死锁的原因:
#include
#include
#define NUM_THREADS 5
#define TCOUNT 10
int count = 0;
pthread_mutex_t mutex1 = PTHREAD_MUTEX_INITIALIZER;
pthread_mutex_t mutex2 = PTHREAD_MUTEX_INITIALIZER;
void *inc_count(void *t) {
pthread_mutex_lock(&mutex1);
count++;
printf("Incrementing count in thread %d to %d\n",*(int*)t, count);
pthread_mutex_unlock(&mutex1);
}
void *dec_count(void *t) {
pthread_mutex_lock(&mutex2);
count--;
printf("Decrementing count in thread %d to %d\n",*(int*)t, count);
pthread_mutex_unlock(&mutex2);
}
int main(void) {
pthread_t threads[NUM_THREADS];
int a[NUM_THREADS], b[NUM_THREADS];
for (int i = 0; i < NUM_THREADS; i++) {
a[i] = i; b[i] = (i+1)%NUM_THREADS;
printf("Creating thread %d and %d\n", i, b[i]);
pthread_create(&threads[i], NULL, inc_count, &a[i]);
pthread_create(&threads[b[i]], NULL, dec_count, &b[i]);
}
pthread_exit(NULL);
}
运行结果:
Creating thread 0 and 1
Incrementing count in thread 0 to 1
Decrementing count in thread 1 to 0
Creating thread 1 and 2
Incrementing count in thread 2 to 1
然后程序卡住,出现死锁.
预防死锁的方法:
5.2.6硬链接与软链接的区别;⭐⭐⭐⭐⭐
5.2.7虚拟内存,虚拟地址与物理地址的转换⭐⭐⭐⭐
虚拟内存:为每个进程设置一个逻辑地址空间,通过映射管理离散的物理内存,实现内存空间的虚拟化.
它带来的好处:
5.2.8计算机中,32bit与64bit有什么区别⭐⭐⭐
32bit与64bit的主要区别:
5.2.9中断和异常的区别⭐⭐⭐⭐⭐
中断与异常的主要区别:
5.2.10中断怎么发生,中断处理大概流程⭐⭐⭐⭐
5.2.11 Linux 操作系统挂起、休眠、关机相关命令⭐⭐
Linux下相关命令:
挂起(suspend):
5.2.12数据库为什么要建立索引,以及索引的缺点⭐⭐
数据库建立索引的好处:
6.1 CPU 内存 虚拟内存 磁盘/硬盘 的关系⭐⭐⭐
6.2 CPU内部结构⭐⭐⭐⭐
CPU主要包含如下内部结构:
6.3 ARM结构处理器简析 ⭐⭐
6.3 ARM结构处理器简析 :
ARM是一种基于RISC(精简指令集计算机)设计理念的处理器结构,主要特征如下:
6.4波特率是什么,为什么双方波特率要相同,高低波特率有什么区别;⭐⭐⭐⭐
波特率:也称波特频率,是通信设备连接的信号输入或者输出的频率,通常以位/秒为单位.
为什么通信双方波特率要相同:
通信双方使用相同的波特率保证数据接收和发送的同步,如果波特率不同则无法正确解码和识别对方发送的数据.
通信发送端按照一定波特率发送数据,接收端正是根据预先约定的波特率来对数据流进行采样和解码的.
如果波特率不一致,那么接收端对数据流的采样就不是按照正确的时序进行,最终导致解码出错和通信失败.
高低波特率的区别:
高波特率:
6.5arm和dsp有什么区别⭐⭐
6.6 ROM RAM的概念浅析⭐⭐⭐
ROM和RAM是计算机存储器的两种主要类型,其主要概念如下:
ROM(只读存储器):
6.7 IO口工作方式:上拉输入 下拉输入 推挽输出 开漏输出⭐⭐⭐⭐
IO口的四种工作方式:
6.8扇区 块 页 簇的概念⭐⭐⭐⭐
扇区(Sector):
是磁盘的最小物理存储单位,大小通常为512字节.
硬盘通过磁头在盘片表面读写数据,一个扇区对应盘片上连续的弧段.
块(Block):
是文件系统对磁盘扇区的逻辑管理单位,包含1个或多个扇区.
文件系统通过块来对磁盘扇区进行抽象和管理,用户操作文件的最小单位即为块.
块的大小在设计文件系统时确定,如4KB或8KB,须是扇区大小的整数倍.
页(Page):
是操作系统实现虚拟内存时的管理单位.由连续的块组成,大小为4KB或更大.
页实现了逻辑地址到物理地址的映射,使应用程序可以像访问连续内存一样访问磁盘.
簇(Cluster):
是文件系统将若干个连续块组织在一起进行文件存储的单位.
文件在磁盘上存放时,会被分配若干簇用来存放用户数据.
簇的大小在设计文件系统时确定,通常为4KB至64KB或更大.
为提高文件的读写效率而设置,文件的最小读写单位即为一个簇.
所以,这几个概念的层次关系为:
扇区→块→页→簇
扇区是磁盘的物理存储单元.
块是文件系统管理扇区的基本单元.
页是操作系统实现虚拟内存时与块对应的逻辑单元.
簇是文件系统将几个连续块组织在一起存放文件的最小单元.
6.9简述处理器在读内存的过程中,CPU核、cache、MMU如何协同工作?画出CPU核、cache、MMU、内存之间的关系示意图加以说明⭐⭐
+-----------+ +---------+
逻辑地址| CPU核 |-------| MMU |
+-----------+ +---------+
| |
| +--------+--------+
+-------| 存储器控制器 |
+--------+--------+
| |
+--------------+ +--------------+
物理地址 | | | |
+-----------+ | +--+--+ +--+--+ |
| 二级缓存 | | |存储器| |存储器| |
+-----------+ | +--+--+ +--+--+ |
+-----------+ | +--+--+ |
| 一级缓存 | | |存储器| |
+-----------+ | +--+--+ |
| |
| |
所以,在读内存过程中,CPU核、缓存、MMU和存储器控制器密切配合:
CPU核发出读请求,首先查询缓存,不命中则送MMU进行地址翻译.
MMU将逻辑地址翻译为物理地址,发送到存储器控制器.
存储器控制器从存储模块读取数据,并更新缓存.
CPU核从一级缓存获取数据并执行.
这种协同机制实现了存储器层次化,使CPU可以高速地执行指令和访问大容量存储.
6.10请说明总线接口USRT、I2C、USB的异同点(串/并、速度、全/半双工、总线拓扑等)⭐⭐⭐⭐⭐
USART(Universal Asynchronous Receiver/Transmitter):
6.11什么是异步串口和同步串口⭐⭐⭐⭐⭐
异步串口:
6.12 FreeRTOS同优先级的任务创建的执行顺序是什么?
在FreeRTOS中,会把新创建的任务以头插法的形式放在同优先级的就绪链表中,若优先级相同,后创建的任务会新得到执行.
题目转载于链接:https://leetcode.cn/circle/discuss/u3CZOa/
来源:力扣(LeetCode)
答案由我自己查询提供,可能会有错误,仅供参考.
定时器频率/定时器的重转载值= 系统时钟频率/定时器的预分频系数/定时器的重装载值
FreeRTOS的内核比RTT的要小很多.FREERTOS是一个精简的内核,而RTT不仅是一个实时内核,还具备丰富的软件包(如MQTT软件包),FreeRTOS的实时性会更高一些,因为它最小上下文具体切换只需要3个计数周期,因为在最理想的情况下,FreeRTOS 的任务切换只需要 3 条机器指令就可以完成:
1. PUSHSR ; 保存当前任务的状态
2. MOV SP,新任务SP ; 切换栈指针到新任务
3. POPSR ; 恢复新任务的状态
这 3 条指令分别用于:保存当前任务现场、切换任务栈、恢复新任务现场,所以理论上 FreeRTOS 的最快上下文切换时间就是这 3 条指令的执行时间,大约相当于 3 个机器周期,这就是 “3 个计数器” 的来历。
相比而言,RT-Thread 的上下文切换会更加复杂一些,除了栈指针和程序状态寄存器之外,还需要保存更多的 CPU 寄存器状态,并在切换后恢复,所以上下文切换时间长一些,一般在 10 个周期左右。
所以,当 FreeRTOS 说其 “最小上下文切换时间有 3 个计数器” 时,其实就是说在最理想的条件下,其任务切换最快可以在 3 个机器周期内完成,这主要得益于其精简的设计思想,切换时只需要保存和恢复最必要的任务现场,所以上下文切换开销很小。而 RT-Thread 由于需要支持更丰富的功能,其任务现场要保存和恢复的内容更多,所以上下文切换开销也更大,时间长一些。
串口中断中数据是怎么处理的?
保存到数组或环形队列里.在STM32中的串口中断回调函数中处理.
串口数据接收,如果一个较大的数据包发送过来(1K字节以上,带帧头 帧长和校验码)你怎么解析和处理?
考虑buffer接收长度,对方发送的数据有异常的处理,接收超时机制
标准答案:
1. 定义包头和包长度字段
需要在数据包中定义包头和包长度两个字段。包头用于标识新包的开始,包长度用以指示整个数据包的长度。这两个字段的定义规则需要发送方和接收方事先确定。
1. 读取包头和包长度
当接收到新数据时,首先读取包头判断是否为新包开始。如果是,则继续读取包长度字段,得到整个数据包的长度信息。
1. 申请包长度所需的缓冲空间
根据获得的包长度信息,需要申请足够的缓冲空间存放整个数据包的内容。如果长度超过默认缓冲区长度,需要动态申请空间。
1. 循环读取数据至缓冲区填满
将串口接收到的数据循环读取至步骤3申请的缓冲区,直到填满为止。这个过程可能伴随数据拷贝,需要考虑接收效率。
1. 校验数据包
接收到完整数据包后,需要对数据包进行校验,验证包长度、CRC校验码以及其他关键字段是否正确。只有校验通过的包才会被进一步处理。
1. 处理数据包内容
对校验通过的数据包,进行解析并处理其内容。完成后继续等待下一个数据包的接收与解析。
1. 错误处理
在数据包接收与校验的过程中,可能出现超时、长度错误、CRC校验失败等问题。需要在代码中加入相应的错误处理逻辑,避免程序崩溃。
所以,对较长数据包的串口接收,关键在于:
1. 定义包头和包长度,获取包长信息;
2. 提前申请足够缓冲区空间,避免缓冲区溢出;
3.循环读取至缓冲区填满,考虑接收效率;
4. 严格校验包内容,过滤错误包;
5. 增加超时和错误处理机制,提高健壮性。
首先判断是否配置了可以抢占,若配置了可以抢占则每次从优先级链表中去查找,如果高优先级链表中的任务就绪则得到执行.
tick中断(这个在free rt os中默认是以systick为滴答时基).
主动调用taskYIELD()进行任务切换。
硬件中断,这个有很多种,比如在串口中断回调函数中,或者是其他的中断回调函数中唤醒了某个高线程,则在中断结束后立刻发生调度,无需等待tick中断的调度。
在FreeRTOS中,如果通过宏配置将任务调度策略设置为非抢占(PREEMPT_OFF)且非礼让(INTERRUPT_OFF),则当前执行的任务将一直运行,其他任务无法被调度执行。
这是因为:
- 非抢占模式下,只有在任务主动放弃CPU(通过taskYIELD()函数调用)时,才会进行任务切换。如果任务一直运行且不主动放弃CPU,则其他任务无法执行。
- 非礼让模式下,禁止所有中断发生。这使得RTOS无法在定时中断中进行任务切换,时间片功能也无法使用。所以当前任务将持续运行,其他任务无法被调度。
- 同优先级任务在非抢占非礼让模式下也无法切换,因为时间片轮转功能被禁止,任务只有主动放弃CPU才可以切换。
- 处在非抢占模式下的任务可以调用taskYIELD()主动放弃CPU,但非礼让模式下该调用无效,任务仍然不会切换。
所以,在非抢占非礼让的配置下,由于时间片功能与中断被禁止,任务切换机制被完全禁止。当前任务可以毫无限制地持续运行,CPU资源无法被其他任务获得。这种模式下任务的实时性是最差的,但可以获得最高的执行效率。
一般来说,非抢占非礼让模式很少使用,除非对CPU利用率和执行效率有极高要求的场合。更多时候,我们会选择:
- 抢占模式:可以根据优先级实现任务自动切换,获得较好的实时性;
- 礼让模式:通过开启中断,可以使用时间片来共享CPU,达到较好的任务切换效果;
- 抢占+礼让:兼顾实时性与效率,是FreeRTOS中最常用的配置模式。
冒泡排序的思路是什么? 解释一下时间复杂度的计算? 为什么是O(N^2)
裸机开发的怎么实现一个软件定时器? 如何定时处理100个任务?
IO口有哪些模式? 推挽输出和开漏输出的区别是什么?
IIC的读时许解释一下?(这个和上面的IIC时许相似,只不过问得更具体)
链表有二分查找吗? 一般什么情况下用二分查找?
DFS,BFS算法解释一下.(深搜和宽搜,像具体得算法还算遇到的比较少了)