项目准备及自我介绍
1. 自我介绍
面试官你好,我叫XXX,就读于重庆邮电大学;实验室是国家信息无障碍研发中心;研究生期间,参与两起机器人项目,一是基于SLAM的清洁机器人,实现了建图、导航、路径规划、弓字型清扫和自动回充等功能;二是基于SLAM的导盲机器人,初步实现了室内外导航功能;本科期间累计获得艺术类省级奖项一次,创业类国家级一等奖一次;研究生期间两次获得学业二等奖学金,目前已发表机器人相关论文EI会议两篇。
2. 清洁机器人项目
2.1项目简介
这个项目叫做基于SLAM的清洁机器人,从我研一刚来的时候就在做,一直到研二;它分为上位机和下位机,下位机是STM32F4 ,上位机用的是Nvidia的TX2。上位机安装ROS,使用ROS中的一些工具来开发我们需要的功能并且可以可视化机器人的状态;ROS是一种分布式软件框架,节点可以运行在不同的计算平台上,通过Topic进行通信,话题属于异步数据流通信;平时在调试时我们配置好网络之后,将机器人设为master(主机),我电脑上的Ubuntu通过ssh命令远程连接机器人,就可以通过ROS中的话题通信了。
然后下层就是负责运动控制、传感器数据采集等;就比如要实现机器人手动导航,上层通过cmd_vel这个话题给下层发线速度和角速度,下层和上层通过串口通信,下层收到了这个话题,再根据机器人运动学模型(我们那个是差分的),算出左右的轮子的速度,再通过PID来调电机。手动导航整个控制的过程就是这样。
要实现更加复杂的功能,可以加更多的传感器,我们的项目加了激光雷达,红外,imu,超声波等;可以实现建图、导航、弓字形清扫、自动充电等功能;项目大概就是这样,谢谢。
2.2 项目难点
2.2.1 红外充电
自动回充的过程:
从底层到上层,整体配合实现充电;充电桩有四个红外发射器,清洁机器人前面有两个红外接收器,充电桩发出1248,底层的接收器接受充电桩发出的信号,再通过串口通信以话题的形式发给上层,上层收到红外信号,根据红外信号来调整机器人的姿态;最后直至充上电;
充电桩红外发射端四个信号1 2 4 8(自己规定的,用来区分信号的),四个信号的波形的二进制转成十进制分别是(132,129,130,136),规定好发射接受的频率,读对应引脚的电平,当是高电平的时候开始计数,在这个时间段内计数,计数之后,跟(132,129,130,136)进行比对,看是哪一个信号的,再把红外数据的结构体u_infare里面的值设置成对应的那个(1 2 4 8),maincpp.cpp中再通过一定频率发送结束到的红外信号;
struct infare
{
uint8_t carleft;
uint8_t carright;
uint8_t carmid;
};
void publishInfaredata()//发布接受到的红外信号
{
raw_infaredata_msg.carmid=u_infare.carmid;
// u_infare.carmid=0;
raw_infaredata_msg.carright=u_infare.carright;
// u_infare.carright=0;
raw_infaredata_msg.carleft=u_infare.carleft;
// u_infare.carleft=0;
raw_infaredata_pub.publish(&raw_infaredata_msg);
u_infare.carmid=0;
u_infare.carleft=0;
u_infare.carright=0;
}
2.2.2 刷机步骤
下载板子官方的sdk manager 下载并创建系统镜像,镜像准备完毕之后,进入恢复模式,通过usb连接,进行烧写,这个程序会执行一个flash.sh脚本来烧写系统;
镜像备份也是通过这个flash.sh脚本
2.3 ROS话题通信(异步)
- talker向master注册发布者信息
- listener同上操作
- master通过RPC向listener发送talker的地址信息
- listener接收到地址信息,通过RPC向talker发送连接请求
- talker确认连接请求,通过RPC向listener确认连接
- listener尝试与talker建立连接
- 发送数据
总结:前五步的通信协议都是RPC,最后传输数据才用TCP
3. 集群聊天服务器项目
项目地址 https://github.com/JackyJiangdongjie/Cluster-chat-server
3.1 项目简介
这个项目分为了四个模块,
第一个网络模块,采用的是开源的muduo网络库,好处就是解耦了网络模块代码和业务模块代码,封装了epoll,让开发者专注于业务模块的代码的开发,用这两个类TcpServer和TcpClient完成网络模块的操作 , 初始化TcpServer ,事件循环,服务器的listenaddr,通过绑定器设置连接回调函数和消息回调函数 ,设置EventLoop的线程个数
服务层用了一些c++11的技术,比如bind 绑定器,消息发生之后,回调操作的绑定,当网络IO有消息请求的话,通过消息请求,从消息里边解析出json,得到消息ID,通过回调来处理这个消息,就是这么的一个过程。
数据存储层,用了关系型数据库MySQL,对于项目上的一些关键数据,进行存储,比如用户的账号,用户离线消息,好友列表,群组列表关系,都是在MySQL里面存储的;
单台服务器下,主要就是这几个模块,单台服务器下,它的并发能力是有限的,为了提高并发能力,项目要支持多机扩展,要部署多台网络服务器的话,需要负载均衡,我这个项目因为主要是基于TCP协议自己去搭建的CS通信,所以是基于Nginx TCP负载均衡,要做一个长连接,长连接应用的是消息聊天通信,客户端不仅仅要主动给服务器发消息,服务器还要主动给客户端推消息,必须得用长连接,短连接,服务端没办法给客户端直接推消息,另外在负载均衡里边,因为我是每个服务器里,有不同的用户进行注册,那在不同服务器上注册的用户要进行通信的话,主要是引入了Redis 作为一个MQ消息队列的一个功能,利用它的发布订阅,在这里边实现了跨服务器的消息通信
/*服务器类,基于muduo库开发*/
class ChatServer
{
public:
// 初始化TcpServer
ChatServer(muduo::net::EventLoop *loop,
const muduo::net::InetAddress &listenAddr)
:_server(loop, listenAddr, "ChatServer")
{
// 通过绑定器设置回调函数
_server.setConnectionCallback(bind(&ChatServer::onConnection,this, _1));
_server.setMessageCallback(bind(&ChatServer::onMessage,this, _1, _2, _3));
// 设置EventLoop的线程个数
_server.setThreadNum(10);
}
// 启动ChatServer服务
void start()
{
_server.start();
}
private:
// TcpServer绑定的回调函数,当有新连接或连接中断时调用
void onConnection(const muduo::net::TcpConnectionPtr &con);
// TcpServer绑定的回调函数,当有新数据时调用
void onMessage(const muduo::net::TcpConnectionPtr &con,
muduo::net::Buffer *buf,
muduo::Timestamp time);
private:
muduo::net::TcpServer _server;
};
/*
客户端实现,基于C++ muduo网络库
*/
class ChatClient
{
public:
ChatClient(muduo::net::EventLoop *loop,const muduo::net::InetAddress &addr)
:_client(loop, addr, "ChatClient")
{
// 设置客户端TCP连接回调接口
_client.setConnectionCallback(bind(&ChatClient::onConnection,this, _1));
// 设置客户端接收数据回调接口
_client.setMessageCallback(bind(&ChatClient::onMessage,this, _1, _2, _3));
}
// 连接服务器
void connect()
{
_client.connect();
}
private:
// TcpClient绑定回调函数,当连接或者断开服务器时调用
void onConnection(const muduo::net::TcpConnectionPtr &con);
// TcpClient绑定回调函数,当有数据接收时调用
void onMessage(const muduo::net::TcpConnectionPtr &con,
muduo::net::Buffer *buf,
muduo::Timestamp time);
muduo::net::TcpClient _client;
}
3.1.1 muduo的网络模型
reactors in threads - one loop per thread
方案的特点是one loop per thread,有一个main reactor负载accept连接,然后把连接分发到某个subreactor(采用round-robin的方式来选择sub reactor),该连接的所用操作都在那个sub reactor所处的线程中完成。多个连接可能被分派到多个线程中,以充分利用CPU。
3.2 数据明文传输的安全问题(json)
数据加密和解密
3.2.1 对称加密算法
客户端和服务端使用的同一套密钥
加解密效率高 AES加解密算法
3.2.2 非对称加密算法
公钥加密的,只能用私钥来解密,私钥加密的,只能用公钥来解密。
加密复杂,效率慢,但安全,RSA加解密算法
具体用在项目上,服务端会有一个RSA的私钥,客户端代码里边集成了一个RSA的公钥,
结合的方法:
用RSA公钥加密AES密钥key 传输到服务端,服务端用RSA私钥也解密AES密钥key 后面就用对称加密算法来传输
3.3 历史消息存储
3.3.1 本地消息存储
3.3.2 云消息存储
3.4 客户端消息如何按序显示
消息添加序列号seq
3.5 Redis实现功能不稳定,还有哪些组件可用
服务器中间件:其他相似MQ消息队列!!!
kafka
zeromq
rabbitmq Topic主题
rocketmq
3.6 redis运行不稳定,挂了的话怎么办
redis 消息积累的过快, 消息消费得过慢,
redis实现得发布订阅功能还是比较简单,实际应用中,非关键业务或者流量不是非常大用到得异步通知订阅功能,还是可以用redis,
如果依赖发布订阅实现核心功能,聊天功能,就需要采用专业级的消息队列 来实现消息传输。
3.7 Redis核心功能 key-value 缓存数据库如何用在项目上
可以把用户的登录的状态存在Redis里边,要查用户的状态,先在Redis里面查,查到了直接返回,没查到,再去数据库里边查,查完了再往Redis里面写,
3.8 为什么要用Redis作为跨服务器通信的组件 为什么各个server不能直接相互通信呢
如果六台服务器,跨服务器通信的时候,因为不在一个服务器上注册的,两个用户要聊天,我的服务器要把我发的消息再转发到你的服务器上,任务服务器上的用户,都有可能跟我通信,如果服务器两两直连,所有的server又要当客户端,又要当服务器,整个设计就很复杂,这个会造成服务器之间耦合性太高,而且,为了检测其他服务器是否在线,还得不断跟其他服务器保持一个心跳机制。
在这里边,由于一台服务器发生故障,那么其他服务器会花很大的力气去维持这个心跳,心跳维持不住了,会拆除这个连接,当我们新去增加一台服务器时,还要想办法让其他服务器去连接这一台服务器,然后这台服务器作为客户端连接所有的其他服务器,这样的设计不适合服务器集群,也就没有服务器中间件的必要了。
进行跨服务器通信的时候,都是引入消息队列,用消息队列来解耦服务器的耦合程度,所有的服务器不需要感知其他服务器的存在,有服务器挂掉的话,不影响其他的服务器,
3.9 如果网络拥塞严重,Server端如何感知客户端在线还是掉线了
增加心跳机制
3.10 项目里面怎么体现设计模式的
项目用到了饿汉式-单例模式,是线程安全的,而懒汉式需要加锁:
单例模式(Singleton Pattern,也称为单件模式),使用最广泛的设计模式之一。其意图是保证一个类仅有一个实例,并提供一个访问它的全局访问点,该实例被所有程序模块共享。
定义一个单例类:
- 私有化它的构造函数,以防止外界创建单例类的对象;
- 使用类的私有静态指针变量指向类的唯一实例;
- 使用一个公有的静态方法获取该实例。
include有一个chatservice.hpp 定义了一个聊天服务器业务处理类 class ChatService
class ChatService
{
public:
// 获取单例对象的接口函数
static ChatService *instance();
private:
ChatService(); //采用单例模式来设计,把构造函数私有化,再写一个唯一的实例,
//再暴露一个instance方法
}
然后在src/server/chatservice.cpp中
#include "chatservice.hpp"
// 获取单例对象的接口函数
ChatService *ChatService::instance()
{
static ChatService service; //这个单例对象service是线程安全的
return &service;
}
3.11 项目不足的地方
3.11.1 加好友问题
加好友的时候应该像QQ那样发送请求,然后等待对方接受再写入
3.11.2 单台服务器关闭时,数据库更新用户在线状态的问题
该项目中用redis的发布-订阅功能进行多台服务器间的通信功能。目前考虑的是所有服务器同时关闭的情况。如若是仅仅关闭一台服务器,那么mysql库的更新用户在线状态的功能是不合理的,如表的结构以及mysql语句所示,一台服务器关闭时会将mysql库中所有用户的在线重置为下线状态。
我的想法是:在user表中增加一列,该列记录的是该次登陆的服务器的ip及端口号,不在线的用户该列可以置为-1。这样当我们仅仅关闭一台服务器时,就可以在数据库中区分开登陆在该服务器中的用户然后选择性得更改其状态。
3.12 项目遇到的问题
3.12.1.离线消息存储
当一个用户有多条离线消息时,登陆成功后只显示一条消息
原因:在设计offlinemessage数据表时对id字段设置为了主键,导致表中不能有重复id,这样使得每个用户只能有一条离线数据。解决也很简单将id字段的主键属性去掉,使得id可以重复。
3.12.2 客户端和服务器 异常退出的问题
(49条消息) C++搭建集群聊天室(十一):客户端 || 服务器 异常退出解决方案_看,未来的博客-CSDN博客
客户端异常推出:此前我们对客户端退出的操作仅仅就是将连接释放掉,没有跟业务联系起来
异常下线原因:没有正常发送json 字符串
现在我们的解决方案是:
-
从用户连接表里 之前用map存储的,找到,然后删除这个用户的链接信息,
-
用户异常推出,相当于下线,根据用户的id在redis中取消订阅通道
-
更新用户的状态信息,设置成离线
void ChatServer::onConnection(const TcpConnectionPtr &conn){
if(!conn->connected()){ //用户断开连接
ChatService::instance()->clientCloseException(conn);
conn->shutdown();
}
}
void ChatService::clientCloseException(const TcpConnectionPtr &conn){
User user;
{
//以conn从哈希表中倒查主键id
lock_guard<mutex> lock(_connMutex);
for(auto it = _userConnMap.begin();it!=_userConnMap.end();){
if(it->second == conn){
//更改用户状态
user.setID(it->first);
//从hash表删除用户信息
_userConnMap.erase(it);
break;
}
else{
it++;
}
}
}
//数据持久化
if(user.getID() != -1){
user.setstate("offline"); //再细一点
_usermodel.updateState(user);
}
}
服务端异常退出:当服务器断开以后,表里边的用户状态还是online,下一次服务器运行的时候,用户去做登录操作的时候,总是报账号已登录
产生的原因:如果我们用ctrl+c强制结束服务器运行的话,就没有机会去修改用户的登录状态,
解决办法:
在main函数中设置信号捕捉
写一个重置方法,把online状态的用户,设置成offline,
update user set state =“offline” where state =“online”;
#include<iostream>
#include<signal.h>
#include "chatserver.hpp"
#include "chatservice.hpp"
using namespace std;
void resetHandler(int){ //这个int就算你不用也加上
ChatService::instance()->reset();
exit(0);
}
int main(){
//捕捉 Ctrl+C 信号
signal(SIGINT,resetHandler);
EventLoop loop;
InetAddress addr("127.0.0.1",7000);
ChatServer server(&loop,addr,"ChatServer");
server.start();
loop.loop();
return 0;
}
3.12.3如何确定用户的身份信息
内存中维护了用户连接表,map保留连接信息+mysql底层维护状态)
专业技能知识点总结
1. C/C++ 容器数据结构 面向对象
1.1 关键字
volatile
1.并行设备的硬件寄存器。
-
一个中断服务程序中修改的供其他程序检测的变量。
volatile提醒编译器,它后面所定义的变量随时都有可能改变。因此编译后的程序每次需要存储或读取这个变量的时候,都会直接从变量地址中读取数据。如果没有volatile关键字 ,则编译器可能优化读取和存储,可能暂时使用寄存器中的值,如果这个变量由别的程序更新了的话,将出现不一致的现象。
-
多线程应用中被几个任务共享的变量。
static
- 在函数体,只会被初始化-次, -个被声明为静态的变量在这一函数被调用过程中维持其值不变。
- 在模块内(但在函数体外) ,一个被声明为静态的变量可以被模块内所用函数访问,但不能被模块外其它函数访问。它是一个本地的全局变量(只能被当前文件使用)。
- 在模块内,一个被声明为静态的函数只可被这一-模块内的其它函数调用。 那就是,这个函数被限制在声明它的模块的本地范围内使用(只能被当前文件使用)。
1.2 STL
map、set
map、set、 multiset、 multimap的底层实现都是红黑树,epol模 型的底层数据结构也是红黑树,linux系统中CFS
进程调度算法,也用到红黑树。
红黑树的特性:
- 每个结点或是红色或是黑色;
- 根结点是黑色;
- 每个叶结点是黑的;
- 如果一个结点是红的,则它的两个儿子均是黑色;
- 每个结点到其子孙结点的所有路径上包含相同数目的黑色结点。
1.3 C++11
1.3.1 右值引用
C++对于左值和右值没有标准定义,但是有一个被广泛认同的说法:
- 可以取地址的,有名字的,非临时的就是左值;
- 不能取地址的,没有名字的,临时的就是右值,表达式结束时就不再存在的临时对象;
右值引用用来绑定到右值,绑定到右值以后本来会被销毁的右值的生存期会延长至与绑定到它的右值引用的生存期。
int &&var = 10;
右值引用的用途:可以修改右值:
// 形参是个右值引用
void change(int&& right_value) {
right_value = 8;
}
int main() {
int a = 5; // a是个左值
int &ref_a_left = a; // ref_a_left是个左值引用
int &&ref_a_right = std::move(a); // ref_a_right是个右值引用
change(a); // 编译不过,a是左值,change参数要求右值
change(ref_a_left); // 编译不过,左值引用ref_a_left本身也是个左值
change(ref_a_right); // 编译不过,右值引用ref_a_right本身也是个左值
change(std::move(a)); // 编译通过
change(std::move(ref_a_right)); // 编译通过
change(std::move(ref_a_left)); // 编译通过
change(5); // 当然可以直接接右值,编译通过
cout << &a << ' ';
cout << &ref_a_left << ' ';
cout << &ref_a_right;
// 打印这三个左值的地址,都是一样的
}
右值引用的存在并不是为了取代左值引用,而是充分利用右值(特别是临时对象)的构造来减少对象构造和析构操作以达到提高效率的目的。
std::move
std::move移动不了什么,唯一的功能是把左值强制转化为右值,让右值引用可以指向左值。其实现等同于一个类型转换:static_cast<T&&>(lvalue),可实现右值引用指向左值
int temp = 666; // 666是个左值
int &ref_temp_left = temp; // 左值引用指向左值
int &&ref_temp_right = std::move(temp); // 通过std::move将左值转化为右值,可以被右值引用指向 move返回的int &&是个右值
std::cout << temp; // 打印结果:666
1.3.2 智能指针
C++里面的四个智能指针: auto_ptr, shared_ptr, weak_ptr, unique_ptr 其中后三个是c++11⽀支持,并且第一个已经被C++11弃用
智能指针的作用是管理理一个指针,因为存在以下这种情况:申请的空间在函数结束时忘记释放,造成内存泄漏。使用智能指针可以很大程度上的避免这个问题,因为智能指针就是一个类,当超出了了类的作用域是,类会自动调用析构函数,析构函数会自动释放资源。所以智能指针的作用原理就是在函数结束时自动释放内存空间,不需要手动释放内存空间。
-
对于 unique_ptr ,实现独占式拥有的概念,同一时间只能有一个智能指针可以指向该对象,因为无法进行拷贝构造和拷贝赋值,但是可以进行移动构造和移动赋值;
-
对于 shared_ptr ,实现共享式拥有的概念,即多个智能指针可以指向相同的对象,该对象及相关资源会在
其所指对象不再使用之后,自动释放与对象相关的资源;它使⽤用计数机制来表明资源被几个指针共享。可以通过成员函数use_count()来查看资源的所有者个数。除了了可以通过new来构造,还可以通过传⼊入auto_ptr,unique_ptr,weak_ptr来构造。当我们调⽤用release()时,当前指针会释放资源所有权,计数减⼀一。当计数等于0时,资源会被释放
-
对于 weak_ptr ,解决 shared_ptr 相互引用时,两个指针的引用计数永远不会下降为0,从而导致死锁问
题。而 weak_ptr 是对象的一种弱引用,可以绑定到 shared_ptr ,但不会增加对象的引用计数。
shared_ptr是如何实现的?
- 构造函数中计数初始化为1;
- 拷贝构造函数中计数值加1;
- 赋值运算符中,左边的对象引用计数减1,右边的对象引用计数加1;
- 析构函数中引用计数减1;
- 在赋值运算符和析构函数中,如果减1后为0,则调用 delete 释放对象。
1.4 C语言
1.4.1指针
作者:安和ahe
链接:https://www.nowcoder.com/discuss/1020406
来源:牛客网
1.5 面向对象
1.5.1 多态
C++如何实现多态?
C++中通过虚函数实现多态。虚函数的本质就是通过基类指针访问派生类定义的函数。每个含有虚函数的类,其实例对象内部都有一个虚函数表指针。该虚函数表指针被初始化为本类的虚函数表的内存地址。所以,在程序中,不管对象类型如何转换,该对象内部的虚函数表指针都是固定的,这样才能实现动态地对对象函数进行调用,这就是C++多态性的原理
动态绑定是如何实现的?
当编译器器发现类中有虚函数时,会创建一张虚函数表,把虚函数的函数⼊口地址放到虚函数表中,并且在对象中增加一个指针 vptr ,用于指向类的虚函数表。当派⽣生类覆盖基类的虚函数时,会将虚函数表中对应的指针进行替换,从而调用派生类中覆盖后的虚函数,从而实现动态绑定。
虚函数表是针对类的,类的所有对象共享这个类的虚函数表,因为每个对象内部都保存一个指向该类虚函数表的指针 vptr ,每个对象的 vptr 的存放地址都不不同,但都指向同一虚函数表。
2. 计算机网络
2.1 三次握手、四次挥手
三次握手过程
最初两端的TCP进程都处于CLOSED关闭状态,A主动打开连接,而B被动打开连接。B的TCP服务器进程先创建传输控制块TCB,准备接受客户进程的连接请求。然后服务器进程就处于LISTEN(收听)状态,等待客户的连接请求。若有,则作出响应。
第一次握手:起初两端都处于CLOSED关闭状态,:A的TCP客户进程也是首先创建传输控制块TCB,然后向B发出连接请求报文段,Client将标志位SYN置为1,随机产生一个值seq=x,并将该数据包发送给Server,Client进入SYN-SENT状态,等待Server确认;
第二次握手:Server收到数据包后由标志位SYN=1得知Client请求建立连接,Server将标志位SYN和ACK都置为1,ack=x+1,随机产生一个值seq=y,并将该数据包发送给Client以确认连接请求,Server进入SYN-RCVD状态,此时操作系统为该TCP连接分配TCP缓存和变量;
第三次握手:Client收到确认后,检查ack是否为x+1,ACK是否为1,如果正确则将标志位ACK置为1,ack=y+1,并且此时操作系统为该TCP连接分配TCP缓存和变量,并将该数据包发送给Server,Server检查ack是否为y+1,ACK是否为1,如果正确则连接建立成功,Client和Server进入ESTABLISHED状态,完成三次握手,随后Client和Server就可以开始传输数据。
2.1.1为什么三次握手中客户端还要发送一次确认呢?可以二次握手吗?
答:主要为了防止已失效的连接请求报文段突然又传送到了B,因而产生错误。如A发出连接请求,但因连接请求报文丢失而未收到确认,于是A再重传一次连接请求。后来收到了确认,建立了连接。数据传输完毕后,就释放了连接,A工发出了两个连接请求报文段,其中第一个丢失,第二个到达了B,但是第一个丢失的报文段只是在某些网络结点长时间滞留了,延误到连接释放以后的某个时间才到达B,此时B误认为A又发出一次新的连接请求,于是就向A发出确认报文段,同意建立连接,不采用三次握手,只要B发出确认,就建立新的连接了,此时A不理睬B的确认且不发送数据,则B一致等待A发送数据,浪费资源。
小林coding 网站上的解答:
TCP 建立连接时,通过三次握手能防止历史连接的建立,能减少双方不必要的资源开销,能帮助双方同步初始化序列号。序列号能够保证数据包不重复、不丢弃和按序传输。
不使用「两次握手」和「四次握手」的原因:
「两次握手」:无法防止历史连接的建立,会造成双方资源的浪费,也无法可靠的同步双方序列号;
「四次握手」:三次握手就已经理论上最少可靠连接建立,所以不需要使用更多的通信次数。
四次挥手
刚开始双方都处于 ESTABLISHED 状态,假如是客户端先发起关闭请求。四次挥手的过程如下:
- 第一次挥手:客户端发送一个 FIN 报文,报文中会指定一个序列号。此时客户端处于
FIN_WAIT1
状态。 即发出连接释放报文段(FIN=1,序号seq=u),并停止再发送数据,主动关闭TCP连接,进入FIN_WAIT1(终止等待1)状态,等待服务端的确认。 - 第二次挥手:服务端收到 FIN 之后,会发送 ACK 报文,且把客户端的序列号值 +1 作为 ACK 报文的序列号值,表明已经收到客户端的报文了,此时服务端处于
CLOSE_WAIT
状态。 即服务端收到连接释放报文段后即发出确认报文段(ACK=1,确认号ack=u+1,序号seq=v),服务端进入CLOSE_WAIT(关闭等待)状态,此时的TCP处于半关闭状态,客户端到服务端的连接释放。客户端收到服务端的确认后,进入FIN_WAIT2(终止等待2)状态,等待服务端发出的连接释放报文段。 - 第三次挥手:如果服务端也想断开连接了,和客户端的第一次挥手一样,发给 FIN 报文,且指定一个序列号。此时服务端处于
LAST_ACK
的状态。 即服务端没有要向客户端发出的数据,服务端发出连接释放报文段(FIN=1,ACK=1,序号seq=w,确认号ack=u+1),服务端进入LAST_ACK(最后确认)状态,等待客户端的确认。 - 第四次挥手:客户端收到 FIN 之后,一样发送一个 ACK 报文作为应答,且把服务端的序列号值 +1 作为自己 ACK 报文的确认号值,此时客户端处于
TIME_WAIT
状态。需要过一阵子以确保服务端收到自己的 ACK 报文之后才会进入 CLOSED 状态,服务端收到 ACK 报文之后,就处于关闭连接了,处于CLOSED
状态。 即客户端收到服务端的连接释放报文段后,对此发出确认报文段(ACK=1,seq=u+1,ack=w+1),客户端进入TIME_WAIT(时间等待)状态。此时TCP未释放掉,需要经过时间等待计时器设置的时间2MSL后,客户端才进入CLOSED状态。
收到一个FIN只意味着在这一方向上没有数据流动。客户端执行主动关闭并进入TIME_WAIT是正常的,服务端通常执行被动关闭,不会进入TIME_WAIT状态。
在socket编程中,任何一方执行close()操作即可产生挥手操作
3. 网络编程
3.1 select poll epoll优缺点
select:
对于 select 这种方式,需要进行 2 次「遍历」文件描述符集合,一次是在内核态里,一个次是在用户态里 ,而且还会发生 2 次「拷贝」文件描述符集合,先从用户空间传入内核空间,由内核修改后,再传出到用户空间中。
select 使用固定长度的 BitsMap,表示文件描述符集合,默认最大值为 1024
,只能监听 0~1023 的文件描述符
poll:
poll 不再用 BitsMap 来存储所关注的文件描述符,取而代之用动态数组,以链表形式来组织,突破了 select 的文件描述符个数限制,当然还会受到系统文件描述符限制。
epoll:
第一点,epoll 在内核里使用红黑树来跟踪进程所有待检测的文件描述字
第二点, epoll 使用事件驱动的机制,内核里维护了一个链表来记录就绪事件,当某个 socket 有事件发生时,通过回调函数内核会将其加入到这个就绪事件列表中,当用户调用 epoll_wait()
函数时,只会返回有事件发生的文件描述符的个数,不需要像 select/poll 那样轮询扫描整个 socket 集合,大大提高了检测的效率。
4.驱动编程
应用程序执行open、ioctl等系统调用,它们的参数和驱动程序中相应函数的参数不是一一对应的,其中经过了内核文件系统层的转换
4.1固件
固件是指设备内部保存的设备“驱动程序”,通过固件,操作系统才能按照标准的设备驱动实现特定机器的运行动作
5 单片机相关
1. 通信协议
1.1总线接口USRT、I2C、USB的异同点(串/并、速度、全/半双工、总线拓扑等)
解析:这个题目是最常问到的之一,需要记得。
UART、I2C、SPI、USB的异同点:
•UART:通用异步串行口,速率不快,可全双工,结构上一般由波特率产生器、UART发送器、UART接收器组成,硬件上两线,一收一发;
•I2C:双向、两线、串行、多主控接口标准。速率不快,半双工,同步接口,具有总线仲裁机制,非常适合器件间近距离经常性数据通信,可实现设备组网;
•SPI:高速同步串行口,高速,可全双工,收发独立,同步接口,可实现多个SPI设备互联,硬件3~4线;
•USB通用串行总线,高速,半双工,由主机、hub、设备组成。设备可以与下级hub相连构成星型结构。
这个表要多看看,都重要,都有问过。
1.2 I2C时序图
参考链接:(50条消息) I2C通信全面解析_陶通宁的博客-CSDN博客_i2c通信的详细讲解
SCL为高电平时,SDA由高变低表示起始信号
SCL为高电平时,SDA由低变高表示停止信号
起始信号和停止信号都是由主机发出,起始信号产生后总线处于占用状态
停止信号产生后总线处于空闲状态
分为四步:
1、将SCL电平拉低
2、若应答,则SDA电平设为高,反之,则SDA电平设为低
3、然后将SCL电平拉高,此时数据稳定 EEPROM就可以读取SDA的数据
4、再将SCL电平拉低,表示让对方发出下一个数据
下面的曲线图要会画出来哦!!!
1.3 SPI
-
MOSI (Master Output, Slave Input)
-
MISO(Master Input,, Slave Output)
-
SCLK (Serial Clock)
-
SS( Slave Select)
每个从设备都有独立的这一条 NSS 信号线,本信号线独占主机的一个引脚,即有多少个从设备,就有多少条片选信号线 ,I2C 协议中通过设备地址来寻址、选中总线上的某个设备并与其进行通讯;而 SPI 协议中没有设备地址,它使用 NSS 信号线来寻址,当主机要选择从设备时,把该从设备的 NSS 信号线设置为低电平,该从设备即被选中,即片选有效,接着主机开始与被选中的从设备进行 SPI 通讯。所以SPI 通讯以 NSS 线置低电平为开始信号,以 NSS 线被拉高作为结束信号。
2.中断
2.1 外部中断的一般配置步骤
① 使能IO口时钟。
② 初始化IO口,设置触发方式:HAL_GPIO_Init();
③ 设置中断优先级,并使能中断通道。
④ 编写中断服务函数:
函数中调用外部中断通用处理函数HAL_GPIO_EXTI_IRQHandler。
void EXTI3_IRQHandler(void)
{
if(EXTI_GetITStatus(EXTI_Line3)!=RESET)//判断某个线上的中断是否发生
{ …中断逻辑…
EXTI_ClearITPendingBit(EXTI_Line3); //清除 LINE 上的中断标志位
}
}
⑥ 编写外部中断回调函数:HAL_GPIO_EXTI_Callback;
2.2 中断为什么要区分上半部和下半部
Linux中断分为硬件中断和内部中断(异常),调用过程:外部中断产生->发送中断信号到中断控制器->
通知处理器产生中断的中断号,让其进一步处理。
对于中断上半部和下半部的产生,为了中断处理过程中被新的中断打断,将中断处理一分为二,上半部
登记新的中断,快速处理简单的任务,剩余复杂耗时的处理留给下半部处理,下半部处理过程中可以被
中断,上半部处理时不可被中断
2.3 中断处理“下半部”机制
中断服务程序一般都是在中断请求关闭的条件下执行的,以避免嵌套而使中断控制复杂化。但是,中断是一个随机事件,它随时会到来,如果关中断的时间太长,CPU就不能及时响应其他的中断请求,从而造成中断的丢失。因此,Linux内核的目标就是尽可能快的处理完中断请求,尽其所能把更多的处理向后推迟;
因此,内核把中断处理分为两部分:上半部(top-half)和下半部(bottom-half),上半部 (就是中断服务程序)内核立即执行,而下半部(就是一些内核函数)留着稍后处理。
首先:一个快速的“上半部”来处理硬件发出的请求,它必须在一个新的中断产生之前终止。
第二:“下半部”运行时是允许中断请求的,而上半部运行时是关中断的,这是二者之间的主要区别。
内核到底什么时候执行下半部,以何种方式组织下半部?
这就是我们要讨论的下半部实现机制,这种机制在内核的演变过程中不断得到改进,在以前的内核中,这个机制叫做bottom-half(以下简称BH)。但是,Linux的这种bottom-half机制有两个缺点:
- 在任意一时刻,系统只能有一个CPU可以执行BH代码,以防止两个或多个CPU同时来执行BH函数而相互干扰。因此BH代码的执行是严格“串行化”的。
- BH函数不允许嵌套
下面主要介绍3种2.6内核中的“下半部”处理机制:
-
软中断请求(softirq)机制
-
小任务(tasklet)机制
-
工作队列机制
工作队列(work queue)是另外一种将工作推后执行的形式,它和前面讨论的tasklet有所不同。工作队列可以把工作推后,交由一个内核线程去执行,也就是说,这个下半部分可以在进程上下文中执行。这样,通过工作队列执行的代码能占尽进程上下文的所有优势。最重要的就是工作队列允许被重新调度甚至是睡眠。
那么,什么情况下使用工作队列,什么情况下使用tasklet。如果推后执行的任务需要睡眠,那么就选择工作队列;如果推后执行的任务不需要睡眠,那么就选择tasklet。另外,如果需要用一个可以重新调度的实体来执行你的下半部处理,也应该使用工作队列。它是唯一能在进程上下文运行的下半部实现的机制,也只有它才可以睡眠。这意味着在需要获得大量的内存时、在需要获取信号量时,在需要执行阻塞式的I/O操作时,它都会非常有用。如果不需要用一个内核线程来推后执行工作,那么就考虑使用tasklet。
(58条消息) 中断处理“下半部”机制_Arrow的博客-CSDN博客
2.4 中断的注意事项
单片机中断
- 中断函数没有返回值,因为中断服务函数的触发是随机的,不可调用,要是有返回值的话,就存在堆栈的问题,但是没有具体的一个内存来存放返回值,会造成混乱,所以不行;
- 中断函数不能进行参数传递
- 在任何情况下都不能直接调用中断函数
- 中断函数使用浮点运算要保存浮点寄存器的状态。
- 如果在中断函数中调用了其它函数,则被调用函数所使用的寄存器必须与中断函数相同,被调函数最好设置为可重入的。
- 在编写中断子程序的时候,应当巧用全局状态变量,在中断子程序中只改变状态变量值
- 中断服务程序的设计对系统的成败有至关重要的作用,要仔细考虑各中断之间的关系和每个中断执行的时间,特别要注意那些对同一个数据进行操作的中断
- 在许多的处理器/编译器中,浮点一般都是不可重入的。有些处理器/编译器需要让额处的寄存器入栈,有些处理器/编译器就是不允许在ISR中做浮点运算。此外,ISR应该是短而有效率的,在ISR中做浮点运算是不明智的。
- 与第三点一脉相承,printf()经常有重入和性能上的问题。
Linux中断
-
【不可以执行耗时的任务】代码执行效率尽量高,代码一定要简洁,尽量少调用其他函数,尽量避免增加中断的执行时长,中断处理需要快进快出。
- 不可以使用耗时很长的函数,因为中断会关闭调度,中断的优先级高于任何任务的优先级,长时间的中断处理会影响到系统的响应速度,使整个系统的任务无法政策运行,造成很多的任务超时,容易导致很多不可预知的后果。【可以使用中断底半部解决】
- 对于一些必须的操作可以放在中断中处理,其他的以异步执行的方式,放到中断底半部中解决。
-
【不可以睡眠或者放弃CPU】中断处理程序中不可以睡眠,不可以使用可能引起睡眠的函数,也不可以使用可能会引起睡眠的锁。
- 不可以使用metux等引起休眠的锁。可以使用自旋锁,但必须保证自旋等待时间足够短。【可以使用自旋锁解决】
- 不可以使用会引起休眠的函数。比如ssleep(), msleep(), kmalloc, copy_to_user(), copy_from_user() 等。
-
【中断是不可重入函数】同一时刻同一中断只能在一个CPU上被触发,中断被响应后会被关闭该中断,相同的中断处理函数不能同时在多个处理器上运行。
为什么在中断里不可以睡眠?
从系统调度的角度来说,调度的触发方式有几种(主动触发、睡眠、唤醒、中断退出时的判断),而在进入中断之前,系统调度会被关闭。在调度函数schedule()中,也会首先判断是否在中断上下文,如果是则直接退出。此时一旦睡眠或者放弃CPU,这时内核无法调度别的进程来执行,系统就会死掉。
为什么在中断里不可以使用耗时很长的函数?
因为中断会关闭调度,中断的优先级高于任何任务的优先级,长时间的中断处理会影响到系统的响应速度,使整个系统的任务无法政策运行,造成很多的任务超时,容易导致很多不可预知的后果。【可以使用中断下半部解决】
原文链接:https://blog.csdn.net/Ivan804638781/article/details/116244716
3. 单片机、嵌入式编程
3.1 判断大小端
#include <stdio.h>
int checkCPU()
{
{
union w
{
int a;
char b;
}c;
c.a =1;
return(c.b == 1);
}
}
int main()
{
if(checkCPU())
printf("小端\n");
else
printf("大端\n");
return 0;
}
6 数据结构与算法
1.二叉树
1.1 红黑树与二叉平衡查找树(AVL)B树 B+树
原文链接:https://blog.csdn.net/u010899985/article/details/80981053
AVL:
AVL树是带有平衡条件的二叉查找树,一般是用平衡因子差值判断是否平衡并通过旋转来实现平衡,左右子树树高不超过1,和红黑树相比,AVL树是严格的平衡二叉树,平衡条件必须满足(所有节点的左右子树高度差的绝对值不超过1)。不管我们是执行插入还是删除操作,只要不满足上面的条件,就要通过旋转来保持平衡,而旋转是非常耗时的,由此我们可以知道AVL树适合用于插入与删除次数比较少,但查找多的情况
红黑树:
一种二叉查找树,但在每个节点增加一个存储位表示节点的颜色,可以是红或黑(非红即黑)。通过对任何一条从根到叶子的路径上各个节点着色的方式的限制,红黑树确保没有一条路径会比其它路径长出两倍,因此,红黑树是一种弱平衡二叉树(由于是弱平衡,可以看到,在相同的节点情况下,AVL树的高度低于红黑树),相对于要求严格的AVL树来说,它的旋转次数少,所以对于搜索,插入,删除操作较多的情况下,我们就用红黑树。
红黑树性质:
-
每个节点非红即黑
-
根节点是黑的;
-
每个叶节点(叶节点即树尾端NULL指针或NULL节点)都是黑的;
-
如图所示,如果一个节点是红的,那么它的两儿子都是黑的;
-
对于任意节点而言,其到叶子点树NULL指针的每条路径都包含相同数目的黑节点;
-
每条路径都包含相同的黑节点;
红黑树应用
1,广泛用于C ++的STL中,地图和集都是用红黑树实现的;
2,着名的Linux的的进程调度完全公平调度程序,用红黑树管理进程控制块,进程的虚拟内存区域都存储在一颗红黑树上,每个虚拟地址区域都对应红黑树的一个节点,左指针指向相邻的地址虚拟存储区域,右指针指向相邻的高地址虚拟地址空间;
3,IO多路复用的epoll的的的实现采用红黑树组织管理的的的sockfd,以支持快速的增删改查;
4,Nginx的的的中用红黑树管理定时器,因为红黑树是有序的,可以很快的得到距离当前最小的定时器;
B树
多叉树,
以一颗最大度数(max-degree)为5(5阶)的b-tree为例,那这个B树每个节点最多存储4个key,5个指针:
在B树中,非叶子节点和叶子节点都会存放数据。
在B树中,非叶子节点和叶子节点都会存放数据。
B+树
常用于数据库和操作系统的文件系统中
最终我们看到,B+Tree 与 B-Tree相比,主要有以下三点区别:
- 所有的数据都会出现在叶子节点。
- 叶子节点形成一个单向链表。
- 非叶子节点仅仅起到索引数据作用,具体的数据都是在叶子节点存放的。
B±tree的查询效率更加稳定
B树在元素遍历的时候效率较低