面试基础知识

本文深入探讨了操作系统中的死锁概念,包括规范定义、资源死锁条件(互斥、占用并等待、不可抢占和环路等待),以及如何通过死锁检测与恢复(如银行家算法和预防策略)来避免和预防死锁。此外,还介绍了进程和线程的基础知识,如进程模型、进程表、状态与调度,以及进程间和线程间的通信方式,如信号量、互斥量和消息队列等。
摘要由CSDN通过智能技术生成

操作系统

死锁

规范定义

如果一个进程集合中的每个进程都在等待只能由该进程集合中的其他进程才能引发的事件,那么,该进程集合就是死锁的。

资源死锁的条件
  • 互斥
  • 占用并等待
  • 不可抢占
  • 环路等待
解决死锁
  • 死锁检测和恢复
  • 死锁避免:银行家算法
  • 死锁预防:破坏四个条件

进程

定义

在进程模型中,计算机上所有可运行的软件,通常也包括操作系统,被组织成若干顺序进程。一个进程就是一个正在执行程序的实例,包括程序计数器、寄存器和变量的当前值。

进程表

一个进程有关的所有信息,除了该进程自身地址空间的内容以外,均存放在操作系统的一张表中,称为进程表(process table),进程表是数组(或链表)结构,当前存在的每个进程都要占用其中一项。

进程表

中断过程

进程的创建

①系统初始化

②执行创建进程的系统调用

③用户请求创建新进程

④批处理作业初始化

进程的状态
  • 运行
  • 就绪
  • 阻塞

进程状态

进程调度
批处理系统
  • 先来先服务
  • 最短作业优先
  • 最短剩余时间优先
  • 最高响应比优先
交互式系统
  • 时间片轮转
  • 优先级调度算法
  • 多级反馈队列调度
僵尸进程

一个子进程结束后,它的父进程并没有等待它(调用wait或者waitpid),那么这个子进程将成为一个僵尸进程。僵尸进程是一个已经死亡的进程,但是并没有真正被销毁。它已经放弃了几乎所有内存空间,没有任何可执行代码,也不能被调度,仅仅在进程表中保留一个位置,记载该进程的进程ID、终止状态以及资源利用信息(CPU时间,内存使用量等等)供父进程收集,除此之外,僵尸进程不再占有任何内存空间。这个僵尸进程可能会一直留在系统中直到系统重启。

危害:占用进程号,而系统所能使用的进程号是有限的;占用内存。

孤儿进程

一个父进程已经结束了,但是它的子进程还在运行,那么这些子进程将成为孤儿进程。孤儿进程会被Init(进程ID为1)接管,当这些孤儿进程结束时由Init完成状态收集工作。

系统调用
  • 进程管理:fork、exec、exit、waitpid
  • 文件管理:open、close、read、write、lseek、stat
  • 目录和文件:mkdir、rmdir、link、unlink、mount、unmount
  • 杂项:chmod、chdir、kill、time

线程

线程内容和系统调用

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-7I4hv7uu-1649384040640)(http://mi_chuan.gitee.io/blog/进程和线程内容.png)]

pthread系统调用

线程间通信

①全局变量(共享内存):多个线程可能更改全局变量,因此全局变量最好声明为volatile

②消息队列。

进程间通信

  • 竞争条件:两个以上进程读写共享数据,最后结果取决于进程运行的精确时序。

  • 临界区:对共享内存进行访问的程序片段。

通信方式
  • 加锁。

  • 信号量:是一个计数器。信号量用于实现进程间的互斥与同步,而不是用于存储进程间通信数据。

  • 互斥量:只有0和1,简化的信号量。

  • 消息传递MessageQueue。消息队列,就是一个消息的链表,是一系列保存在内核中消息的列表。用户进程可以向消息队列添加消息,也可以向消息队列读取消息。消息队列与管道通信相比,其优势是对每个消息指定特定的消息类型,接收的时候不需要按照队列次序,而是可以根据自定义条件接收特定类型的消息。

  • 管道FIFO。半双工的(即数据只能在一个方向上流动),具有固定的读端和写端。它只能用于具有亲缘关系的进程之间的通信(也是父子进程或者兄弟进程之间)。它可以看成是一种特殊的文件,对于它的读写也可以使用普通的read、write 等函数。但是它不是普通的文件,并不属于其他任何文件系统,并且只存在于内存中。管道分为pipe(无名管道)和fifo(命名管道)两种,除了建立、打开、删除的方式不同外,这两种管道几乎是一样的。他们都是通过内核缓冲区实现数据传输。

  • 共享存储SharedMemory。共享内存允许两个或多个进程共享一个给定的存储区,这一段存储区可以被两个或两个以上的进程映射至自身的地址空间中,一个进程写入共享内存的信息,可以被其他使用这个共享内存的进程,通过一个简单的内存读取读出。

  • 信号(signal、kill)。

编译过程

编译链接

Linux 系统启动过程

5个阶段

内核引导

计算机打开电源后,首先是BIOS开机自检,按照BIOS中设置的启动设备(通常是硬盘)来启动。

操作系统接管硬件以后,首先读入 /boot 目录下的内核文件。

运行 init

init 进程是系统所有进程的起点,你可以把它比拟成系统所有进程的老祖宗,没有这个进程,系统中任何进程都不会启动。

init 程序首先是需要读取配置文件 /etc/inittab。

运行级别

Linux系统有7个运行级别(runlevel):

  • 运行级别0:系统停机状态,系统默认运行级别不能设为0,否则不能正常启动
  • 运行级别1:单用户工作状态,root权限,用于系统维护,禁止远程登陆
  • 运行级别2:多用户状态(没有NFS)
  • 运行级别3:完全的多用户状态(有NFS),登陆后进入控制台命令行模式
  • 运行级别4:系统未使用,保留
  • 运行级别5:X11控制台,登陆后进入图形GUI模式
  • 运行级别6:系统正常关闭并重启,默认运行级别不能设为6,否则不能正常启动
系统初始化

在init的配置文件中有这么一行: si::sysinit:/etc/rc.d/rc.sysinit 它调用执行了/etc/rc.d/rc.sysinit,而rc.sysinit是一个bash shell的脚本,它主要是完成一些系统初始化的工作,rc.sysinit是每一个运行级别都要首先运行的重要脚本。

它主要完成的工作有:激活交换分区,检查磁盘,加载硬件模块以及其它一些需要优先执行任务。

建立终端

rc执行完毕后,返回init。这时基本系统环境已经设置好了,各种守护进程也已经启动了。

init接下来会打开6个终端,以便用户登录系统。

用户登录系统

登录方式有三种:

  • (1)命令行登录
  • (2)ssh登录
  • (3)图形界面登录

Linux系统调用

添加系统调用
  1. 下载内核源代码
  2. 添加系统调用号
vim ./arch/x86/entry/syscalls/syscall_64.tbl
334 64  mycopy          sys_mycopy
  1. 声明系统调用函数原型
vim include/linux/syscalls.h
asmlinkage long sys_mycopy(const char __user *target, const char __user *source);
  1. 添加系统调用函数的定义
vim kernel/sys.c
get_ds: 获得kernel的内存访问地址范围
get_fs: 取得当前的地址访问限制值
set_fs: 设置当前的地址访问限制值
SYSCALL_DEFINE2(mycopy, const char __user *, _target, const char __user *, _source){
    char *target;
    char *source;
    char buf[512]; 
	int sor_fd, tar_fd, Readsum, WriteRes;
	
	//get point
	target = strndup_user(_target,PAGE_SIZE);
	if(IS_ERR(target)){
	    printk("point error!\n");
	    return -1;
	}
	
	source = strndup_user(_source,PAGE_SIZE);
	if(IS_ERR(source)){
	    printk("point error!\n");
	    return -1;
	}

	// kernel -> User space    -->  fs
	mm_segment_t fs;
	fs = get_fs();  //get access restriction value
	set_fs(get_ds());  //set kernel restriction value

	// open file
	sor_fd = sys_open(source, O_RDONLY, S_IRUSR);
	if (sor_fd == -1)
	{
		printk("copy: open %s error\n", source);
		set_fs(fs);   //recovery
		return -1;
	}

	tar_fd = sys_open(target, O_WRONLY | O_CREAT, S_IRUSR | S_IWUSR);
	if (tar_fd == -1)
	{
		printk("copy: create %s error\n", target);
		set_fs(fs);   //recovery
		return -1;
	}
	printk("open file success\n");

	while (1)
	{
		Readsum = sys_read(sor_fd, buf, 512);
		if (Readsum == -1)
		{
			printk("copy: read %s error\n", source);
			set_fs(fs);   //recovery
			return -2;
		}
		else if (Readsum > 0)
		{
			WriteRes = sys_write(tar_fd, buf, Readsum);
			if (WriteRes != Readsum)
			{
				printk("copy: write %s error\n", target);
				set_fs(fs);   //recovery
				return -2;
			}
		}
		else if (Readsum == 0)
		{
			printk("copy: copy %s complete\n", source);
			break;
		}
	}

	// close file
	sys_close(tar_fd);
	sys_close(sor_fd);
	set_fs(fs);   //recovery

	return 0;
}

调用 sys_open,sys_read,sys_write 函数时,为保护内核空间,会首先

对参数进行检查,为防止发生段错误,需要通过设置 set_fs(KERNEL_DS)将其能

访问的空间限制扩大到内核空间,在完成内核函数调用返回用户空间时,需恢复

成原来的访问范围:

mm_segment_t fs; 

fs = get_fs(); //get access restriction value 

set_fs(get_ds()); //set kernel restriction value 

// close file 

sys_close(tar_fd); 

sys_close(sor_fd);

set_fs(fs); //recovery
  1. 编译内核
  2. 将生成的内核文件(arch/x86_64/boot/bzImage),复制进/boot中
  3. 添加引导到grub
  4. 重启,选择进入新的内核
  5. 测试系统调用
进程控制
函数名描述文件
fork创建一个新进程kernel/fork.c
clone按指定条件创建子进程kernel/fork.c
execve运行可执行文件fs/exec.c
exit中止进程kernel/exit.c
getpid获取进程标识号kernel/sys.c
pause挂起进程,等待信号kernel/signal.c
vfork创建一个子进程,以供执行新程序,常与execve等同时使用kernel/fork.c
wait等待子进程终止
waitpid等待指定子进程终止
文件系统控制
函数名描述文件
open打开文件fs/open.c
creat创建新文件fs/open.c
close关闭文件描述字fs/open.c
read读文件fs/read_write.c
write写文件fs/read_write.c
lseek移动文件指针fs/read_write.c
chdir改变当前工作目录fs/open.c
chmod改变文件方式fs/open.c
chown改变文件的属主或用户组fs/open.c
stat取文件状态信息fs/stat.c
mkdir创建目录fs/namei.c
link创建链接fs/namei.c
symlink创建符号链接fs/namei.c
unlink删除链接fs/namei.c
mount安装文件系统fs/namespace.c
umount卸下文件系统

IO多路复用

同步IO

一个进程可以监视多个描述符,一旦某个描述符就绪(一般是读就绪或者写就绪),能够通知程序进行相应的读写操作。需要在读写事件就绪后自己负责进行读写,也就是说这个读写过程是阻塞的。

异步I/O

无需自己负责进行读写,异步I/O的实现会负责把数据从内核拷贝到用户空间。

select
  1. select最大的缺陷就是单个进程所打开的FD是有一定限制的,它由FD_SETSIZE设置,默认值是1024**。**
  2. 对socket进行扫描时是线性扫描,即采用轮询的方法,效率较低。
  3. 需要维护一个用来存放大量fd的数据结构,这样会使得用户空间和内核空间在传递该结构时复制开销大。
poll

没有最大连接数的限制,原因是它是基于链表来存储的

epoll

epoll使用“事件”的就绪通知方式,通过epoll_ctl注册fd,一旦该fd就绪,内核就会采用类似callback的回调机制来激活该fd,epoll_wait便可以收到通知。

Linux常用命令

开关机与用户切换
命令功能
shutdown –h now立刻进行关机
shutdown –r now现在重新启动计算机
reboot现在重新启动计算机
su切换用户
passwd修改用户密码
logout用户注销
快捷命令
命令功能
tab命令补全
clear清屏
history查找历史命令
ctrl+k删除此处至末尾所有内容
ctrl+u删除此处至开始所有内容
ctrl+w清除当前行
ctrl+l清屏
ctrl+a光标跳到开头
ctrl+e光标跳到结尾
ctrl+左右箭头向左/右移动一个单词
常用工具命令
命令功能
wc文本统计
du文件大小统计
find文件检索命令
常用目录/文件操作命令
命令功能
ls -a展示当前目录下所有的文件
ll展示当前目录下文件的详细信息
ll -h友好的显示当前目录下文件的详细信息
pwd显示目前的目录
chmod u=rwx,g=rw,o=r aaa.txt修改文件/目录的权限
chmod ug+x test1.txt修改文件/目录的权限
nl带行号显示文件内容
less按页显示文件内容
系统常用操作命令
命令功能
ifconfig显示ip
kill -9 pid杀死某个进程
ps查当前进程
netstat [-a,-t,-u,-n,-l,-p]查看端口开放情况
df -h查看磁盘分区

计算机网络

协议分层

  • TCP/IP四层模型: 应用层、传输层、网络层、网络接口层。
  • 五层体系结构: 应用层、传输层(段)、网络层(包)、数据链路层(帧)、物理层(比特流)。

可靠数据传输

SW3

超时重传、序号、确认号、差错检测

GBN(回退N步)

累积确认、序号、校验和、超时重传

img

img

img

SR(选择重传)

img

img

TCP

  • 面向连接的点对点传输,可靠传输。

  • 最大报文段长度(MSS)取决于最大传输单元(MTU)(1500字节):TCP首部40字节,所以MSS为1460字节。

TCP报文段结构

TCP首部20字节,UDP首部8字节。

img

三次握手

①第一步:客户端的TCP首先向服务器端的TCP发送一个特殊的TCP报文段。该报文段不包含应用层数据。报文段首部标志位SYN=1,报文段称为SYN报文段。客户端随机地选择一个初始序号(client_isn)。报文段封装到一个IP数据报发送到服务器。

②第二步:TCP SYN报文段的IP数据报到达服务器主机,服务器为该TCP连接分配TCP缓存和变量,并向该客户TCP发送允许连接的报文段,这个报文段也不包含应用层数据。首先,SYN=1。其次,首部的确认号字段被置为client _ isn + 1。最后,服务器选择自己的初始序号 (server_isn)。该允许连接的报文段被称为SYNACK报文段(SYNACK segment)。

③第三步:收到SYNACK报文段后,客户也要给该连接分配缓存和变量。客户主机则向服务器发送另外一个报文段;这最后一个报文段对服务器的允许连接的报文段进行了确认,TCP报文段首部的确认字段server_isn + 1,SYN=0。三次握手的第三个阶段可以在报文段负载中携带客户到服务器的数据。

img

四次挥手
  • 客户应用进程发出一个关闭连接命令。

  • 客户端发送FIN连接释放报文后,服务端接收到这个报文立即回复ACK=1报文,此时客户端到服务端的单向连接已断开,而服务端进入close-wait状态,是为了等待所有数据都发送到客户端,再发送FIN连接释放报文。

  • 客户端接收到FIN释放报文后回复ACK=1报文,不会直接进入CLOSED状态,而是进入Time_Wait状态,需等待一个时间计时器设置的时间2MSL = 2*2mins = 4mins,这样做是为了确认最后A向B发送的报文能够到达,并等待本连接产生的所有报文段在网络中消失。

img

可靠性数据传输

①累积确认,发送方仅维持已发送但未确认最小序号(SendBase)和下一个发送序号(NextSeqNum)。

②接收方缓存失序报文。

③冗余ACK,快速重传。

④超时事件。

流量控制

发送方维护一个接收窗口,接收方通过指示其大小说明还有多少缓存空间。

  • LastByteRead:主机B上的应用进程从缓存读出的数据流的最后一个字节的编号。

  • LastByteRcvd:从网络中到达的并且已放入主机B接收缓存中的数据流的最后一个字节的编号。

TCP不允许已分配的缓存溢出,下式必须成立:
L a s t B y t e R c v d − L a s t B y t e R e a d = R e v B u f f e r LastByteRcvd-LastByteRead = RevBuffer LastByteRcvdLastByteRead=RevBuffer
接收窗口用rwnd表示,根据缓存可用空间的数量来设置:
r w n d = R e v B u f f e r − [ L a s t B y t e R e v d − L a s t B y t e R e a d ] rwnd=RevBuffer-[LastByteRevd-LastByteRead] rwnd=RevBuffer[LastByteRevdLastByteRead]
当主机B的接收窗口为0时,主机A继续发送只有一个字节数据的报文段。这些报文段将会被接收方确认。最终缓存将开始清空,并且确认报文里将包含一个非0的rwnd值。

拥塞控制(加性增、乘性减)
拥塞原因

①多跳路径

②路由器有限缓存

③排队时延、处理时延、传输时延

拥塞控制方法

拥塞窗口表示为cwnd,它对一个TCP发送方能向网络中发送流量的速率进行了限制。

①慢启动:TCP连接开始,cwnd的值通常初始置为一个MSS的较小值。发送速率起始慢,但在慢启动阶段以指数增长。

②拥塞避免:当到达或超过ssthresh的值时,每个RTT只将cwnd的值增加一个MSS,进入线性增长阶段。

③超时指示的丢包:TCP发送方将ssthresh(“慢启动阀值”)设置为cwnd/2,即ssthresh置为拥塞窗口值的一半。将cwnd设置为l并重新开始慢启动过程。

④3个冗余ACK指示丢包:TCP发送方将cwnd的值减半再加上3个MSS,将ssthresh的值记录为原cwnd的值的一半。接下来进入快速恢复状态。

UDP

特点
  • 无连接,不可靠,尽力而为的交付服务;

  • 无拥塞控制;

  • 支持一对一、一对多、多对一、多对多。

应用

用于丢包容忍率高,低时延的场合。

img

UDP报文段结构

img

Socket

类型
  • 流类型(Stream Sockets)。

    流式套接字提供了一种可靠的、面向连接的数据传输方法,使用传输控制协议TCP。

  • 数据报类型(Datagram Sockets)。

    数据报套接字提供了一种不可靠的、非连接的数据包传输方式,使用用户数据报协议UDP。

I/O模式
  • 阻塞式I/O(blocking I/O)

    在阻塞方式下,收发数据的函数在调用后一直要到传送完毕或者出错才能完成,在阻塞期间,除了等待网络操作的完成不能进行任何操作。阻塞式I/O是一个Winsock API函数的缺省行为。

  • 非阻塞式I/O(non-blocking I/O)

    对于非阻塞方式,Winsock API函数被调用后立即返回;当网络操作完成后,由Winsock给应用程序发送消息(Socket Notifications)通知操作完成,这时应用程序可以根据发送的消息中的参数对消息做出响应。Winsock提供了2种异步接受数据的方法:一种方法是使用BSD类型的函数select(),另外一种方法是使用Winsock提供的专用函数WSAAsyncSelect()。

连接流程

socket

HTTP

版本内容
HTTP/1.0传输内容格式不限制,增加PUT、PATCH、HEAD、 OPTIONS、DELETE命令
HTTP/1.1持久连接(长连接)、节约带宽、HOST域、管道机制、分块传输编码
HTTP/2多路复用、服务器推送、头信息压缩、二进制协议等
HTTP报文格式

http报文格式

HTTP特点
  1. 无状态:协议对客户端没有状态存储,对事物处理没有“记忆”能力,比如访问一个网站需要反复进行登录操作
  2. 无连接:HTTP/1.1之前,由于无状态特点,每次请求需要通过TCP三次握手四次挥手,和服务器重新建立连接。比如某个客户机在短时间多次请求同一个资源,服务器并不能区别是否已经响应过用户的请求,所以每次需要重新响应请求,需要耗费不必要的时间和流量。
  3. 基于请求和响应:基本的特性,由客户端发起请求,服务端响应
  4. 简单快速、灵活
  5. 通信使用明文、请求和响应不会对通信方进行确认、无法保护数据的完整性
针对无状态的一些解决策略
  1. 通过Cookie/Session技术
  2. HTTP/1.1持久连接(HTTP keep-alive)方法,只要任意一端没有明确提出断开连接,则保持TCP连接状态,在请求首部字段中的Connection: keep-alive即为表明使用了持久连接
HTTPS特点
  1. 内容加密:采用混合加密技术,中间者无法直接查看明文内容
  2. 验证身份:通过证书认证用户和服务器,确保数据发送到正确的客户机和服务器
  3. 保护数据完整性:防止传输的内容被中间人冒充或者篡改
HTTPS和HTTP的区别
  1. HTTPS是加密传输协议,HTTP是明文传输协议
  2. HTTPS需要用到SSL证书,而HTTP不用
  3. HTTPS比HTTP更加安全,对搜索引擎更友好,利于SEO
  4. HTTPS标准端口443,HTTP标准端口80
  5. HTTPS基于传输层,HTTP基于应用层
TLS/SSL工作原理

TLS的基本工作方式是,客户端使用非对称加密与服务器进行通信,利用证书实现身份验证并协商对称加密使用的密钥,然后对称加密算法采用协商密钥对信息以及信息摘要进行加密通信,不同的节点之间采用的对称密钥不同,从而可以保证信息只能通信双方获取。

常见状态码
状态码含义
200 OK表示从客户端发来的请求在服务器端被正确处理
301 moved permanently永久性重定向,表示资源已被分配了新的 URL
403 forbidden表示对请求资源的访问被服务器拒绝
404 not found表示在服务器上没有找到请求的资源
500 internal sever error表示服务器端在执行请求时发生了错误
501 Not Implemented服务器不支持请求的功能,无法完成请求
503 service unavailable表明服务器暂时处于超负载或正在停机维护,无法处理请求
Post 和 Get
  • Get 多用于无副作用,幂等的场景;Post 多用于副作用,不幂等的场景
  • Get 请求能缓存,Post 不能
  • Post 相对 Get 安全一点点,因为Get 请求都包含在 URL 里,且会被浏览器保存历史纪录
  • Post 可以通过 request body来传输比 Get 更多的数据,Get 没有这个技术
  • Post 支持更多的编码类型且不对数据类型限制

C++

static关键字

局部变量

静态局部变量使用static修饰符定义,即使在声明时未赋初值,编译器也会把它初始化为0。且静态局部变量存储于进程的全局数据区,即使函数返回,它的值也会保持不变。

普通局部变量存储于进程栈空间,使用完毕会立即释放。编译器一般不对普通局部变量进行初始化,也就是说它的值在初始时是不确定的,除非对其显式赋值。

全局变量

全局变量定义在函数体外部,在全局数据区分配存储空间,且编译器会自动对其初始化。

普通全局变量对整个工程可见,其他文件可以使用extern外部声明后直接使用。也就是说其他文件不能再定义一个与其相同名字的变量了(否则编译器会认为它们是同一个变量)。

静态全局变量仅对当前文件可见,其他文件不可访问,其他文件可以定义与其同名的变量,两者互不影响。

函数

静态函数只能在声明它的文件中可见,其他文件不能引用该函数。不同的文件可以使用相同名字的静态函数,互不影响。

静态数据成员

静态数据成员存储在全局数据区,静态数据成员在定义时分配存储空间,所以不能在类声明中定义

静态数据成员是类的成员,无论定义了多少个类的对象,静态数据成员的拷贝只有一个,且对该类的所有对象可见。也就是说任一对象都可以对静态数据成员进行操作。而对于非静态数据成员,每个对象都有自己的一份拷贝。

由于上面的原因,静态数据成员不属于任何对象,在没有类的实例时其作用域就可见,在没有任何对象时,就可以进行操作

和普通数据成员一样,静态数据成员也遵从public, protected, private访问规则

静态数据成员的初始化格式:<数据类型><类名>::<静态数据成员名>=<值>

类的静态数据成员有两种访问方式:<类对象名>.<静态数据成员名> 或 <类类型名>::<静态数据成员名>

静态成员函数

静态成员函数没有this指针,它无法访问属于类对象的非静态数据成员,也无法访问非静态成员函数,它只能调用其余的静态成员函数

出现在类体外的函数定义不能指定关键字static

非静态成员函数可以任意地访问静态成员函数和静态数据成员

堆栈

栈区(stack)

由编译器自动分配释放 ,存放函数的参数值,局部变量的值等。其操作方式类似于数据结构中的栈。

堆区(heap)

一般由程序员分配释放, 若程序员不释放,程序结束时可能由OS回收 。注意它与数据结构中的堆是两回事,分配方式倒是类似于链表,呵呵。

全局区(静态区)(static)

全局变量和静态变量的存储是放在一块的,初始化的全局变量和静态变量在一块区域, 未初始化的全局变量和未初始化的静态变量在相邻的另一块区域。 - 程序结束后有系统释放

文字常量区

常量字符串就是放在这里的。 程序结束后由系统释放

程序代码区

存放函数体的二进制代码。

空类、空结构大小

空类、空结构大小为1字节
#include<iostream>
#include<stdio.h>

struct test1{};	//空结构体
class test2{}; 	//空类
                                                                    
int main()
{
   test1 a;
   test2 b;
   std::cout<<"size of empty struct: "<<sizeof(a)<<std::endl;
   std::cout<<"size of empty class: "<<sizeof(b)<<std::endl;
   return 0;
}
包含构造函数、析构函数、成员函数类大小为 1字节
#include<iostream>    
#include<stdio.h>    
    
struct test1{};//空结构    
class test2    
{//包括构造函数和析构函数的类    
  public:   
  test2(){};    
  ~test2(){};    
};
class test3{//包含构造函数和析构函数,成员函数
public:
	test3(){};
	~test3(){};
	void printt(){
		printf(".");
	}
};    
    
int main()    
{    
  test1 a;    
  test2 b;
  test3 c;    
  std::cout<<"空结构体大小: "<<sizeof(a)<<std::endl;    
  std::cout<<"空类大小: "<<sizeof(b)<<std::endl;    
  std::cout<<"包含构造函数、析构函数、成员函数类大小: "<<sizeof(c)<<std::endl;

  return 0;    
} 
包含构造函数和虚析构函数的类、一个int成员类:一个机器字长
#include<iostream>    
#include<stdio.h>    
    
struct test1{};//空结构    
class test2    
{//包含构造函数和虚析构函数的类    
  public:                                                           
  test2(){};    
  virtual ~test2(){};
};
class test3
{
public:
	test3(){};
	int a = 0;
};    
    
int main()    
{    
  test1 a;    
  test2 b;    
  test3 c;
  std::cout<<"空结构体大小:"<<sizeof(a)<<std::endl;    
  std::cout<<"包含构造函数和虚析构函数的类大小:"<<sizeof(b)<<std::endl;   
  std::cout<<"包含一个int成员:"<<sizeof(c)<<std::endl;
  return 0;    
} 

C++11

右值引用
  • 可以取地址的、有名字的就是左值,反之,不能取地址的、没有名字的就是右值。

  • 右值表示字面常量、表达式、函数的非引用返回值

左值引用

const 类型 &”为 “万能”的引用类型,它可以接受非常量左值、常量左值、右值对其进行初始化。

int &a = 2;       // 左值引用绑定到右值,编译失败, err
int b = 2;        // 非常量左值
const int &c = b; // 常量左值引用绑定到非常量左值,编译通过, ok
const int d = 2;  // 常量左值
const int &e = d; // 常量左值引用绑定到常量左值,编译通过, ok
const int &b = 2; // 常量左值引用绑定到右值,编程通过, ok
右值引用
int && r1 = 22;
int x = 5;
int y = 8;
int && r2 = x + y;
T && a = ReturnRvalue();

右值引用是不能够绑定到任何的左值的

int c;
int && d = c; //err
智能指针

简介:
C++ 语言没有自动内存回收机制,程序员每次 new 出来的内存都要手动 delete。程序员忘记 delete,流程太复杂,最终导致没有 delete,异常导致程序过早退出,没有执行 delete 的情况并不罕见。

7 种智能指针:

std::auto_ptr

std::auto_ptr 可用来管理单个对象的堆内存,但是,请注意如下几点:

(1)尽量不要使用“operator=”。如果使用了,请不要再使用先前对象。
(2)记住 release() 函数不会释放对象,仅仅归还所有权。
(3)std::auto_ptr 最好不要当成参数传递
(4)由于 std::auto_ptr 的“operator=”问题,有其管理的对象不能放入 std::vector 等容器中

boost::scoped_ptr

(1)独享内存所有权
(2)没有 release() 函数
(3)不能用于处理指针复制,参数传递
(4)管理单个对象的堆内存

boost::shared_ptr

(1)专门用于共享所有权,由于要共享所有权,在内部使用了引用计数
(2)没有 release() 函数
(3)提供了一个函数 use_count() ,此函数返回 boost::shared_ptr 内部的引用计数
(4)管理单个对象的堆内存

boost::scoped_array

(1)用于管理动态数组,独享所有权
(2)使用内存数组来初始化
(3)没有重载 operator*
(4)没有 release 函数

boost::shared_array

(1)内部使用了引用计数
(2)使用内存数组来初始化
(3)没有重载 operator*

boost::weak_ptr

boost::weak_ptr 主要用在软件架构设计

boost::intrusive_ptr

插入式的智能指针,内部不含有引用计数,需要程序员自己加入引用计数

总结

  • 在可以使用 boost 库的场合下,拒绝使用 std::auto_ptr,因为其不仅不符合 C++ 编程思想,而且极容易出错。

  • 在确定对象无需共享的情况下,使用 boost::scoped_ptr(当然动态数组使用 boost::scoped_array)。

  • 在对象需要共享的情况下,使用 boost::shared_ptr(当然动态数组使用 boost::shared_array)。

  • 在需要访问 boost::shared_ptr 对象,而又不想改变其引用计数的情况下,使用 boost::weak_ptr,一般常用于软件框架设计中。

  • 最后一点,也是要求最苛刻一点:在你的代码中,不要出现 delete 关键字(或 C 语言的 free 函数),因为可以用智能指针去管理。

auto自动类型推导
#include <iostream>
using namespace std;

int func(int, int);
auto func2(int, int) -> int;

template<typename T1, typename T2>
auto sum(const T1 & t1, const T2 & t2) -> decltype(t1 + t2)
{
    return t1 + t2;
}

template <typename T1, typename T2>
auto mul(const T1 & t1, const T2 & t2) -> decltype(t1 * t2)
{
    return t1 * t2;
}

int main()
{
    auto a = 3;
    auto b = 4L;
    auto pi = 3.14;

    auto c = mul( sum(a, b), pi );
    cout << c << endl;  // 21.98

    return 0;
}
void fun(auto x =1) {}  // 1: auto函数参数,有些编译器无法通过编译

struct str
{
    auto var = 10;   // 2: auto非静态成员变量,无法通过编译
};

int main()
{
    char x[3];
    auto y = x;
    auto z[3] = x; // 3: auto数组,无法通过编译

    // 4: auto模板参数(实例化时),无法通过编译
    vector<auto> x = {1};

    return 0;
}
decltype
#include <typeinfo>
#include <iostream>
#include <vector>
using namespace std;

int main()
{
    int i;
    decltype(i) j = 0;
    cout << typeid(j).name() << endl;   // 打印出"i", g++表示integer

    float a;
    double b;
    decltype(a + b) c;
    cout << typeid(c).name() << endl;   // 打印出"d", g++表示double

    vector<int> vec;
    typedef decltype(vec.begin()) vectype; // decltype(vec.begin()) 改名为 vectype

    vectype k;  // 这是auto无法做到的
    //decltype(vec.begin()) k;  // 这是auto无法做到的
    for (k = vec.begin(); k < vec.end(); k++)
    {
        // 做一些事情
    }

    enum {Ok, Error, Warning}flag;   // 匿名的枚举变量
    decltype(flag) tmp = Ok;

    return 0;
}
defaulted 函数

C++ 的类有四类特殊成员函数,它们分别是:默认构造函数、析构函数、拷贝构造函数以及拷贝赋值运算符

这些类的特殊成员函数负责创建、初始化、销毁,或者拷贝类的对象。如果程序员没有显式地为一个类定义某个特殊成员函数,而又需要用到该特殊成员函数时,则编译器会隐式的为这个类生成一个默认的特殊成员函数。

#include <iostream>
#include <string>
using namespace std;

class X
{ 
 public:  
   X() = default; //Inline defaulted 默认构造函数
   X(const X&); 
   X& operator = (const X&); 
   ~X() = default;  //Inline defaulted 析构函数
 }; 

 X::X(const X&) = default;  //Out-of-line defaulted 拷贝构造函数
 X& X::operator = (const X&) = default;     //Out-of-line defaulted  拷贝赋值操作符

class Y{};

 int main(){
 	X obj1;
 	Y obj2;
 	cout << "add defaulted functions class X: " << sizeof(obj1) << endl;
 	cout << "empty class Y size: " << sizeof(obj2) << endl;
 	return 0;
 }
deleted 函数

显式的禁用某个函数,C++11 标准引入了一个新特性:deleted 函数。

class X
{           
public: 
    X(); 
    X(const X&) = delete;  // 声明拷贝构造函数为 deleted 函数
    X& operator = (const X &) = delete; // 声明拷贝赋值操作符为 deleted 函数
}; 

 int main()
 { 
      X obj1; 
      X obj2=obj1;   // 错误,拷贝构造函数被禁用
      X obj3; 
      obj3=obj1;     // 错误,拷贝赋值操作符被禁用
 }
初始化
类内成员初始化
class Mem
{
public:
    Mem(int i): m(i){} //初始化列表给m初始化
    int m;
};
class Group
{
public:
    Group(){}

private:
    int data = 1;       // 使用"="初始化非静态普通成员,也可以 int data{1};
    Mem mem{2}; // 对象成员,创建对象时,可以使用{}来调用构造函数
    string name{"mike"};
};
列表初始化
int a[]{1, 3, 5};
int i = {1};  
int j{3}; 
初始化结构体类型
struct Person  
{  
  std::string name;  
  int age;  
};  

int main()  
{  
    Person p = {"Frank", 25};  
    std::cout << p.name << " : " << p.age << std::endl;  
} 
std的初始化
std::vector<int> ivec1(3, 5);  
std::vector<int> ivec2 = {5, 5, 5};  
std::vector<int> ivec3 = {1,2,3,4,5}; //不使用列表初始化用构造函数难以实现 
防止类型收窄
int main(void)
{
    const int x = 1024;
    const int y = 10;

    char a = x;                 // 收窄,但可以通过编译
    char* b = new char(1024);   // 收窄,但可以通过编译

    char c = { x };             // err, 收窄,无法通过编译
    char d = { y };             // 可以通过编译
    unsigned char e{ -1 };      // err,收窄,无法通过编译

    float f{ 7 };               // 可以通过编译
    int g{ 2.0f };              // err,收窄,无法通过编译
    float * h = new float{ 1e48 };  // err,收窄,无法通过编译
    float i = 1.2l;                 // 可以通过编译

    return 0;
}
基于范围的for循环
#include <iostream>
using namespace std;

int main()
{
    int a[] = { 1, 2, 3, 4, 5 };
    int n = sizeof(a) / sizeof(*a); //元素个数

    for (int i = 0; i < n; ++i)
    {
        int tmp = a[i];
        cout << tmp << ", ";
    }
    cout << endl;

    for (int tmp : a)
    {
        cout << tmp << ", ";
    }
    cout << endl;

    //使用引用访问元素
    for (int i = 0; i < n; ++i)
    {
        int &tmp = a[i];
        tmp = 2 * tmp;
        cout << tmp << ", ";
    }
    cout << endl;

    for (int &tmp : a)
    {
        tmp = 2 * tmp;
        cout << tmp << ", ";
    }
    cout << endl;

    return 0;
}
nullptr
#include <iostream>
using namespace std;

void func(int a)
{
    cout << __LINE__ << " a = " << a <<endl;
}

void func(int *p)
{
     cout << __LINE__ << " p = " << p <<endl;
}

int main()
{
    int *p1 = nullptr;
    int *p2 = NULL;

    if(p1 == p2)
    {
        cout << "equal\n";
    }

    //int a = nullptr; //err, 编译失败,nullptr不能转型为int

    func(0); //调用func(int), 就算写NULL,也是调用这个
    func(nullptr);
    cout << sizeof(nullptr) << endl;

    return 0;
}
lambda表达式
#include <iostream>
using namespace std;

class MyFunctor
{
public:
    MyFunctor(int tmp) : round(tmp) {}
    int operator()(int tmp) { return tmp + round; }

private:
    int round;
};

int main()
{
    //仿函数
    int round = 2;
    MyFunctor f1(round);//调用构造函数
    cout << "result1 = " << f1(1) << endl; //operator()(int tmp)

    //lambda表达式
    auto f2 = [=](int tmp) -> int { return tmp + round; } ;
    cout << "result2 = " << f2(1) << endl;

    return 0;
}
#include <vector>
#include <algorithm> //std::for_each
#include <iostream>
using namespace std;

vector<int> nums;
vector<int> largeNums;

class LNums
{
public:
    LNums(int u): ubound(u){} //构造函数

    void operator () (int i) const
    {//仿函数
        if (i > ubound)
        {
            largeNums.push_back(i);
        }
    }
private:
    int ubound;
};

int main()
{
    //初始化数据
    for(auto i = 0; i < 10; ++i)
    {
        nums.push_back(i);
    }
    int ubound = 5;

    //1、传统的for循环
    for (auto itr = nums.begin(); itr != nums.end(); ++itr)
    {
        if (*itr > ubound)
        {
            largeNums.push_back(*itr);
        }
    }

    //2、使用仿函数
    for_each(nums.begin(), nums.end(), LNums(ubound));

    //3、使用lambda函数和算法for_each
    for_each(nums.begin(), nums.end(), [=](int i)
        {
            if (i > ubound)
            {
                largeNums.push_back(i);
            }
        }
        );

    //4、遍历元素
    for_each(largeNums.begin(), largeNums.end(), [=](int i)
        {
            cout << i << ", ";
        }
        );
    cout << endl;

    return 0;
}
线程
强类型枚举
字符串字面值
移动语义
可变参数模板

C++编译链接

预编译
  • 由源文件“.cpp/.c”生成“.i”文件,这是在预编译阶段完成的;gcc -E .cpp/.c —>.i

  • 主要功能

    • 展开所有的宏定义,消除“#define”;
    • 处理所有的预编译指令,比如#if、#ifdef等;
    • 处理#include预编译指令,将包含文件插入到该预编译的位置;
    • 删除所有的注释“/**/”、"//"等;
    • 添加行号和文件名标识,以便于编译时编译器产生调试用的行号信息以及错误提醒;
    • 保留所有的#program编译指令,原因是编译器要使用它们;
  • 缺点:不进行任何安全性及合法性检查

编译—核心

编译过程就是把经过预编译生成的文件进行一系列语法分析、词法分析、语义分析优化后生成相应的汇编代码文件。

  • 由“.i”文件生成“.s”文件,这是在编译阶段完成的;gcc -S .i —>.s

  • 主要功能

    • 词法分析:将源代码文件的字符序列划分为一系列的记号,一般词法分析产生的记号有:标识符、关键字、数字、字符串、特殊符号(加号、等号);在识别记号的同时也将标识符放好符号表、将数字、字符放入到文字表等;有一个lex程序可以实现词法扫描,会按照之前定义好的词法规则将输入的字符串分割成记号,所以编译器不需要独立的词法扫描器;
    • 语法分析:语法分析器将对产生的记号进行语法分析,产生语法树----就是以表达式尾节点的树,一步步判断如何执行表达式操作。
    • 语义分析:由语法阶段完成分析的并没有赋予表达式或者其他实际的意义,比如乘法、加法、减法,必须经过语义阶段才能赋予其真正的意义;语义分析主要分为静态语义和动态语义两种;静态语义通常包括声明和类型的匹配、类型的转换。比如当一个浮点型的表达式赋值给一个整型的表达式时,其中隐含了一个浮点型到整型转换的过程。只要存在类型不匹配编译器会报错。经过语义分析后的语法树的所有表达式都有了类型。动态语义分析只有在运行阶段才能确定;
汇编:生成可重定位的二进制文件;(.obj文件)
  • 由“.s”文件生成的“.obj”文件;gcc -c .s–>.o;

  • 此文件中生成符号表,能够产生符号的有:所有数据都要产生符号、指令只产生一个符号(函数名);

链接
  • 合并所有“.obj”文件的段并调整段偏移和段长度(按照段的属性合并,属性可以是“可读可写”、“只读”、“可读可执行”,合并后将相同属性的组织在一个页面内,比较节省空间),合并符号表,进行符号解析完成后给符号分配地址;其中符号解析的意思是:所有.obj符号表中对符号引用的地方都要找到该符号定义的地方。在编译阶段,有数据的地方都是0地址,有函数的额地方都是下一行指令的偏移量-4(由于指针是4字节);可执行文件以页面对齐。

  • 符号的重定位(链接核心):将符号分配的虚拟地址写回原先未分配正确地址的地方

Linux下的ELF文件主要有以下四种
可重定位文件.obj

这种文件包括数据和指令,可以被链接成为可执行文件(.exe)或者共享目标文件(.so),静态链接库可以归为这一类;

可执行文件.exe

这种文件包含了可以直接运行的程序,它的代表就是ELF可执行文件,他们一般都没有扩展名;

共享目标文件.so

这种文件包含了数据和指令,可以在以下两种情况下使用:一是链接器使用这种文件与其他可重定位文件和共享目标文件链接,二是动态链接器将几个共享目标文件与可执行文件结合,作为进程映像的一部分使用。

核心转储文件

当进程意外终止时,系统可以将该进程的地址空间的内容及种植的一些信息转储到核心文件中,比如core dump文件。

DataBase

基础

事务的概念和特性

事务(Transaction)是一个操作序列,不可分割的工作单位,以BEGIN TRANSACTION开始,以ROLLBACK/COMMIT结束

特性:原子性一致性隔离性持久性

并发一致性问题
  • 丢失修改:一个事务对数据进行了修改,在事务提交之前,另一个事务对同一个数据进行了修改,覆盖了之前的修改;
  • 脏读(Dirty Read):一个事务读取了被另一个事务修改、但未提交(进行了回滚)的数据,造成两个事务得到的数据不一致;
  • 不可重复读(Nonrepeatable Read):在同一个事务中,某查询操作在一个时间读取某一行数据和之后一个时间读取该行数据,发现数据已经发生修改(可能被更新或删除了);
  • 幻读(Phantom Read):当同一查询多次执行时,由于其它事务在这个数据范围内执行了插入操作,会导致每次返回不同的结果集(和不可重复读的区别:针对的是一个数据整体/范围;并且需要是插入操作)
四种隔离级别
  • 未提交读(Read Uncommited):在一个事务提交之前,它的执行结果对其它事务也是可见的。会导致脏读、不可重复读、幻读;
  • 提交读(Read Commited):一个事务只能看见已经提交的事务所作的改变。可避免脏读问题;
  • 可重复读(Repeatable Read):可以确保同一个事务在多次读取同样的数据时得到相同的结果。(MySQL的默认隔离级别)。可避免不可重复读;
  • 可串行化(Serializable):强制事务串行执行,使之不可能相互冲突,从而解决幻读问题。可能导致大量的超时现象和锁竞争,实际很少使用。
乐观锁和悲观锁
  • 悲观锁:认为数据随时会被修改,因此每次读取数据之前都会上锁,防止其它事务读取或修改数据;应用于数据更新比较频繁的场景;
  • 乐观锁:操作数据时不会上锁,但是更新时会判断在此期间有没有别的事务更新这个数据,若被更新过,则失败重试;适用于读多写少的场景。乐观锁的实现方式有:
    • 加一个版本号或者时间戳字段,每次数据更新时同时更新这个字段;
    • 先读取想要更新的字段或者所有字段,更新的时候比较一下,只有字段没有变化才进行更新
封锁类型
  • 排它锁
  • 共享锁
  • 意向锁
MVCC(多版本并发控制)

MVCC在每行记录后面都保存有两个隐藏的列,用来存储创建版本号删除版本号

  • 创建版本号:创建一个数据行时的事务版本号(事务版本号:事务开始时的系统版本号;系统版本号:每开始一个新的事务,系统版本号就会自动递增);
  • 删除版本号:删除操作时的事务版本号;
  • 各种操作:
    • 插入操作时,记录创建版本号;
    • 删除操作时,记录删除版本号;
    • 更新操作时,先记录删除版本号,再新增一行记录创建版本号;
    • 查询操作时,要符合以下条件才能被查询出来:删除版本号未定义或大于当前事务版本号(删除操作是在当前事务启动之后做的);创建版本号小于或等于当前事务版本号(创建操作是事务完成或者在事务启动之前完成)

通过版本号减少了锁的争用,提高了系统性能;可以实现提交读可重复读两种隔离级别,未提交读无需使用MVCC

表连接
  • 内连接(Inner Join):仅将两个表中满足连接条件的行组合起来作为结果集
    • 自然连接:只考虑属性相同的元组对;
    • 等值连接:给定条件进行查询
  • 外连接(Outer Join)
    • 左连接:左边表的所有数据都有显示出来,右边的表数据只显示共同有的那部分,没有对应的部分补NULL;
    • 右连接:和左连接相反;
    • 全外连接(Full Outer Join):查询出左表和右表所有数据,但是去除两表的重复数据
  • 交叉连接(Cross Join):返回两表的笛卡尔积(对于所含数据分别为m、n的表,返回m*n的结果)
触发器

由事件(比如INSERT/UPDATE/DELETE)来触发运行的操作(不能被直接调用,不能接收参数)。在数据库里以独立的对象存储,用于保证数据完整性(比如可以检验或转换数据)。

约束类型
  • 主键约束(Primary Key)
  • 唯一约束(Unique)
  • 检查约束(check)
  • 非空约束(NOT NULL)
  • 外键约束(Foreign Key)
视图

从数据库的基本表中通过查询选取出来的数据组成的虚拟表(数据库中存放视图的定义)。可以对其进行增/删/改/查等操作。视图是对若干张基本表的引用,一张虚表,查询语句执行的结果,不存储具体的数据(基本表数据发生了改变,视图也会跟着改变);可以跟基本表一样,进行增删改查操作(ps:增删改操作有条件限制);如连表查询产生的视图无法进行,对视图的增删改会影响原表的数据。

  • 通过只给用户访问视图的权限,保证数据的安全性
  • 简化复杂的SQL操作,隐藏数据的复杂性(比如复杂的连接);
游标

用于定位在查询返回的结果集的特定行,以对特定行进行操作。使用游标可以方便地对结果集进行移动遍历,根据需要滚动或对浏览/修改任意行中的数据。主要用于交互式应用。

数据库索引的实现原理
使用B树和B+树的比较

InnoDB的索引使用的是B+树实现,B+树对比B树的好处:

  • IO次数少:B+树的中间结点只存放索引,数据都存在叶结点中,因此中间结点可以存更多的数据,让索引树更加矮胖;
  • 范围查询效率更高:B树需要中序遍历整个树,只B+树需要遍历叶结点中的链表;
  • 查询效率更加稳定:每次查询都需要从根结点到叶结点,路径长度相同,所以每次查询的效率都差不多
使用B树索引和哈希索引的比较

哈希索引能以 O(1) 时间进行查找,但是只支持精确查找,无法用于部分查找和范围查找,无法用于排序与分组;B树索引支持大于小于等于查找,范围查找。哈希索引遇到大量哈希值相等的情况后查找效率会降低。哈希索引不支持数据的排序。

索引的优缺点
  • 大大加快了数据的检索速度
  • 可以显著减少查询中分组和排序的时间;
  • 通过创建唯一性索引,可以保证数据库表中每一行数据的唯一性;
  • 将随机 I/O 变为顺序 I/O(B+Tree 索引是有序的,会将相邻的数据都存储在一起)

缺点:建立和维护索引耗费时间空间,更新索引很慢。

创建索引
  • 某列经常作为最大最小值;
  • 经常被查询的字段;
  • 经常用作表连接的字段;
  • 经常出现在ORDER BY/GROUP BY/DISDINCT后面的字段

MySQL

Effective MySQL

Effective MySQL

高性能MySQL

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-wsMF4fbr-1649384040679)(http://mi_chuan.gitee.io/blog/高性能MySQL.png)]

InnoDB存储引擎

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-qPCiMoGk-1649384040680)(http://mi_chuan.gitee.io/blog/InnoDB存储引擎.png)]

参考文献

网址

计算机基础知识

Linux教程

C++

C++11新特性学习

《深入理解C++11》

《Effective STL》

《C Primer Plus》

《深度探索C++对象模型》

《Effective C++》

OS

《精通Linux》

《Unix系统调用》

《Linux命令行与shell脚本编程大全》

《Linux+Shell脚本攻略》

《数据结构(C++语言版)》

《现代操作系统》

Computer Network

《计算机网络:自顶向下方法》

DataBase

《Effective MySQL》

《高性能MySQL》

《InnoDB存储引擎》

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值