自定义博客皮肤VIP专享

*博客头图:

格式为PNG、JPG,宽度*高度大于1920*100像素,不超过2MB,主视觉建议放在右侧,请参照线上博客头图

请上传大于1920*100像素的图片!

博客底图:

图片格式为PNG、JPG,不超过1MB,可上下左右平铺至整个背景

栏目图:

图片格式为PNG、JPG,图片宽度*高度为300*38像素,不超过0.5MB

主标题颜色:

RGB颜色,例如:#AFAFAF

Hover:

RGB颜色,例如:#AFAFAF

副标题颜色:

RGB颜色,例如:#AFAFAF

自定义博客皮肤

-+
  • 博客(49)
  • 资源 (8)
  • 收藏
  • 关注

转载 brpc源码阅读

好久都没有更新了,最近主要在看brpc的源码,确实非常精彩,对于锁、lock-free和wait-free的策略令人拍案叫绝,下面是最近看到的一篇文章,总结的挺好,推荐给大家看看。brpc线程模型和网络模型...

2021-07-04 21:41:15 271

原创 c++编译过程

前言c++是一种编译型语言,在运行之前需要先经过编译器编译,虽然编译器由不同的厂家实现,但是他们需要遵守共同的规范,本文就介绍下c++的编译过程。编译的四个阶段先展示下本文实验用到的代码://main.cpp#include <stdio.h>int main(){ printf ("hello, world"); return 0;}预处理阶段预处理阶段主要处理带“#”号的文件,如#include 或#define等信息,对于include的头文件,预处理

2021-06-07 15:52:58 482

原创 设计模式(二):工厂模式

前言工厂模式提供了一种创建对象的最佳方式,我们在创建对象时不会对客户端暴露创建逻辑,并且是通过使用一个共同的接口来指向新创建的对象。工厂模式分为简单工厂模式、工厂方法模式和抽象工厂模式,c++工厂模式主要利用了多态的特性,有关多态的内容可以参考c++多态。简单工厂模式简单工厂模式包括三个组件:工厂类:用于创建一个指定具体实例对象。具体产品类:工厂模式创建的对象。抽象产品类:具体产品继承的父类或实现的接口。可以通过一个实例来了解简单工厂模式,假设我们需要通过工厂模式生产衣服,工厂类定义为cl

2021-06-06 13:24:51 146

原创 虚拟地址空间(二)

前言在我最开始写博客的时候,第一篇就介绍了虚拟地址空间,但当时受限于自己的水平,并且很多内容当时还没有介绍,所以当时写的比较简单,这篇文章算是还愿了吧。/proc/{pid}/mapslinux的根目录下有很多目录,比如/etc通常是存放参数文件的,/bin通常存放二进制的可执行程序,/lib通常存放库文件,/include存放头文件,/proc存放的则是进程相关的文件。对每个正在运行的文件,/proc下都有一个文件名为该进程pid号的文件夹,例如有一个pid为10086的进程正在运行,那么就存在/p

2021-06-06 00:38:21 162

原创 快速排序及其优化

快排的原理快排是综合性能最好的算法,也凭借优秀的性能成为常用的排序算法。快排基于分治的策略,每次选择一个基准值,将数组分为小于基准值和大于基准值两部分,然后对基准左右两侧的数组递归调用这个过程,直至找到所有元素的位置。上面的图片展示了快排的基本过程,但问题是如何找到基准值的位置?最简单的想法是遍历所有元素,但这样就已经排好序了,失去了快排的意义。快排使用两端逼近的思路来找到基准值的位置,相关代码可以这样组织:vector<int> nums//待排序数组int pivot; //基

2021-06-04 22:11:29 1130 1

原创 死锁条件、避免、检测和解除

什么是死锁?如果一个进程集合中的每个进程都在等待只能由此集合中的其他进程才能引发的事件,而无限期陷入僵持的局面称为死锁,例如下面的模型:此时进程A和进程B就永远处于这个状态,称进程A和进程B处于死锁状态。上述的概念换成线程也同样适用,为叙述简洁,这里就使用进程来叙述。死锁条件死锁的产生必须满足四个条件:资源互斥条件:使用资源必须是独占式地,资源本身不可分割,同一资源在某时刻至多只能被一个进程使用;请求和保持条件:进程在请求资源时,不释放目前占有的资源;不可剥夺条件:已经被某进程获取的资源在

2021-06-03 23:27:35 600

原创 epoll 的LT与ET实例

epoll的数据结构与函数epoll需要使用struct epoll_event 来记录需要监听的事件,将需要监听的文件描述符记录在epoll_event.data.fd中,将需要监听的事件记录在epoll_event.events中,常见的监听事件包括:EPOLLIN:读事件;EPOLLOUT:写事件;EPOLLRDHUP:读关闭事件,本端调用shutdown(SHUT_RD)或对端调用shutdown(SHUT_WR);EPOLLHUP:读写关闭事件,本端调用shutdown(SHUT_RD

2021-06-03 17:26:27 198

原创 select使用实例

select函数是多路复用的一种,本文我们给出一个select的通信实例,看下select的代码如何组织,先上代码:#include <unistd.h>#include <iostream>#include <string>#include <sys/socket.h>#include <errno.h>#include <cstring>#include <netinet/in.h>#include &l

2021-06-02 00:28:31 663 3

原创 五种网络IO模型

前言网络IO指的是网络通讯时,socket读取的过程,具体包括数据从硬件接口传递到内核态,再从内核态拷贝到用户态。在服务器中存在大量的网络连接,这些网络连接带来了大量的IO消耗,因此了解网络模型对我们编写服务器程序是十分必要的。阻塞式IO模型在TCP(三)中,我们给出了一个利用TCP进行client-server通信的例子,在这个例子中,server调用listen后,将等待client的连接,如果没有client连接,server将一直阻塞。这种阻塞等待IO信号的方式,被称为阻塞式IO,阻塞式IO

2021-06-01 00:38:16 87

原创 HTTPS

在TCP/IP网络模型和OSI网络模型中,已经介绍了HTTP相关的内容,但HTTP是明文传输,在网络中很容易被不法分子获取,因此现在广泛使用HTTPS协议代替HTTP协议。HTTPS协议默认监听443端口,而HTTP协议默认监听80端口。此外,HTTPS将HTTP报文进行了加密,HTTPS报文实际上是加密后的HTTP报文,这种加密通过SSL(或TLS,TLS是SSL的正式版本)来完成,即HTTPS=HTTP+SSL。在了解SSL如何加密之前,我们先了解一下有哪些加密方式。对称加密对称加密指通信双方使

2021-05-31 00:39:07 237

原创 TCP/IP网络模型和OSI网络模型

网络模型的结构OSI(Open System Interconnection)是由国际标准化组织(ISO)制定的网络模型,该模型定义了不同计算机如何互相连接,是设计和描述计算机网络通信的基本框架。OSI模型将网络通信分为7层:OSI仅是一个理论上的模型,实际使用的是TCP/IP网络模型,TCP/IP模型将OSI模型的应用层,表示层和会话层合并为应用层:由于TCP/IP是实际使用的模型,后续我们就以TCP/IP模型为例进行介绍,OSI也是基本相似的过程。将网络协议分层的原因是,网络是一个非常复杂的

2021-05-30 22:58:28 703

原创 TCP(四)

前言tcp大致的知识前面都已经介绍完毕了,本文我们再回答一些tcp的细节问题。client连接server时,如果server未开启,或监听的端口不是client连接的端口,会发生什么事情?我们还是使用TCP(三)中使用的server和client程序,先不启动server,仅启动client,使用wireshark抓包。程序运行失败,error被设置为connection refused,下面我们看看wireshark:从抓包情况来看,client向端口9007发起连接,此时操作系统为cli

2021-05-30 19:00:46 207

原创 TCP(三)

TCP server在cs模型中,server只需要等待client的连接,因此server包括下面四个步骤:调用socket(),初始化一个socket实例,调用bind(),将socket绑定到网卡上,调用listen(),等待客户端的连接,与客户端三次握手后,调用accept()。server的代码如下://server.cpp#include <unistd.h>#include <arpa/inet.h>#include <sys/types

2021-05-29 23:45:32 79

原创 TCP(二)

前言TCP是面向连接的协议,两个端口希望通过TCP通信时,需要在两个端口间先建立传输链路,这被称为三次握手过程,结束通信时,需要断开端口间的传输链路,这被称为四次挥手过程,本文我们来详细了解这两个过程。三次握手三次握手的过程可以结合下图来了解:在第一次握手之前,client和server都处于close状态,client发起对server的连接时,将发送第一次握手请求。第一次握手请求的内容包括syn标志位的设置和seq序列号的发送,我们在TCP(一)中介绍过,syn标志位的设置意味这这个包是用于

2021-05-29 18:04:23 129

原创 TCP(一)

前言TCP协议是传输层的重要协议,TCP协议保证将数据包完整、可靠地送达对端,是网络中最重要的协议之一,本文我们结合TCP的首部格式来了解TCP。TCP的首部格式TCP首部包括20bytes的固定字段和64bytes的可选字段(如下图),16位的源端口号和目的端口号是传输层的基本要求,下面我们通过这些字段来理解TCP的机制。应答TCP为每个数据包添加自增的32位序列号,在接收到数据包时,TCP也需要根据包的序列号向对端发送应答,使用client-server模型,可以通过下图来表达这个过程,其中

2021-05-27 21:46:38 239

原创 进程调度算法

前言在进程与线程中,我们介绍过进程的五种状态,还介绍了线程调度状态和调度算法与进程是一样的,本文我们就介绍一下常见的进程调度算法,当然按照我们之前的介绍,这些算法对线程同样适用。先来先服务(First Come First Serve)先来先服务的调度方式就和这个算法的名字一样,使用一个队列存储就绪态进程的task_struct,cpu空闲时,就从队列中取出第一个task_struct进行调度,在这个进程处理运行态时,别的进程不可抢占cpu,只能在就绪队列中等待被调度。先来先服务算法优点是很好理解,

2021-05-27 00:26:57 116

原创 STL(八):关联式容器

前言关联式容器与序列式容器不同,元素插入关联式容器后,插入位置是由容器采用一定的算法计算的,与插入的时间无关。本文介绍STL中哈系表、集合和map等关联式容器。哈希表hashtable是SGI STL中的哈希表,标准库中使用unordered_map代替hashtable,但unordered_map也是以hashtable为基础的。哈希表是一种查找操作只需要o(1)时间复杂度的数据结构,哈希表由一串连续的的bucket构成,每个bucket上挂着一个链表,链表中储存的就是用户的数据,哈系表通过散列函

2021-05-26 22:25:44 157

原创 不同形态的二叉树

满二叉树对二叉树来说,第i层的节点个数最多为2^(i-1)个,如果二叉树的每一层的节点个数都达到最大值,即叶子节点全部在最后一层,非叶子节点一定有左右孩子,这种二叉树称为满二叉树,下面展示了一棵满二叉树。完全二叉树完全二叉树的判断条件没有满二叉树那么苛刻,完全二叉树要求最下面两层的节点,可以没有孩子节点,也可以仅有一个孩子,但最下层的叶子节点必须都在左侧,比如下面的树:需要注意最下层叶子节点必须在左侧,像下面的树违反了这个规则,不是完全二叉树:二叉搜索树二叉搜索树是一种非常实用的二叉树,对

2021-05-26 15:55:46 817

原创 二叉树

前言树形数据结构是应用非常广泛的结构,包括二叉树、平衡二叉树、完全二叉树、红黑树、B树和B+树等,本文先介绍基础的树形结构——二叉树。二叉树二叉树是指节点可以有两个孩子节点的树型结构,其中左边的孩子节点称为左孩子,右边的孩子节点称为右孩子。可以通过下面的结构来描述一个二叉树节点,为了数据结构的完整性,这里把构造方法也加上。struct treeNode{ treeNode *left; treeNode *right; int value;//构造函数 treeNode():value

2021-05-26 00:17:58 88

原创 STL(七):heap和priority_queue

前言终于快写完序列式容器了!剩最后两个heap和priority_queue,这篇一起写完,明天要开始关联式容器了。heapheap是STL中的堆数据结构,堆其实是一种完全二叉树,即非叶子节点的左右孩子必不为空的二叉树。堆分为最大堆和最小堆,最大堆的特点就是堆顶的元素必定是所有元素中的最大值,而最小堆的特点就是堆顶元素的值必定是所有元素中的最小值,比如下面的结构就是一个最大堆。STL底层使用vector来储存堆中的元素比如图中的最大堆会被储存为[23, 20, 13, 3, 10, 4],这是由于

2021-05-25 21:37:04 229

原创 STL(六):list和slist

前言我们已经介绍了vector和deque容器及其适配器,本文我们介绍STL中的链表——list和forward_list。链表链表是在内存中一种不连续的数据结构,链表的每个元素都是一个节点,节点中包含数据和指向下一个元素的指针,如下图:链表通过指针来寻找下一个元素,因此在内存中可以不连续,这极大地提高了使用内存的灵活性。与链表类似的是双向链表,双向链表包含next和prev两个指针:listSTL的list是一种双向链表数据结构,与vector、deque的实现不同,list也是继承自一个

2021-05-25 17:57:10 279

原创 STL(五):deque和stack,queue

前言deque是一种序列式容器,翻译为双端队列,deque实现了常数级别的头尾的插入和删除操作,但代价是deque具有比vector复杂的多的机制。deque的设计deque的类图与vector非常相似,数据结构都是存放在_Deque_base中,deque继承_Deque_base,然后定义一些内嵌类型和成员方法。类图中展示了deque的数据成员主要包括一个指针数组和两个迭代器,这与deque的结构有关,deque维护一个中控器,就是指针数组_M_map,中控器中每个元素指向一个数组,数组中是d

2021-05-25 00:43:49 290

原创 STL(四):vector和array

前言STL的容器分为序列式容器和关联式容器,其中序列式容器的特点是每个元素均有特定的位置,这个位置和元素的值无关,下面我们介绍四种序列式容器。vectorvector是一种支持动态扩容的数组,在SGI中,其继承关系如下:这里与GNU的STL稍有不同,GNU中_vector_base包含一个名为_vector_impl的成员,这个成员实现了_M_allocate()和_M_deallocate(),而SGI中可以看到是直接在_Vector_base中实现的。vector默认的allocator会为

2021-05-24 15:34:36 339

原创 STL(三):traits

前言在前面的介绍中,我们已经多次接触到traits,traits是一种泛型编程技法,用侯捷老师的话说,traits可以回答算法提出的问题,本文我们就来看看基本的traits技法和STL中的traits。traits原理泛型编程通过传入模板元的方法统一了不同类型的接口,在编译时,类型能够自动推导为传入的类型,这被称为泛化,比如下面的结构:template <typename T>struct A{ bool value = true;};泛化时与传入的参数类型是无关的,无论T的类

2021-05-23 23:05:55 225

原创 STL(二):Iterator

前言如果把STL的algorithm包含了各种算法的实现,这些算法必须能对不同的容器和容器的适配器进行操作,比如下面的例子:#include <bits/stdc++.h>using namespace std;int main(){ vector<int> m_vec{1, 3, 2, 4}; deque<int> m_deque{1, 3, 2, 4}; sort(m_vec.begin(), m_vec.end()); so

2021-05-23 20:53:01 194

原创 STL(一):allocator

系列文章目录提示:这里可以添加系列文章的所有文章的目录,目录需要自己手动添加例如:第一章 Python 机器学习入门之pandas的使用提示:写完文章后,目录可以自动生成,如何生成可参考右边的帮助文档文章目录系列文章目录前言一、pandas是什么?二、使用步骤1.引入库2.读入数据总结两级配置前言提示:这里可以添加本文要记录的大概内容:例如:随着人工智能的不断发展,机器学习这门技术也越来越重要,很多人都开启了学习机器学习,本文就介绍了机器学习的基础内容。提示:以下是本篇文章正文内容,

2021-05-22 23:13:47 134

原创 进程间通信(三):消息队列与共享内存

前言我们在进程间通信(一)和进程间通信(二)中,我们介绍了两种传统的通信机制,本文我们介绍两种XSI进程通信机制——消息队列和共享内存。ftokXSI通信机制会在内核中创建一个IPC结构体,并使用key作为这个结构体的外部标识,ftok函数用于生成这样一个key,ftok的函数原型如下:#include <sys/ipc.h>key_t ftok(const char *path, int id); //失败返回-1path参数用于引用一个现有的文件,ftok函数按照path取

2021-05-21 23:49:26 482

原创 进程间通信(二):管道

前言管道是一种非常古老的进程通信机制,在目前的Linux操作系统中也随处可见,本文我们来探讨一下不同类型管道的机制和使用方法。原理在Linux文件系统(一)中我们提到过文件的类型,其中一类文件类型就是管道文件,实际上,管道的实现原理正是借助了文件系统来实现的,下图展示了管道的读写过程。读进程和写进程分别打开一个file结构体并链接到同一个i-node,通过i-node找到对应数据页进行读写。PIPEpipe被称为无名管道,是一种只能用于亲缘关系间的单工通信方法,pipe使用下面的函数创建:#

2021-05-20 23:23:30 157

原创 进程间通信(一):信号

前言信号是软件中断的一种,是一种较早产生的进程间通信机制,信号的名称都以“SIG”开头,比如常见的SIGABRT、SIGALRM等。信号的种类Linux中的信号被定义为正整数常量,在bits/signum.h文件中如下:#define SIGSTKFLT 16 /* Stack fault (obsolete). */#define SIGPWR 30 /* Power failure imminent. *//* Historical signals specified by POSI

2021-05-17 23:34:31 217

原创 c++内存对齐

为什么需要内存对齐?cpu在存取指令时,以字长为单位进行存取,32位机器的默认字长为4字节,64位机器的默认字长为8字节。cpu每次读u取时,都从对齐字长的整数倍开始读取,而存放数据时,cpu以对齐字长的整数倍进行存放,这要求我们对struct或class内的数据排布提出了要求。假如没有内存对齐,数据被任意存放,cpu读取数据时,cpu需要先取出若干个对齐字长的内存,然后去掉开头和尾部不属于所需数据的字节,最后留下的数据块合并后再存入寄存器。考虑内存对齐的作用,cpu存取数据时可以直接取出若干对齐字节大

2021-05-16 23:29:51 181

原创 C++四种类型转换

前言在c语言中,通过下面的代码进行一次类型转换:int a = (int)10.0;这种转换通常称为强转,强转存在几个明显的缺点:强转形式上不易被发现,c语言中有很多类型和括号的组合,几乎随处可见,编码人员一眼扫过通常不易察觉强转。强转可能是不安全的,比如下面的代码中,将父类指针转为子类,从内存模型角度上来说,子类内存比父类多出一块,多出的部分结果是不可预知的。#include <iostream>class base{public: int a = 10;};

2021-05-16 16:17:46 129

原创 Linux文件系统(二)

文件描述符与文件的关系在虚拟地址空间中,进程将打开的文件描述符放在pcb的文件描述符表中,文件描述符指向一个文件表项,文件表项则指向文件的i-node节点。i-node节点的具体结构可以参考Linux文件系统(一)中的描述,文件表项虽然是系统层面管理的,但对于相同的i-node,不同进程对应的文件表项是不同的,文件表项实际上是struct file结构体,文件描述符表就是元素为struct file的数组,同一进程中多次打开同一文件,对应的文件表项和i-node都相同,而不同进程打开同一文件,文件表项不

2021-05-16 14:09:52 88

原创 中断

前言中断是一种使CPU挂起正在执行的程序,转而执行其他特殊事件的操作,可以用下图形象地表示这个过程。中断的执行过程中断是通过软件和硬件配合完成的事件,在指令的执行过程中,如果处理器芯片上的中断引脚电压变高,并在总线上获取了中断向量,就会触发中断。当cpu接收了一个中断向量,cpu首先保护现场,然后通过中断向量可以在中断向量表中找到对应的中断描述符,这种中断描述符通常被称为中断门。此时cpu需要检查中断特权级,如果特权级低于当前特权级,中断将被屏蔽,反之将执行中断门指向的中断处理程序,执行完毕后恢复现

2021-05-13 23:23:26 158

原创 Linux文件系统(一)

提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档Linux有一个著名的理念:一切皆文件,在Linux中,可以把任何东西都理解为文件,这是因为Linux文件包含很多类型:普通文件,包括文本文件和二进制文件;目录文件,文件目录本身其实就是一个目录类型的文件,可以通过vim等文本编辑器直接打开,但只有操作系统内核才能直接写目录文件;块特殊文件,提供对设备(如磁盘)带缓冲的访问;字符特殊文件:提供对设备不带缓冲的访问;管道:用于进程间网络通讯的管道文件;套接字:用于进程间通讯的套接

2021-05-11 23:24:44 113

原创 虚拟地址与物理地址的转换

在虚拟地址空间那篇文章中我们通过虚拟地址空间简单地介绍了虚拟地址空间,知道了应用程序中使用的是虚拟地址,需要通过MMU转换成物理地址,本文将详细介绍虚拟地址如何转换成物理地址。页、页框、页表linux操作系统以页为单位管理虚拟内存,通常一页为4k,而物理内存是以块为单位管理的,物理内存被分成很多与页大小相同的块,被称为页框,每个页地址与页框对应,这种对应关系被记录在页表中,页表是MMU中的数据结构,下图是页表中较为常见的几个属性。页框存在于物理内存上,而页可以放置在任意的页框中,这是能够进行地址转

2021-05-10 23:34:40 6364

原创 线程同步

线程安全多线程程序共享虚拟地址空间,使得多个线程可以操作同一变量,但这也可能带来意料之外的问题,比如下面的代码:#include <bits/stdc++.h>#include <pthread.h>#include <errno.h>int num = 0;void* add(void *){ ++num; return nullptr;}int main(){ pthread_t tid[10]; for(int

2021-05-08 23:09:48 72

原创 进程与线程

前言我们写好的代码,通过编译后变成可执行的二进制文件,被称为程序,程序加载后被称为进程,进程中又可能包含多个线程,本文就介绍下进程和线程。进程进程是分配资源的基本单位,每个进程都有自己独立的内存空间,进程通常有5种状态,分别是:创建状态,就绪状态,等待状态,执行状态和停止状态,它们之间的转换关系如下图:进程刚创建完成时是就绪状态,此时需要等待cpu的调度,cpu调度进程后变为运行态,之后如果一直占据cpu直到时间片用完,则进入就绪态等待再次被调度,如果需要等待某事件(如鼠标键盘输入或io事件等)则

2021-05-07 21:34:17 622

原创 设计模式(一):c++11单例模式

懒汉式单例模式是一种设计模式,其目的是保证程序中某个类只能存在一个实例,通常日志系统和程序中的管理类(比如用于管理回调函数的handle_manager等)会以单例模式实现,显然程序不希望这些数据结构存在多个。c++单例模式有很多种写法,但c++11之后的单例模式,拒绝花里胡哨,直接上代码:#include <iostream>class singleton{public: static singleton getInstance(){ static sing

2021-05-06 22:59:27 322 2

原创 进程关系

终端登录linux系统启动时,内核创建ID为1的init进程,当有新用户登录时,如果终端允许登录,init进程fork出子进程,然后使用exec装载getty程序,getty程序具有超级用户权限,使用RW方式打开终端,此时将会设置文件描述符0、1、2,并等待用户输入账户登录,用户输入账户名后,进程使用exec装载login程序。login程序此时仍拥有超级用户权限,等待用户输入正确密码后,设置当前目录,终端所有权、访问权限、组ID、用户ID等信息,这些信息之前被记录在/etc/passwd中,如果多次输

2021-05-05 23:16:27 72

原创 fork/vfork

前言unix操作系统提供了一系列进程的控制原语来操作进程,fork()/vfork()就是其中用于创建新进程的两种系统调用。forkfork()函数创建的新的子进程是原本父进程的副本,从虚拟地址空间的角度来看,可以用下图表示。此时的子进程几乎和父进程一摸一样,包括代码的执行位置。fork()函数在unistd.h文件中声明,下面是函数原型:pid_t fork();pid_t是进程id的类型,fork()失败时,返回值为-1,fork()成功时,父进程返回子进程的进程id,子进程返回0。下面

2021-05-04 00:52:09 91

SGI STL 关联式容器源码

SGI STL 关联式容器源码

2021-05-26

SGI STL heap相关代码

SGI STL heap相关代码

2021-05-25

SGI STL list相关代码

SGI STL list相关代码

2021-05-25

SGI STL slist相关代码

SGI STL slist相关代码

2021-05-25

SGI STL deque相关代码

SGI STL deque相关代码

2021-05-24

SGI STL vector相关源码

SGI vector源码

2021-05-24

iterator.zip

STL iterator相关代码

2021-05-23

allocator.zip

STL中allocator相关源代码

2021-05-23

空空如也

TA创建的收藏夹 TA关注的收藏夹

TA关注的人

提示
确定要删除当前文章?
取消 删除