- c语言中const *p的用法
(1)const int *p;
或 int const *p;
指向常量整数的指针,通过这个指针不能修改它所指向的整数值,但可以修改指针本身来指向其他地址
cpp
const int a = 10;
const int *p = &a;
// *p = 20; // 错误:不能通过*p修改a的值
int b = 20;
p = &b; // 合法:可以修改指针p,使其指向其他地址
(2)int *const p;
常量指针,指向一个整数。指针本身是常量,不能修改,但可以通过指针修改它所指向的整数值
cpp
int a = 10;
int *const p = &a;
*p = 20; // 合法:可以通过p修改a的值
// p = &b; // 错误:不能修改指针p本身
(3)const int *const p;
表示一个指向常量整数的常量指针,既不能修改指针所指向的地址,也不能通过指针修改它所指向的整数值
cpp
const int a = 10;
const int *const p = &a;
// *p = 20; // 错误:不能通过*p修改a的值
// p = &b; // 错误:不能修改指针p本身
- TCP/IP的四层模型是什么?
(1)应用层
应用层是TCP/IP协议栈的顶层,它为应用程序提供网络服务接口。该层协议包括HTTP、FTP、SMTP、Telnet等,负责与用户直接交互,处理特定的应用程序任务
- HTTP(HyperText Transfer Protocol):用于网页浏览。
- FTP(File Transfer Protocol):用于文件传输。
- SMTP(Simple Mail Transfer Protocol):用于电子邮件传输。
- DNS(Domain Name System):用于域名解析。
(2)传输层
传输层负责在两个主机之间建立、维护和终止传输会话,并保证数据包的可靠传输。该层主要包含TCP和UDP协议
- TCP(Transmission Control Protocol):提供可靠的、有序的、面向连接的传输服务。它通过三次握手建立连接,确保数据包按序到达,并通过确认机制保证数据的正确传输。
- UDP(User Datagram Protocol):提供无连接、不可靠的传输服务,适用于对速度要求高但对可靠性要求低的场景,如实时视频传输。
(3)网络层
网络层负责将数据包从源传输到目的地,主要通过IP协议来实现。该层还包括路由选择、逻辑地址管理等功能
- IP(Internet Protocol):提供数据包的寻址和路由功能,是整个互联网的基础。IP协议分为IPv4和IPv6两个版本。
- 其他相关协议包括ICMP(Internet Control Message Protocol)和ARP(Address Resolution Protocol)
(4)数据链路层
数据链路层负责在相邻节点之间传输数据帧,提供物理地址寻址、帧同步、错误检测和纠正等功能。该层协议包括Ethernet、PPP(Point-to-Point Protocol)
- Ethernet(以太网):是局域网中最常用的数据链路层协议。
- PPP(Point-to-Point Protocol):用于点对点连接,常用于电话拨号连接和一些宽带连接。
- 数据结构的框架
(1)线性数据结构(一对一)
线性数据结构指的是数据元素按顺序排列,每个元素只有一个前驱和一个后继,常见的线性数据结构有:
-
数组
-
链表
(1)单向链表
(2)双向链表
(3)循环链表
-
栈(先进后出)
-
队列(先进先出)
(2)树型结构(一对多)
-
二叉树
-
二叉搜索树
-
平衡树
-
红黑树
(3)图形结构(多对多网状结构)
(4)哈希表
- 链表的作用
(1)动态分配内存
链表在需要频繁插入和删除操作的场景中表现优越。由于链表不需要预先分配固定大小的内存,可以根据需要动态增加和减少节点,节省内存
(2)实现元素的灵活插入和删除
(3)实现抽象数据类型(如栈、队列等)
- 链表的应用场景
(1)实现栈和队列的数据结构
单向链表实现入栈出栈、入队出队的操作
(2)在编译器中实现符号表或字典的实现
(3)处理大量未知大小的数据集
当需要处理的数据集较大且大小无法预知时,链表的动态内存分配优势显现出来,特别是频繁的插入和删除操作
(4)实现哈希表
(5)浏览器网页的前进和后退
- 数据库的流程
数据库是一套用于存储、管理和检索数据的系统。数据库的流程可以分为这几个步骤:包括设计、创建、操作、优化和维护。
(1)数据库的设计
- 需求分析
(1)确定系统需求,包括数据存储、检索、更新和管理需求
(2)与用户、业务分析师和其他相关方进行交流,了解业务流程和数据需求
-
概念设计
-
逻辑设计
-
物理设计
(2)数据库创建
-
创建数据库
-
创建表和约束
(3)数据操作
-
数据插入
-
数据查询
-
数据更新
-
数据删除
(4)数据库优化
-
查询优化
-
索引管理
-
性能监测
(5)数据库维护
-
备份和恢复
-
数据库安全
-
数据库迁移
(6)数据库管理
-
用户管理
-
日志管理
-
数据库调整
-
目录操作流程
-
c语言中#ifndef的作用
(1)防止重复定义
在大型项目中,头文件可能会被多个源文件包含,使用#ifndef可以防止多次包含同一个头文件而导致的编译错误
当一个头文件被多次包含时,可能会导致重复定义的错误。#ifndef和#define一起使用可以确保头文件内容只被包含一次
(2)条件编译
根据编译环境的不同,选择性编译代码
可以用来根据条件编译特定的代码片段。例如,在不同的编译环境中包含不同的代码
- c语言中头文件#include "stdio.h" 和 #include <stdio.h>中""和<>有啥区别
(1)双引号#include "header.h"包含项目中自定义的头文件
编译器会先在当前目录中搜索头文件,如果找不到再去系统目录中搜索
(2)尖括号#include <header.h>包含标准库头文件或第三方库头文件
编译器只会在系统标准包含路径中搜索头文件
- 熟悉哪些协议?下载文件常用的协议?
(1)MODBUS协议
(2)HTTP协议
(3)FTP协议
(4)TCP/IP协议
(5)UDP协议
(6)下载文件常用的协议
- HTTP
通过浏览器或下载管理器来下载网页资源、文档、软件等
- FTP
通常用于下载大型文件或从特定服务器下载
- 设计一个点对点通信的软件,选择TCP还是UDP,为什么?
(1)TCP(传输控制协议)
- 特点
(1)可靠性强
(2)流量控制
根据网络状况调整数据发送速率,防止网络阻塞
(3)连接建立
TCP需要建立连接(三次握手),有一定延迟
- 适用场景
(1)保证文件完整性的文件传输
(2)消息传递
需要确保消息有序且完整到达的场景,如聊天应用程序
(2)UDP(用户数据报协议)
- 特点
(1)无连接
(2)不可靠传输
(3)资源开销小
- 适用场景
(1)实时的音视频传输:容忍数据丢失
(2)在线游戏
(3)广播通信
(3)选择理由
-
若通信软件需要可靠的消息传输应该选用TCP
-
若通信软件强调实时性并且能够容忍数据的丢包和乱序,并且需要低延迟,则选择UDP
-
排序算法总结(以数组举例)
(1)冒泡排序(时间复杂度O(n^2)、排序稳定)
- 原理
重复遍历要排序的数组,比较相邻的元素大小,如果相邻元素的的顺序错误,交换元素的位置,直到数组变为顺序结构为之
- 代码
cpp
void BubbleSort(int array[], int len)
{
int i = 0;
int j = 0;
int tmp = 0;
for (i = 0; i < len - 1; ++i)
{
for (j = 0; j < len - 1 - i; ++j)
{
if (array[j] > array[j+1])
{
tmp = array[j];
array[j] = array[j+1];
array[j+1] = tmp;
}
}
}
}
(2)插入排序(时间复杂度O(n^2)、排序稳定)
- 原理
将数组分为已排序和未排序两部分,每次从未排序部分取出一个元素,将其插入到已排序部分中的合适位置,直到所有的元素都被排序完成。
- 代码
cpp
void InsertSort(int array[], int len)
{
int i = 0;
int j = 0;
int tmp = 0;
for (i = 0; i < len; ++i)
{
tmp = array[i];//暂时存储当前要插入的值
j = i;
while (j > 0 && a[j-1] > tmp)//判断要插入的值是否小于要比较的值
{
a[j] = a[j-1];//把大的数值向后移
--j;
}
a[j] = tmp;//找到合适的位置
}
}
(3)选择排序(时间复杂度O(n^2)、排序不稳定)
- 原理
每次从未排序部分找到最小(最大)的元素放入已排序部分的末尾,直到所有元素排序完成
- 代码
cpp
void SelectSort(int array[], int len)
{
int i = 0;
int j = 0;
int tmp = 0;
int minpos = 0;
for (i = 0; i < len-1; ++i)
{
minpos = i;
for (j = i+1; j < len; ++j)//在未排序数组中寻找最小值
{
if (array[minpos] > array[j])
{
minpos = j;//如果当前最小值大于未排序数组元素中的值,更新最小值下标
}
}
if (minpos != i)//如果最小值下标位置已经更新了
{
tmp = array[i];
array[i] = array[minpos];
array[minpos] = tmp;
}
}
}
- 选择排序不稳定的例子
cpp
数组: [4, 5, 3, 5, 2]
↑ ↑
初始索引:0 1 2 3 4
cpp
[2, 5, 3, 5, 4]
cpp
[2, 3, 5, 5, 4]
cpp
[2, 3, 4, 5, 5]
此时两个5之间的相对前后位置发生了变化
(4)快速排序(时间复杂度O(n*logn)、排序不稳定)
采用分治法,将一个数组分成较小的子数组,然后递归排序这些子数组。
(1)选取基准元素
(2)通过基准值,将数组划分排列,比基准值小的元素放在基准元素的左边,比基准值大的元素放在基准值的右边
(3)递归对基准值左边和右边的数组进行上述操作,直到子数组大小为0或一个,结束排序
- 原理图解
(1)首先从最左边选择1为基准值,i 和 j作为位置指针
(2)将基准值先与j位置数值进行比较,1>0,基准值左边比基准值小,基准值右边比基准值大,所以将j位置数值赋值给i指针位置的,覆盖1,若发生数值赋值操作,固定赋值方的指针,转向被赋值指针处继续与基准值进行比较
(3)从i开始,0<1,i向前走,2>1,大于基准值的值放在基准值后面,将i处的值2赋值给j处的0,固定i位置,转向j继续进行比较
(4)重复上述操作,直到i与j相遇,将基准值赋值给i处,完成第一轮排序
(5)之后展按照上述规则递归处理,直到分割的数组只有一个或零个元素完成排序
- 代码
cpp
#include <stdio.h>
#include <string.h>
void print_array(int *a, int len)
{
int i = 0;
for (i = 0; i < len; i++)
{
printf("%d ", a[i]);
}
printf("\n");
}
void quick_sort(int *a, int begin, int end)
{
if (begin >= end)
{
return;
}
int i = begin;
int j = end;
int key = a[i];//基准值
while (i < j)
{
while (i < j && a[j] >= key)
{
--j;
}
a[i] = a[j];
while (i < j && a[i] <= key)
{
++i;
}
a[j] = a[i];
}
a[i] = key;//遍历一轮
quick_sort(a, begin, i-1);
quick_sort(a, i+1, end);
}
int main(void)
{
int str[] = {1, 2 , -3, -4, 5, 6, 7, -8, 9, 0};
int len = sizeof(str) / sizeof(str[0]);
quick_sort(str, 0, len-1);
print_array(str, len);
return 0;
}
(5)希尔排序
- 数据库类型
(1)关系型数据库(RDBMS)
- MySQL
(1)特点
开源、易于使用、高性能、
(2)应用场景
Web开发、电子商务
- SQLite
(1)特点
轻量级、适用嵌入式、无需服务器
(2)应用场景
移动应用、嵌入式系统、
- Microsoft SQL Server
(1)特点
商业化、与Windows生态系统集成度高
(2)应用场景
企业应用
(2)非关系型数据库(NoSQL)
- MongoDB
(1)特点
文档数据库、、基于JSON的存储、扩展性强
(2)应用场景
实时分析、大数据应用、物联网(loT)
(3)区别
- 数据模型
(1)关系型数据库采用表格的关系模型、适合结构化数据和复杂查阅
(2)非关系型数据库采用键值对、文档、列族或图的形式,适合灵活的数据模型
- 扩展性
(1)关系型数据库一般纵向扩展
(2)非关系型数据库一般横向扩展
- 查询能力
(1)关系型数据库适用SQL进行复杂查询和操作
(2)非关系型数据库提供灵活的查询语言和API接口
- 链表头插尾插哪种效率高
(1)需要频繁在头部插入节点而不在乎节点顺序,头插法更高效
(2)如果要保持节点插入顺序或实现队列,尾插法更合适
- 链表查重结束的条件?
(1)遍历到链表末尾,下一个节点指向NULL
(2)在遍历过程中发现重复节点
- 如何调试段错误?
(1)GDB调试
- 在编译代码时加入-g选项
gcc filename.c -g
- 使用gdb调试生成代码
gdb a.out
- GDB调试命令
l 查看代码
b 函数名/行号 设置断点
r 运行代码
n 单步运行
c 直接运行到下一处断点
s 进入函数内部调试
p 变量名 查看变量对应值
q 退出
(2)打印法调试
在程序可能出现段错误的位置加入打印,前一句能够打印出来,后一句打印不出来,问题就可以定位到两次打印中间的代码部分
(3)core文件调试法
-
配置core文件
-
ulimit -c unlimited
-
编译代码加入-gxuanx
gcc filename.c -o filename
-
运行代码使其产生段错误,段错误产生后会生成一个包含错误信息的core文件
-
gdb a.out core能够找到错误产生的位置
-
数据结构对编程的帮助有哪些
(1)组织和管理数据
(2)提高算法效率
(3)简化代码实现
(4)解决特定问题
(5)增强代码可读性和维护性
- 常用链表的适用场景?
(1)单向链表
-
实现队列(FIFO)
-
动态数据集合:例如动态管理学生名单或动态更新的购物车
(2)双向链表
-
双向遍历
-
编辑器的撤销操作
(3)循环链表
-
约瑟夫回环问题
-
时钟算法
-
熟悉哪些协议?下载时用什么协议
(1)HTTP协议
(2)TCP/IP协议
(3)FTP协议
(4)MODBUS协议
(5)文件下载时常用HTTP/HTTPS协议
- 栈区存放什么?栈区的大小?
(1)栈区存储的数据
-
局部变量:函数内定义的变量
-
传递给函数的参数
-
函数调用后需要范围的地址
-
临时变量:编译器在生成代码时使用的一些临时数据
-
栈指针
(2)栈区大小(可以修改默认值)
-
Windows:通常1MB
-
Linux:通常8MB
-
数据区放什么?数据区的大小?
(1)数据区存放的数据
-
已经初始化的静态变量和全局变量
-
未初始化的静态变量和全局变量
-
字符串常量
(2)数据区大小
由操作系统动态管理和分配
(3)特点
-
变量未初始化时默认为0
-
变量在程序编译时开辟空间,程序结束时回收空间
-
linux中的常用命令
文件和目录管理
-
ls
:列出目录内容。sh复制代码
ls -l # 以长格式列出目录内容
-
cd
:改变当前目录。sh复制代码
cd /path/to/directory # 切换到指定目录
-
pwd
:显示当前工作目录的绝对路径。sh复制代码
pwd # 显示当前目录
-
cp
:复制文件或目录。sh复制代码
cp source_file destination_file # 复制文件 cp -r source_directory destination_directory # 复制目录
-
mv
:移动或重命名文件或目录。sh复制代码
mv old_name new_name # 重命名文件或目录 mv file_name /path/to/destination # 移动文件
-
rm
:删除文件或目录。sh复制代码
rm file_name # 删除文件 rm -r directory_name # 删除目录
-
mkdir
:创建新目录。sh复制代码
mkdir new_directory # 创建新目录
-
rmdir
:删除空目录。sh复制代码
rmdir empty_directory # 删除空目录
文件内容查看和编辑
-
cat
:连接文件并显示内容。sh复制代码
cat file_name # 显示文件内容
-
more
:逐屏查看文件内容。sh复制代码
more file_name # 逐屏显示文件内容
-
less
:逐屏查看文件内容,支持前后翻页。sh复制代码
less file_name # 逐屏显示文件内容
-
head
:显示文件开头部分内容。sh复制代码
head file_name # 显示文件的前10行 head -n 20 file_name # 显示文件的前20行
-
tail
:显示文件末尾部分内容。sh复制代码
tail file_name # 显示文件的后10行 tail -n 20 file_name # 显示文件的后20行 tail -f file_name # 动态显示文件的新内容(如日志文件)
-
nano
:简易文本编辑器。sh复制代码
nano file_name # 使用nano编辑文件
-
vim
:功能强大的文本编辑器。sh复制代码
vim file_name # 使用vim编辑文件
系统管理
-
top
:实时显示系统性能和进程信息。sh复制代码
top # 实时显示系统信息
-
ps
:显示当前进程信息。sh复制代码
ps aux # 显示所有进程信息
-
kill
:终止进程。sh复制代码
kill process_id # 终止指定进程 kill -9 process_id # 强制终止指定进程
-
df
:显示文件系统磁盘空间使用情况。sh复制代码
df -h # 以人类可读的格式显示磁盘使用情况
-
du
:显示文件和目录的磁盘使用情况。sh复制代码
du -sh directory_name # 显示目录大小
-
free
:显示系统内存使用情况。sh复制代码
free -h # 以人类可读的格式显示内存使用情况
-
uname
:显示系统信息。sh复制代码
uname -a # 显示所有系统信息
网络管理
-
ping
:测试网络连接。sh复制代码
ping www.example.com # 测试与目标主机的网络连接
-
ifconfig
:配置网络接口(需要超级用户权限)。sh复制代码
ifconfig # 显示网络接口信息
-
netstat
:显示网络连接、路由表和网络接口信息。sh复制代码
netstat -tuln # 显示所有监听端口
-
ssh
:通过SSH协议远程登录到另一台主机。sh复制代码
ssh user@hostname # 通过SSH登录远程主机
用户管理
-
useradd
:添加新用户(需要超级用户权限)。sh复制代码
sudo useradd new_user # 添加新用户
-
passwd
:更改用户密码。sh复制代码
passwd # 更改当前用户密码 sudo passwd user_name # 更改指定用户密码
-
su
:切换用户身份。sh复制代码
su - # 切换到超级用户 su - user_name # 切换到指定用户
-
sudo
:以超级用户权限执行命令。sh复制代码
sudo command # 以超级用户权限执行命令
- linux下的进程命令
(1)ps(显示当前进程信息)
ps aux # 显示所有进程的详细信息
ps -ef # 显示所有进程的完整格式信息
(2)top(实时显示系统性能和进程信息)
(3)pstree(以树状图显示进程信息)
(4)kill(终止进程)
kill process_id # 终止指定进程
kill -9 process_id # 强制终止指定进程
- linux下的网络命令
(1)ping(测试网络连接)
ping www.example.com # 测试与目标主机的网络连接
(2)ifconfig(显示所有网络接口信息)
(3)ip(显示和操作网络接口、路由)
(4)netstat(显示网络连接、路由表和网络接口信息)
- linux下查看网卡信息命令
(1)ifconfig(显示所有网络接口信息)
(2)ip(显示和操作网络接口信息)
ip addr show # 显示所有网络接口的地址信息
ip link show # 显示所有网络接口的信息
ip addr show eth0 # 显示特定网络接口eth0的地址信息
- 单链表和双链表的区别?
(1)单向链表(每个节点有数据部分和指向下一个节点的指针)
- 优点
(1)内存占用少(只有一个数据和指向下一个节点的指针)
(2)实现相对简单
- 缺点
(1)无法反向遍历
(2)查找效率低
(2)双向链表(每个节点包含数据部分、指向上一个节点的指针、指向下一个节点的指针)
- 优点
(1)能够双向遍历
(2)更高的操作灵活性
- 缺点
(1)内存占用多
(2)实现复杂
(3)区别
-
单向链表:结构简单、内存占用少、但只能单向遍历、操作灵活性差
-
双向链表:结构复杂、内存占用多,但支持双向遍历、操作灵活性高
-
如果知道某个节点的值,想删除怎么操作?
(1)单向链表
两个指针一前一后,遍历所有节点,寻找到要删除的节点,进行删除操作
(2)双向链表
只需要一个指针进行遍历删除即可
- 编程时什么时候适合多进程?什么时候适合多线程
(1)多进程
- 需要任务隔离
当你需要隔离不同任务或模块,确保它们之间的崩溃或故障不会影响其他任务时
- 需要稳定性和安全性
当你需要提高系统的稳定性和安全性时,因为每个进程有自己的内存空间,崩溃或安全问题不会影响其他进程
- 资源隔离,不需要共享资源
当任务需要独立的资源(如文件描述符、网络连接等),并且这些资源不能被共享时
(2)多线程
- 共享内存场景
多个任务需要频繁访问共享数据时,线程可以通过共享内存进行高效的通信
-
轻量级任务
-
需要实时性响应
当应用需要响应用户输入或进行实时处理时,线程可以在同一个进程中并发执行任务,提高响应速度
4, 上下文切换开销低
当你需要频繁的上下文切换,但进程切换开销过大时,线程的上下文切换开销更小
(3)区别
-
多进程:任务隔离、资源独立性高、稳定性安全性要求高
-
多线程:共享内存、高效资源利用、实时性要求高
-
列举五个操作进程的命令
(1)ps(查看当前系统的进程信息)
ps aux # 显示所有用户的所有进程详细信息
ps -ef # 以完整格式显示所有进程
(2)top(实时显示系统中的进程活动)
(3)kill(终止进程)
kill <PID> # 发送SIGTERM信号终止指定PID的进程
kill -9 <PID> # 发送SIGKILL信号强制终止指定PID的进程
(4)pstree(以树状图显示进程信息)
(5)ulimit(设置用户进程的资源限制)
ulimit -a # 显示所有限制
ulimit -n <number> # 设置文件描述符限制
- 什么是阻塞?
(1)定义
在执行程序时,一个操作或进程由于某些原因无法继续执行,必须等待某个条件满足后才能继续,这种情况下程序或进程会暂停,直到阻塞消除
(2)常见场景
-
IO操作
-
锁
-
等待条件
-
为什么多进程不需要互斥、多线程需要互斥?
(1)多进程不需要互斥,因为每个进程都有独立的内存空间,进行间不直接共享内存数据,不会出现多个进程同时访问和修改同一内存位置的情况
(2)多线程需要互斥,线程共享同一内存空间,必须通过同步机制来管理对共享资源的访问,防止数据竞争和不一致性
(3)互斥主要目的是保证数据的完整性和一致性,防止多个执行单元(线程)在同一时间修改共享数据是出现不可预知的结果
- 数据结构中表、栈、队列的区别?
(1)表
表是一种有序的数据集合,每个元素有唯一的位置,表可以是动态的,能够随时增加或删除元素
插入:可以在任意位置插入元素
删除:可以删除任意位置的元素
访问:可以通过索引访问任何位置的元素
(2)栈
一种先入后出的数据结构
入栈:在栈顶添加元素
出栈:从栈顶移出元素
访问:只能访问栈顶元素
(3)队列
先进先出的数据结构
入队:在队尾添加元素
出队:从队首移除元素
访问:只能访问队首和队尾元素
(4)总结
-
表:有序数据集合,可以在任意位置插入、删除和访问,适用于需要频繁访问和修改任意位置元素的场景
-
栈:先进后出,只能在栈顶进行插入和删除,适用于后进先出的场景
-
队列:先进先出,只能在队尾插入和队首删除操作,适用于需要先进先出的场景
-
怎么实现文件的拷贝?
(1)linux命令:cp
(2)TCP
(3)管道
-
如何定义一个指向整形5个元素的数组指针?
int array[5] = {1, 2, 3, 4, 5};
int (*pstr)[5] = &array; -
Linux中如何测试内存大小?
(1)free
显示系统中内存的使用情况,包括总内存、已经使用内存、共享内存、空闲内存、缓存和缓冲区等
free -h
(2)top
实时显示系统任务信息和内存使用情况
- 如何在C语言中定义一个能够变长的数组空间?
(1)用malloc分配初始内存空间
cpp
#include <stdio.h>
#include <stdlib.h>
int main() {
int initial_size = 5;
int *array = (int *)malloc(initial_size * sizeof(int));
if (array == NULL) {
printf("内存分配失败\n");
return 1;
}
// 使用数组
for (int i = 0; i < initial_size; i++) {
array[i] = i;
printf("%d ", array[i]);
}
printf("\n");
// 释放内存
free(array);
return 0;
}
(2)使用realloc来调整数组的大小
cpp
#include <stdio.h>
#include <stdlib.h>
int main() {
int initial_size = 5;
int new_size = 10;
// 初始分配
int *array = (int *)malloc(initial_size * sizeof(int));
if (array == NULL) {
printf("内存分配失败\n");
return 1;
}
// 初始化数组
for (int i = 0; i < initial_size; i++) {
array[i] = i;
}
// 调整大小
int *temp = (int *)realloc(array, new_size * sizeof(int));
if (temp == NULL) {
printf("内存重新分配失败\n");
free(array); // 确保释放原内存
return 1;
}
array = temp;
// 初始化新空间
for (int i = initial_size; i < new_size; i++) {
array[i] = i;
}
// 使用新数组
for (int i = 0; i < new_size; i++) {
printf("%d ", array[i]);
}
printf("\n");
// 释放内存
free(array);
return 0;
}
- linux中如何查看进程的状态图?
pstree命令能够以树状图形式显示进程信息
- 如何使UDP稳定?
(1)超时和确认机制
使用超时和确认机制,确保发送的数据包被正确接收
(2)重传机制
在应用层实现重传机制,如果在一定时间内没有收到ACK(确认)包,则重新发送数据包
(3)错误监测和校验机制
(4)流量控制和拥塞控制
- 什么是同步和异步?它们的区别是什么?
(1)同步
指任务按顺序执行,一个任务必须等前一个任务完成后,才能开始下一个任务。
程序阻塞执行:当前任务执行完之前,后续任务必须等待
任务执行逻辑清晰
会因为其中某个长时间执行的任务导致整体执行延迟变高
(2)异步
指的是任务可以并发执行,不必等待其他任务完成。程序可以继续执行其他任务,不会被阻塞
(3)同步和异步的区别
- 执行方式
(1)同步:按顺序执行,后续任务等待前一项任务完成才能开始执行
(2)异步任务并发执行,不必等待
- 效率
(1)同步:适合短时间的任务,长时间的任务会阻塞程序
(2)异步:适合长时间或IO密集型任务
- 复杂性
(1)同步:逻辑简单,易于调试
(2)异步:逻辑复杂,不易调试
- 介绍数据库的工作原理
数据库的工作原理涉及多方面的知识,包括数据存储、检索、索引、事务处理、并发控制、故障恢复等
(1)数据存储
数据库系统将数据存储在磁盘上的文件中,这些文件通常被组织成表。每个表由行(记录)和列(字段)组成。
- 行(Row): 每行代表一条记录。
- 列(Column): 每列代表一个数据字段
存储结构
- 页(Page): 数据库通常将数据存储在固定大小的页中(如4KB或8KB)。
- 表空间(Tablespace): 页进一步组织成表空间,用于管理表的数据存储。
(2)数据检索
数据检索是通过SQL查询语句实现的。查询语句由数据库管理系统(DBMS)解析、优化和执行
查询处理
- 解析器(Parser): 将SQL查询语句解析成语法树。
- 优化器(Optimizer): 优化查询计划,选择最优的执行路径。
- 执行器(Executor): 执行优化后的查询计划,检索数据。
(3)索引
索引是提高查询速度的关键技术。数据库通过索引来快速定位数据,类似于书籍的目录
- B-树索引: 常见的索引类型,适用于范围查询。
- 哈希索引: 适用于等值查询。
- 全文索引: 用于文本搜索
(4)事务处理
事务是数据库中的一组操作,具有ACID特性(原子性、一致性、隔离性、持久性)
事务特性
- 原子性(Atomicity): 事务中的所有操作要么全部完成,要么全部不完成。
- 一致性(Consistency): 事务使数据库从一个一致状态转换到另一个一致状态。
- 隔离性(Isolation): 事务的执行彼此隔离。
- 持久性(Durability): 事务完成后,结果持久保存在数据库中。
(5)并发控制
并发控制确保多个事务能够安全地并发执行,避免数据不一致
并发控制方法
-
锁机制: 用于控制对数据的访问。
- 共享锁(S): 允许多个事务读取数据。
- 排他锁(X): 只允许一个事务修改数据。
-
多版本并发控制(MVCC): 通过维护数据的多个版本,提供高并发性能
(6)故障恢复
数据库系统通过日志和备份机制实现故障恢复,保证数据的可靠性。
恢复机制
- 日志(Log): 记录事务操作,支持事务回滚和重做。
- 检查点(Checkpoint): 定期将内存中的数据写入磁盘,缩短恢复时间。
- 备份(Backup): 定期备份数据库,防止数据丢失
(7)数据库架构
数据库系统通常采用客户端-服务器架构。
主要组件
- 客户端: 用户接口,发送SQL请求。
- 服务器: 处理SQL请求,管理数据库。
- DBMS内核: 实现存储管理、查询处理、事务处理等功能。
- 存储引擎: 负责数据的存储和检索
(8)数据库类型
- 关系数据库(RDBMS): 使用表格结构存储数据,如MySQL、PostgreSQL。
- 面向对象数据库(OODBMS): 将对象存储在数据库中。
- NoSQL数据库: 非关系型数据库,适用于大数据和分布式存储,如MongoDB、Cassandra
- 互斥和同步的定义和区别
(1)互斥
- 定义
互斥是在任何给定的时刻,只有一个线程或进程能够访问某个资源或执行某段代码。这是为了防止多个线程同时修改共享资源,导致数据不一致
- 实现方式
加锁机制实现互斥
(1)互斥锁
(2)自旋锁
(3)信号量
(4)读写锁
(2)同步
- 定义
同步是指协调多个线程或进程的执行顺序,确保它们按照预期的顺序执行。
- 实现方式
(1)条件变量
(2)信号量
(3)互斥和同步的区别
- 互斥
防止多个线程同时访问共享资源,确保资源访问的独占性
用于保护共享资源,防止数据竞争和不一致
- 同步
协调多个线程的执行顺序,确保线程间的依赖关系和协作
- linux中与文件相关的命令有哪些
(1)文件和目录操作
(1)ls
(2)cd
(3)pwd
(4)mkdir
(5)rmdir
(6)rm
(7)cp
(8)mv
(2)文件查看命令
(1)cat
(2)more
(3)less
(4)head
(5)head
(6)tail
(3)文件搜索与比较
(1)find
- 查找文件和目录。
- 示例:
find /home -name "file.txt"
在/home目录下查找名为file.txt的文件。
(2)locate
- 快速查找文件。
- 示例:
locate file.txt
查找系统中名为file.txt的文件。
(3)grep
- 搜索文件内容。
- 示例:
grep "hello" file.txt
搜索file.txt中包含hello的行。
(4)diff
- 比较文件内容差异。
- 示例:
diff file1.txt file2.txt
比较file1.txt和file2.txt的内容差异
(5)cmp
- 比较两个文件是否相同。
- 示例:
cmp file1.txt file2.txt
比较file1.txt和file2.txt。
(4)文件权限与属性
(1)chmod
- 修改文件权限。
- 示例:
chmod 755 file.txt
设置file.txt的权限为755
(2)stat
- 显示文件详细信息。
- 示例:
stat file.txt
显示file.txt的详细信息
(5)文件归档和压缩
(1)tar
- 归档文件。
- 示例:
tar -cvf archive.tar dir/
创建名为archive.tar的归档文件,包含目录dir
(2)gzip
- 压缩文件。
- 示例:
gzip file.txt
压缩file.txt,生成file.txt.gz
(3)gunzip
- 解压缩文件。
- 示例:
gunzip file.txt.gz
解压file.txt.gz,生成file.txt
(4)zip
- 创建压缩文件。
- 示例:
zip archive.zip file1.txt file2.txt
创建名为archive.zip的压缩文件,包含file1.txt和file2.txt
(5)unzip
- 解压缩文件。
- 示例:
unzip archive.zip
解压archive.zip
(6)其他常用命令
(1)touch
- 创建空文件或更新文件的修改时间。
- 示例:
touch newfile.txt
创建名为newfile.txt的空文件
(2)ln
- 创建链接文件。
- 示例:
ln -s file.txt linkfile.txt
创建file.txt的符号链接linkfile.txt
(3)file
- 查看文件类型。
- 示例:
file file.txt
显示file.txt的文件类型
- 使用进程和网络相关的命令?
(1)进程相关命令
- ps
ps aux
:显示所有进程,包括其他用户的进程。ps -ef
:以完整格式显示所有进程
- top
实时显示系统的资源使用情况,包括CPU、内存和每个进程的使用情况
- kill
kill [PID]
:终止指定 PID 的进程。kill -9 [PID]
:强制终止指定 PID 的进程
- pstree
pstree
:显示进程树。pstree -p
:显示进程树并包括 PID
(2)网络相关命令
-
ifconfig
-
ip
-
ping
-
netstat 显示网络连接、路由表、接口统计等
-
HTTP报文格式
HTTP(超文本传输协议)是一种用于分布式、协作和超媒体信息系统的应用层协议。它是万维网的数据通信基础。HTTP 报文分为请求报文和响应报文,每种报文都包含三个部分:起始行、头部字段和消息主体
(1)请求报文格式
- 请求行
包含请求方法、请求的 URL 和 HTTP 版本
例如:GET /example.html HTTP/1.1
GET
是请求方法,/example.html
是请求的资源路径,HTTP/1.1
是使用的 HTTP 版本
- 请求头部
由一系列的键值对组成,每行一对,常见的头部字段有 Host
(指定服务器的域名和端口)、User-Agent
(客户端的信息,如浏览器类型和版本)、Accept
(客户端能够接受的内容类型)等
例如:
Host: www.example.com
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/115.0
- 空行
用于分隔请求头部和请求体
- 请求体
并非所有的请求都有请求体,例如 GET 请求通常没有,而 POST 请求常用于携带表单数据、JSON 数据等
(2)响应报文格式
- 状态行
包含 HTTP 版本、状态码(如 200 表示成功,404 表示未找到资源等)以及状态描述
例如:HTTP/1.1 200 OK
- 响应头部
与请求头部类似,也是一系列的键值对,常见的有 Content-Type
(响应内容的类型)、Content-Length
(响应内容的长度)等
例如
Content-Type: text/html; charset=utf-8
Content-Length: 1234
- 空行
分隔响应头部和响应体
- 响应体
服务器返回的具体内容,如 HTML 页面、JSON 数据等
- 内存碎片、内存泄漏、内存溢出
(1)内存碎片
由于频繁malloc和free小空间,导致大的连续空间由于中间存在小空间被占用而无法得到申请空间的现象称为内存碎片。分为外部碎片和内部碎片
(2)内存泄漏
指程序在运行过程中分配了空间但未能释放,从而导致程序占用的内存不断增加,最终可能耗尽系统内存资源。(malloc但未能及时free)
(3)内存溢出
也称内存越界,操作超过变量范围的空间
- 原子操作
在多线程或多进程环境中,对共享资源的操作能够保证在操作执行过程中不会被终端或干扰的操作。要么完全执行,要么完全不执行,不存在中间状态
- 如何利用CPU占用率查看内存信息?
top
top
命令是一个实时的系统监控工具,可以显示 CPU 和内存的使用情况。通过观察 top
输出的信息,可以间接分析内存使用情况
- 什么是MAC地址(在数据链路层)
(1)定义
MAC地址是数据链路层地址,用在局域网中唯一标识网络接口设备。
(2)功能
确保网络中的数据能够正确到达目标设备。数据链路层负责处理帧的封装和解封装,错误检测和流量控制
- 详细介绍wireshark
(1)定义
Wireshark 是一个广泛使用的网络协议分析工具,它允许用户捕获和分析网络上的数据包,帮助诊断网络问题、进行安全分析和进行协议开发等
(2)主要功能
-
数据包捕获
-
协议分析
Wireshark 支持解码多种网络协议,包括 IP、TCP、UDP、HTTP、DNS、SMTP 等
-
数据过滤
-
数据分析
-
到处和报告
-
什么是时间复杂度和空间复杂度?
(1)时间复杂度(时间复杂度越低,程序效率越高)
衡量算法执行所需的时间,也是算法中随着变量的改变,时间增长的趋势
(1)常数时间复杂度 O(1)O(1)O(1): 算法的执行时间与输入规模无关,无论输入多大,执行时间都是常量
(2)对数时间复杂度 O(logn)O(\log n)O(logn): 执行时间随着输入规模的对数增长。常见于二分查找算
(3)线性时间复杂度 O(n): 执行时间与输入规模成正比。比如for循环遍历数组
(4)线性对数时间复杂度 O(nlogn): 执行时间为 n 和 logn 的乘积.比如快速排序
(5)二次时间复杂度 O(n2): 执行时间与输入规模的平方成正比/比如冒泡、插入排序
(2)空间复杂度
空间复杂度衡量算法在执行过程中所需的内存空间。即算法中变量对内存空间大小的影响趋势
(3)如何分析时间复杂度和空间复杂度
-
基本操作: 找出算法中最耗时的基本操作(如加法、赋值、数组访问等),并计算它们的执行次数。
-
循环: 循环结构通常是影响时间复杂度的关键部分,单层循环的时间复杂度通常是 O(n),嵌套循环会叠加复杂度。
-
递归: 递归算法的时间复杂度通常通过递归方程求解,空间复杂度则与递归调用栈的深度相关。
-
空间开销: 除了考虑变量和数据结构的空间需求,还要考虑递归调用栈和临时变量的空间需求。
- 什么是排序算法的稳定性
在对待排序数据进行排序时,两个相同元素的相对位置在排序前后不发生改变,贼排序算法稳定
稳定的排序算法,排序结束后,n1和n2的相对位置不变
- 快速排序和冒泡排序的对比和区别
(1)时间复杂度
-
冒泡排序:O(n^2)
-
快速排序:O(n*logn)
(2)空间复杂度
-
冒泡排序:O(1)
-
快速排序:O(logn)
快速排序虽然是原地排序,但用到了递归,需要使用栈空间,它的空间复杂度取决于递归深度
(3)稳定性
-
冒泡排序:稳定
-
快速排序:不稳定
(4)实际应用
-
冒泡排序:时间复杂度较高,只能处理数据量小的情况,或作为教学算法
-
快速排序:时间复杂度较低,广泛应用在实际开发中
(5)总结
整体看,快排比冒泡排序法更优。
它在大多数情况下都能提供较快的排序速度(O(nlogn)O(n \log n)O(nlogn)),而冒泡排序在处理大数据集时往往表现不佳(O(n2)O(n^2)O(n2))。快速排序的唯一缺点是它不是稳定的,并且在极端情况下可能会退化为 O(n2)O(n^2)O(n2)。但通过适当的优化,快速排序仍然是非常高效的通用排序算法
- 二叉树的遍历
(1)前序遍历:根节点 -> 左子树 -> 右子树 (深度优先)
cpp
void pre_order(TreeNode *proot)//前序遍历 根 左 右
{
if (NULL == proot)//一次前序遍历结束的标志
{
return ;
}
printf("%c", proot->data);
pre_order(proot->pl);
pre_order(proot->pr);
}
(2)中序遍历:左子树 -> 根节点 -> 右子树(深度优先)
cpp
void mid_order(TreeNode *proot)//中序遍历 左 根 右
{
if (NULL == proot)
{
return ;
}
mid_order(proot->pl);
printf("%c", proot->data);
mid_order(proot->pr);
}
(3)后续遍历:左子树 -> 右子树 -> 根节点(深度优先)
cpp
void pos_order(TreeNode *proot)//后续遍历 左 右 根
{
if (NULL == proot)
{
return ;
}
pos_order(proot->pl);
pos_order(proot->pr);
printf("%c", proot->data);
}
(4)层序遍历:按层次从上到下、从左到右访问节点(广度优先)
- 原理
(1)利用队列和二叉树的结构特性,首先队列为空,入队A节点的地址,队列非空,出队A节点地址,打印A节点数据,并分别将A节点的左子树节点B和右子树节点D入队,队列非空,按先入先出的原则,先出队B节点,打印B节点数据,然后依次将B节点的左子树节点E和右子树节点F入队,重复执行上述过程,直到遍历结束
(2)代码
cpp
//创建队列
QueList *create_queue()
{
QueList *pque = malloc(sizeof(QueList));
if (NULL == pque)
{
perror("fail malloc");
return NULL;
}
pque->pfront = NULL;
pque->prear = NULL;
pque->clen = 0;
return pque;
}
//入队
int push_queue(QueList *pque, DataType data)
{
QueNode *pnode = malloc(sizeof(QueNode));
if (NULL == pnode)
{
perror("fail malloc");
return -1;
}
pnode->data = data;
pnode->pnext = NULL;
if (is_empty_queue(pque))
{
pque->pfront = pnode;
pque->prear = pnode;
}
else
{
pque->prear->pnext = pnode;
pque->prear = pnode;
}
pque->clen++;
return 0;
}
//出队
int pop_queue(QueList *pque, DataType *pdata)
{
if (is_empty_queue(pque))
{
return 1;
}
QueNode *pfree = pque->pfront;
pque->pfront = pfree->pnext;
if (pdata != NULL)
{
*pdata = pfree->data;
}
free(pfree);
pque->clen--;
if (NULL == pque->pfront)
{
pque->prear = NULL;
}
return 0;
}
//层序遍历
void layer_order(TreeNode *proot)//层序遍历
{
DataType outdata;
QueList *pque = create_queue();
if (NULL == pque)
{
return ;
}
push_queue(pque, proot);
while (!is_empty_queue(pque))
{
pop_queue(pque, &outdata);
printf("%c", outdata->data);
if (outdata->pl != NULL)
{
push_queue(pque, outdata->pl);
}
if (outdata->pr != NULL)
{
push_queue(pque, outdata->pr);
}
}
destroy_queue(pque);
}
- 什么是广度优先遍历和深度优先遍历?它们的的区别是是什么?
(1)广度优先遍历(Breadth-First Search, BFS)
- 遍历方式
从起始节点开始,按照层次顺序逐层遍历,先访问距离起始节点最近的节点,再访问下一层的节点,直到所有节点都被访问
- 实现方法
通常使用队列(Queue)数据结构来实现。每次从队列中取出一个节点,并将该节点的所有未访问邻居节点依次加入队列
- 适用场景
适用于需要找到最短路径的场景,比如在无权图中寻找最短路径
(2)深度优先遍历(Depth-First Search, DFS)
- 遍历方法
从起始节点开始,沿着某一分支不断深入,直到无法继续深入为止,然后回溯到前一个节点,继续遍历未访问的其他分支
- 实现方式
通常使用栈(Stack)数据结构来实现(可以用递归的方式进行隐式地使用系统栈)。每次从栈顶取出一个节点,访问其未访问的邻居节点,依次入栈继续深入
- 适用场景
适用于需要遍历完整路径或所有可能路径的场景,比如在搜索迷宫时找到所有可能的路线
(3)区别
-
顺序:BFS按层次顺序遍历,DFS按深度顺序遍历
-
数据结构:BFS使用队列,DFS使用栈(递归实现时使用系统栈)
-
路径:BFS优先找到最短路径,DFS更适合搜索整棵树或图中的所有路径
-
介绍内存片
内存片(也称为DRAM芯片或内存芯片)是计算机内存的主要组件之一,用于存储数据和程序代码以供处理器随时访问。内存片在计算机系统中扮演着临时存储的角色,具有高速、随机访问的特点
(1)内存片
内存片通常由多个存储单元(cells)组成,每个单元存储一位数据(0或1)。这些单元按照行和列排列,形成矩阵结构。每个单元由一个晶体管和一个电容器组成:
- 电容器:用于存储电荷,表示数据位的状态(满电表示1,放电表示0)。
- 晶体管:用于控制电容器的充放电状态,从而读写数据
(2)内存片类型
内存片主要有以下几种类型:
- 动态随机存取存储器(DRAM):这是最常见的内存类型,数据存储在电容中,由于电容会漏电,所以需要定期刷新数据。DRAM速度较快,广泛用于计算机主内存(RAM)。
- 静态随机存取存储器(SRAM):使用触发器来存储每一位数据,不需要像DRAM那样定期刷新,因此速度更快,但成本更高,功耗也更大。常用于CPU缓存。
- 闪存(Flash Memory):是一种非易失性存储器,不需要电源就能保存数据。常见于固态硬盘(SSD)和USB闪存驱动器。
- 只读存储器(ROM):数据写入后不能更改,通常用于存储固件或操作系统的启动程序。
(3)工作原理
- 读操作:当处理器需要读取数据时,内存控制器会发送行和列地址到内存片。内存片根据地址定位到对应的存储单元,并通过晶体管读取电容器中的电荷状态,将数据传回处理器。
- 写操作:写操作与读操作类似,区别在于处理器会将数据写入内存片,通过晶体管控制电容器的电荷状态来存储新的数据
(4)性能指标
- 容量:内存片的存储容量通常以字节为单位,现代内存片的容量范围从几百MB到数百GB。
- 速度:包括时钟频率(如DDR4-3200,表示3200 MHz)和数据传输速率(如3200 MT/s),决定了内存的访问速度。
- 延迟:表示从处理器发出请求到内存返回数据之间的时间延迟,通常以时钟周期为单位表示,如CL16
(5)应用
内存片被广泛应用于各种设备中,如个人计算机、服务器、智能手机、平板电脑、嵌入式系统等。它们为处理器提供快速访问的数据存储空间,是确保系统高效运行的关键组件
- 线程池和多进程的区别?epoll和线程池的关系和区别?epoll能否代替线程池?
(1)线程池
- 定义
线程池是一种管理和复用线程的技术,通过事先创建的一组线程并循环使用它们来处理任务,从而减少线程创建和销毁的开销,提高系统响应速度和资源利用率
- 例子
(1)
现在要与别人打电话,我买一部手机,跟别人打电话,结束后把电话扔掉,再与人打电话时,再买一部手机,与人通信,最后再把手机扔掉。这样反复重复浪费钱,不如直接买一部手机一直与人打电话,不用反复购买电话,节省钱
(2) 以银行取钱为例
-
一共有五个窗口,对应线程池中的提前创建好的五个线程
-
默认开放三个窗口供用户提供存取服务,对应线程池中开放三个能够重复使用的线程
-
当有人取钱时,在空闲窗口存取,若窗口已经满了,让人在等待区等待窗口空闲,对应当任务来临时,在线程池中启用线程处理任务,若线程池中启用的线程到达上限,将后来的任务存入等待队列(先进先出)中
-
当银行没有空闲柜台且等待区人数到达上限时,银行会恢复未使用的柜台进行营业,然后从等待区传唤等待者进行业务办理,等待区的其他人往前移动,新的用户加入等待区,直到柜台全部启动且等待区人数到达上限,此时还有新的用户进入时,拒绝访问,需要到其他银行点去办理,对应于线程池中如果正在执行的线程到达上限,唤醒未使用的线程,将等待队列中的任务加入线程中进行处理,若线程池中的线程全部用于进行处理任务,且等待队列中无能够加入新任务的节点,则无法处理新的任务
- 线程池的优点
(1)减少资源消耗
线程池通过复用线程,避免了频繁创建和销毁线程带来的资源消耗
(2)提高响应速度
线程池中的线程可以立即响应任务请求,无需等待新线程的创建
(3)提高系统稳定性
线程池可以控制并发线程的数量,避免多线程导致系统资源的耗尽
(4)灵活的管理机制
可以根据系统负载动态调整线程池的大小,适应不同的应用场景
(2)线程池与多进程的区别
- 资源开销
(1)线程池
线程共享同一进程的内存空间,线程之间切换开销较小,但需要注意线程同步问题
(2)多进程
每个进程有独立的内存空间,进程之间切换开销较大,但进程间相互独立,不易影响彼此
- 通信方式
(1)线程池
线程之间通过共享内存进行通信,需要考虑同步机制(如锁)来避免数据竞争
(2)多进程
进程之间通常通过IPC(如管道、消息队列、共享内存)进行通信,相对复杂
- 使用场景
(1)线程池
适合CPU密集型任务或需要大量并发I/O操作的场景
(2)多进程
适合需要独立运行、隔离性强的任务,特别是需要利用多核CPU的场景
(3)epoll与线程池的关系和区别
- 用途
(1)epoll
用于高效管理和监视大量I/O事件,通常用于网络服务器的事件驱动模型
(2)线程池
用于管理和调度线程执行任务,主要解决并发任务的处理问题
- 协同工作
epoll和线程池可以结合使用
epoll负责监视IO时间,当有IO事件发生时,将任务分配给线程池中的线程处理,从而实现高效的并发IO处理
- 实现方式
(1)epoll
通过内核支持实现,适用于Linux系统,具有高效的事件通知机制
(2)线程池
通常由应用程序实现,依赖于操作系统的线程管理机制
(4)epoll不能完全代替线程池
epoll主要用于管理和监视I/O事件,而线程池用于管理并发任务的执行。它们各自解决不同的问题,并且在高并发服务器中经常协同使用,以实现高效的事件处理和任务调度
具体示例:
- 高并发网络服务器 :
- epoll 负责监视网络套接字的I/O事件。
- 当有新的I/O事件发生时,通过epoll返回的事件通知,将处理任务分配给线程池中的空闲线程。
- 线程池中的线程处理具体的I/O读写操作或业务逻辑
- 常用的进程间通信方法(IPC)
(1)管道(Pipe)
- 无名管道(Unnamed Pipe):用于具有亲缘关系的进程间通信(如父子进程)。
- 有名管道(Named Pipe or FIFO):用于不具有亲缘关系的进程间通信,支持任意进程间的数据传输。
(2)消息队列
允许进程以消息的形式发送和接收数据,通过消息队列标识符进行通信
(3)信号(Signal)
用于通知进程某些事件的发生,信号是一种异步通信机制
(4)信号量(Semaphore)
用于进程间的同步和互斥,控制对共享资源的访问
(5)套接字(Socket)
主要用于网络通信,但也可以用于本地进程间通信,通过TCP/IP协议传输数据
- 无名管道和有名管道的区别
(1)无名管道
- 只能用于具有亲缘关系的进程间通信(如父子进程)。
- 在创建时,管道的读端和写端由进程继承。
- 使用
pipe()
系统调用创建。
(2)有名管道
- 可以用于不具有亲缘关系的任意进程间通信。
- 有文件系统中的路径名,任何进程都可以通过路径名访问管道。
- 使用
mkfifo()
系统调用创建
- 无名和有名管道的创建
(1)无名管道
- 创建管道
使用 pipe()
系统调用创建无名管道
cpp
int pipefd[2];
if (pipe(pipefd) == -1) {
perror("pipe");
exit(EXIT_FAILURE);
}
- 使用管道
父进程和子进程分别关闭不需要的端口
cpp
pid_t pid = fork();
if (pid == -1) {
perror("fork");
exit(EXIT_FAILURE);
} else if (pid == 0) {
// 子进程
close(pipefd[1]); // 关闭写端
// 读数据
} else {
// 父进程
close(pipefd[0]); // 关闭读端
// 写数据
}
(2)有名管道
- 创建有名管道
使用 mkfifo()
系统调用创建有名管道
cpp
const char *fifo_path = "/tmp/myfifo";
if (mkfifo(fifo_path, 0666) == -1) {
perror("mkfifo");
exit(EXIT_FAILURE);
}
- 打开和使用有名管道
使用 open()
系统调用打开有名管道进行读写
(1)写入端
cpp
int fd = open(fifo_path, O_WRONLY);
if (fd == -1) {
perror("open");
exit(EXIT_FAILURE);
}
// 写数据
write(fd, "Hello", 5);
close(fd);
(2)读取端
cpp
int fd = open(fifo_path, O_RDONLY);
if (fd == -1) {
perror("open");
exit(EXIT_FAILURE);
}
// 读数据
char buffer[128];
read(fd, buffer, sizeof(buffer));
close(fd);
- 什么是中断上下文?什么是中断顶半部和中断底半部?
(1)中断上下文是指处理器在处理中断服务程序时所处的执行环境
(2)中断顶半部和底半部
- 中断顶半部(中断上半部)
为中断处理函数部分,一般用于处理过程比较快,不会占用太长事件处理的中断任务
- 中断底半部(中断下半部)
中断处理中比较耗时的操作一般会放在中断下半部执行,以便让中断处理函数快进快出
- 什么是信号?如何使用信号?使用信号处理函数需要注意什么?可以在信号处理函数中malloc空间吗,malloc空间会发生什么?
(1)信号是一种进程间通信(IPC)机制,用于向进程发送通知,告知其发生了异步事件。信号可以打断进程的正常执行流,以处理特定事件
常见信号
SIGINT
(2): 用户中断(如Ctrl+C)。SIGKILL
(9): 强制终止进程。SIGTERM
(15): 请求终止进程。SIGSEGV
(11): 段错误(非法内存访问)。SIGALRM
(14): 定时器信号
(2) 信号的使用和处理
(1)信号的处理
-
默认处理:系统预定义的默认处理行为,如终止进程、忽略信号等
-
捕获处理:进程可以定义自己的信号处理函数(信号处理程序)来处理特定信号
-
忽略处理:进程可以选择忽略某些信号(但
SIGKILL
和SIGSTOP
不能被忽略或捕获)
(2)信号的发送
-
用户通过键盘输入(如Ctrl+C)
-
进程间通过系统调用
kill
发送信号 -
内核发送信号(如定时器信号
SIGALRM
) -
硬件异常导致的信号(如段错误
SIGSEGV
)
(3)使用信号
- 发送信号
kill(pid_t pid, int sig)
用于向指定进程发送信号
pid
指定接收信号的进程ID,sig
指定发送的信号类型
- 捕获信号
signal(int signum, sighandler_t handler)
用于设置信号处理程序
signum
是信号编号,handler
是处理函数的指针,可以是一个函数地址、SIG_IGN
(忽略信号)或SIG_DFL
(使用默认处理
- 示例代码
cpp
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
void handle_sigint(int sig) {
printf("Caught signal %d\n", sig);
}
int main() {
// 设置SIGINT信号的处理函数
signal(SIGINT, handle_sigint);
// 无限循环,等待信号
while (1) {
printf("Running...\n");
sleep(1);
}
return 0;
}
在这个例子中,当用户按下Ctrl+C时,进程会捕获SIGINT
信号并调用handle_sigint
函数,而不是终止进程
(3)信号处理函数的注意事项
- 可重入性
信号处理函数应该是可重入的,因为它可能在任何时间点被调用。不可重入的函数(如printf
、malloc
、free
等)在信号处理函数中可能导致未定义行为
- 最小化处理时间
信号处理函数应该尽量短小快速,避免长时间运行影响系统响应
- 异步信号安全
在信号处理函数中应该只调用异步信号安全(async-signal-safe)的函数。Linux手册中列出了这些函数,包括_exit
、signal
、write
等
- 共享数据的同步
处理共享数据时需要小心,可能需要使用自旋锁等同步机制,避免竞态条件
(4)在信号处理函数中使用malloc
在信号处理函数中调用malloc可能会导致未定义行为。这是因为malloc及其相关函数不是异步信号安全的,它们可能使用内部全局数据结构进行内存管理,这些数据结构在信号处理函数中可能被不安全地访问和修改
可能发生的问题
(1)死锁
malloc
可能在信号处理函数中尝试获取内存管理锁,而该锁可能已被其他代码持有,导致死锁
(2)内存损坏
多线程环境下,malloc
可能导致内存管理数据结构的并发访问,导致内存损坏或崩溃
(3)结论
为了安全处理信号,建议在信号处理函数中避免使用非异步信号安全的函数,例如避免在信号处理函数中使用malloc。
处理方法是在主程序中预分配好所需的内存,信号处理函数中直接使用这些预分配的内存或只调用异步信号安全的函数,确保信号处理函数的可重入性和安全性
(5)信号的应用场景
- 进程控制
使用信号来控制进程的启动、停止、终止等
- 定时操作
使用SIGALRM
信号进行定时操作
- 异常处理
捕获如段错误(SIGSEGV
)等异常进行错误处理
- 进程间通信
使用信号进行简单的进程间通信和同步
- 常用代码调试方法?(包含检测段错误)
(1)GDB(GNU调试器)
GDB是一个功能强大的调试器,可以用于调试C程序。一般步骤是启动GDB,设置断点、运行程序、打印变量值,调试分母为零的错误
- 编译代码时加入-g选项
gcc filename.c -g
- 使用gdb调试生成的代码
gdb a.out
- gdb调试命令
l 查看代码
b 函数名/行号 设置断点
r 运行代码
n 单步运行
c 直接运行到下一断点处
s 进入函数内部调试
p 变量名 查看变量对应的值
q 退出
(2)使用printf打印调试
在可能出现错误的位置加入打印,前一句能够打印出来,后一句打印不出来,问题就可以定位到两次打印中间的代码
(3)core文件调试法(检测段错误)
-
配置core文件
-
ulimit -c unlimited
-
编译代码加入-g选项
gcc filename.c -g
-
运行代码使其产生段错误,段错误产生后会生成一个包含错误信息的core文件
-
gdb a.out core找到错误产生的位置
(4)使用valgrind进行内存调试
valgrind
是一款内存调试工具,可以检测内存泄漏、非法内存访问等问题
操作步骤
- 正常编译程序
cpp
gcc -o my_program my_program.c
- 使用valgrind运行程序
cpp
valgrind --leak-check=full ./my_program
(5)使用静态分析工具
静态分析工具如cppcheck
可以在编译前检测代码中的潜在问题
- 安装
cppcheck
cpp
sudo apt-get install cppcheck
- 使用
cppcheck
分析代码
cpp
cppcheck my_program.c
(6)使用IDE调试
许多集成开发环境(IDE)如CLion、Code::Blocks、Eclipse等都提供了图形化的调试功能,支持设置断点、单步执行、查看变量值等
- linux中什么是内存泄漏?内存泄漏的原理是什么?
(1)内存泄漏
内存泄漏(Memory Leak)是指程序在运行过程中动态分配的内存未被释放,导致这部分内存无法被重用。虽然程序已经不再需要这些内存,但由于未能释放,它们仍然占用内存资源,导致系统的可用内存逐渐减少,最终可能引发内存不足的问题
(2)内存泄漏的原因
内存泄漏的根本原因是程序未能正确管理动态内存分配
- 未释放内存
程序使用malloc
、calloc
、realloc
等函数分配内存,但在使用完后没有调用free
释放这部分内存
- 丢失指针引用
程序分配内存后,将指向这块内存的指针覆盖或修改,导致无法再访问和释放这块内存
cpp
int *ptr = (int *)malloc(sizeof(int) * 10);
ptr = NULL; // 原来的内存地址丢失,无法释放
- 循环引用
特别是在使用复杂数据结构(如链表、树)时,如果存在循环引用,可能导致内存无法正确释放
cpp
struct node {
struct node *next;
};
struct node *n1 = (struct node *)malloc(sizeof(struct node));
struct node *n2 = (struct node *)malloc(sizeof(struct node));
n1->next = n2;
n2->next = n1; // 循环引用
free(n1);
free(n2); // 由于循环引用,内存不能完全释放
- 踩内存是什么(关于越界访问)?越界访问你了解多少?当你的程序发生越界访问,你怎么调试?
(1)踩内存
- 定义
程序错误地修改了它无权访问的内存区域。踩内存可能导致不可预见的行为,包括程序崩溃、数据损坏和安全漏洞。
- 出现踩内存的原因
(1)缓冲区溢出
程序写入超过数组或缓冲区边界的数据,导致相邻内存区域被覆盖
(2)使用已释放的内存
程序在释放内存后继续使用这段内存,导致未定义行为
(3)双重释放
程序试图释放已经释放的内存,再次释放可能导致内存管理数据结构的损坏
(4)程序使用未初始化或已失效的指针,导致访问无效内存
(2)越界访问
- 定义
越界访问是踩内存的一个具体表现,指程序访问了数组或缓冲区边界之外的内存区域
- 越界访问可能导致产生的问题
(1)数据损坏
改变其他变量的数据,导致程序逻辑错误
(2)程序崩溃
访问非法内存地址,导致段错误(Segmentation Fault
(3)安全漏洞
攻击者可以利用越界访问执行恶意代码或泄露敏感信息
(3)调试越界访问
- 使用GDB调试
GDB是一个强大的调试器,可以帮助检测和调试越界访问问题
- 使用valgrind
valgrind
是一款强大的内存调试工具,可以检测内存错误,包括越界访问
- 使用
AddressSanitizer
AddressSanitizer
是一个内存错误检测工具,可以在编译时启用
- 静态分析工具
静态分析工具如cppcheck
和clang-tidy
可以在编译前检测代码中的潜在问题
- 什么是网络传输中的丢包和粘包?如何解决丢包和粘包?
(1)丢包
- 定义
丢包是指在数据传输过程中,数据包未能到达目的地,导致数据的缺失。丢包会影响数据的完整性和传输质量
- 产生丢包的原因
(1)网络拥塞
网络中数据流量过大,导致路由器和交换机无法处理所有的数据包,部分数据包被丢弃
(2)硬件故障
网络设备如路由器、交换机、网卡等故障,导致数据包丢失
(3)信号干扰
无线网络中,由于信号干扰或弱信号导致数据包未能成功传输
(4)软件问题
网络协议栈中的软件错误或配置错误也可能导致丢包
- 丢包的解决方法
(1)使用可靠的传输协议:如TCP协议,有丢包重传机制
(2)优化网络环境:减少网络拥塞、升级网络设备,避免信号干扰
(3)质量检测和监控:使用网络监控工具检测和分析丢包情况,及时处理网络故障
(2)粘包
- 定义
粘包是指在数据传输过程中,多个数据包被连接在一起,接收端无法区分这些数据包,即不同数据之间缺乏分界线和分隔标志,导致数据解析错误。粘包通常发生在TCP传输中,因为TCP是流量传输协议
- 粘包产生的原因
(1)数据发送过快
发送端连续发送多个数据包,而接收方处理速度跟不上,导致多个数据包在接收端缓冲区中积累,形成一个大的数据包
(2)TCP协议的特性
TCP是面向字节流的协议,它并不关心数据包的边界。在传输过程中,TCP会根据网络情况进行分段和重组,这可能会导致粘包
(3)接收方读取数据的方式
接收方读取数据时,没有按照预期的数据边界来读取,而是一次性读多个数据包的内容
- 粘包的解决方法
(1)使用定长消息
每个数据包固定长度,接收方每次读取固定长度的数据。但这种方法的效率较低,不适合长度变化较大的数据
(2)使用特殊分隔符
在每个数据包的末尾加一个特殊的分隔符(如换行符、特殊字符等),接收方读取数据时,通过检测分隔符来区分不同的数据包。这种方法比较简单,但需要确保分隔符在实际数据中不会出现
(3)在数据包前加上长度字段
在每个数据包的前面加上一个表示数据包长度的字段(通常为定长字段),接收方先读取长度字段,根据长度字段的值再读取相应长度的数据包。这是比较常用且有效的方法