-
Notifications
You must be signed in to change notification settings - Fork 0
/
content.json
1 lines (1 loc) · 450 KB
/
content.json
1
{"meta":{"title":"MapleStory","subtitle":null,"description":"CS:Dalian University of technology","author":"Eyc","url":"http://yoursite.com"},"pages":[],"posts":[{"title":"[后台开发工程师总结系列] 11. 分布式原理","slug":"后台开发工程师总结系列-11-分布式原理","date":"2019-03-09T09:17:43.000Z","updated":"2019-03-09T09:20:39.707Z","comments":true,"path":"2019/03/09/后台开发工程师总结系列-11-分布式原理/","link":"","permalink":"http://yoursite.com/2019/03/09/后台开发工程师总结系列-11-分布式原理/","excerpt":"分布式什么是分布式分布式系统是一组通过网络进行通信,为完成共同任务而协调工作节点组成的计算机系统。分布式系统的出现是为了用廉价的、普通的单机完成无法计算、存储的任务。其目的是利用更多的机器, 处理更多的数据。 分布式系统挑战分布式系统需要大量机器协作,面临诸多挑战: 异构的机器与网络 分布式中的机器配置不一样、运行的服务语言、架构不同、因此处理能力不一样。而且网络带宽、延时、丢包率也不一样。 普遍的节点故障 虽然单个节点的故障率低,但是节点数据达到一定规模,出故障的概率就变高了。分布式需要故障发生时,系统仍然是可用的。 不可靠的网络 节点间通过网络通信,而网络问题:分割、延时、丢包、乱序","text":"分布式什么是分布式分布式系统是一组通过网络进行通信,为完成共同任务而协调工作节点组成的计算机系统。分布式系统的出现是为了用廉价的、普通的单机完成无法计算、存储的任务。其目的是利用更多的机器, 处理更多的数据。 分布式系统挑战分布式系统需要大量机器协作,面临诸多挑战: 异构的机器与网络 分布式中的机器配置不一样、运行的服务语言、架构不同、因此处理能力不一样。而且网络带宽、延时、丢包率也不一样。 普遍的节点故障 虽然单个节点的故障率低,但是节点数据达到一定规模,出故障的概率就变高了。分布式需要故障发生时,系统仍然是可用的。 不可靠的网络 节点间通过网络通信,而网络问题:分割、延时、丢包、乱序 分布式特性透明性、可扩展性、可靠和可用性、高性能、一致性 组件、理论、协议使用web、APP、SDK通过HTTP、TCP连接到系统,在分布式系统中,为了高并发、高可用,一般都是多个节点提供相同的服务。选择哪个节点来进行服务,这就是负载均衡。负载均衡思想简单、使用很广泛,在分布式系统、大型网站方方面面有应用。 通过负载均衡找到一个节点,然后处理真正的用户请求。请求有可能很简单,也有可能很复杂。简单的请求比如读取数据,可能是有缓存的,及分布式缓存。如果缓存没有明忠,就绪数据库中拉取数据。复杂的请求可能还会调用其他服务。 假设服务A需要调用B的服务,两个节点需要通信,通信建立在TPC|IP上,但是每个应用都写socket是复杂的事情,同理http,于是有了进一步的抽象,有了RPC,远程调用跟本地一样方便。 一个请求可能包含诸多操作,其实涉及到多个服务。一个服务怎么去找到另一个服务呢?通信地址是需要的,怎么获取这个地址,最简单的方法就是写死配置,或者写入数据库。但是这些方法操作不方便,这时候需要注册与发现:提供一个节点向协调中心注册自己的地址,便于服务去拉取地址 以上可以看到,协调中心提供了中心化的服务:一组节点提供类似单点的服务,比如命令服务、分布式锁、注明的就是chubby、 zookeeper 回到用户,请求操作会产生数据、日志、通常为信息。这时有一些系统可能对这些信息感兴趣,如个性化推荐、监控等,这里就抽象出了两个概念(消息生产者与消费者)。那么生产者怎么发送给消费者呢,RPC需要指定发送消息,但是实际情况生产者并不清楚,也不关心谁会消费这个消息。简单来说生产者只需往消息队里传入消息即可。消息队列起到了异步处理、解耦的作用 上面提到,用户会产生一些数据,这些数据忠实记录了用户操作习惯、爱好,是各个行业宝贵的财富。这就产生了分布式计算平台如 Hadoop、Storm 最后用户操作完成后需要持久化,但是数据量很大,单个节点难以存储。这就需要分布式储存,将数据划分到不同的节点上,同时防止数据丢失,进行备份。 负载均衡 Nginx 高性能、高并发 的负载均衡服务器,负载均衡、反向代理、静态内容缓存、访问控制 LVS 基于集群技术和linux操作系统实现高性能、高可用服务器 webserver java: Tomacat Apache Python: gunicorn、uswgi、twisted service SOA、微服务、spring boot 、django 容器 docker cache memcache、 redis 协调中心 zookeeper、 etcd RPC框架 grpc、dubbo(阿里RPC框架) 消息队列 kafka、rabbitMQ 异步处理、应用解耦、流量削峰、消息通信 实时数据平台 storm 离线数据平台 hadoop、spark DB MySQL、Oracle、MongoDB 负载均衡集群中应用服务器节点通常被设计成无状态,用户可以请求任何一个节点 负载均衡服务器会根据每个节点的情况将用户请求转发到合适的节点上 负载均衡服务器用来实现高可用及伸缩性 高可用:当某个节点故障时, 负载均衡服务器会将用户请求发送到另外的节点上,从而保证所有的服务持续可用 伸缩性:根据系统的整体负载情况,容易的添加、移除节点 负载均衡算法 轮询(Round Robin) 轮询算法把请求轮流的发送到每个服务上 该算法适合每个服务器性能差不多的情况,如果有性能存在差异,性能较差的服务器可能无法承担较大的负载 加权轮询 加权轮询是在轮询的基础上,根据服务器的性能差异,为服务器赋予一定 的权值。性能较高的服务器赋予较高的权值。 最少连接 由于每个请求的连接时间不一样,使用轮询或加权轮询,可能让一台服务器连接数过大,而另一台服务器连接数过小,造成负载不均衡。最少连接算法就是把当前的请求发送到最少连接数的服务器上。 4 加权最少连接 最少连接的基础上,根据服务器性能为每台服务器分配权重,根据权重处理连接数 5 随机算法 把请求随机的发送到服务器上 6 源地址hash 源地址计算hash值 后,通过对服务器数量取模取得目标服务器的序号。 转发实现 HTTP重定向 HTTP重定向负载均衡服务器使用某种负载均衡算法得到IP地之后,将地址写入HTTP报文中,状态码为302,客户端收到重定向报文后重新请求。 缺点:两次请求,延迟高 HTTP 负载均衡处理能力有限,会限制集群的规模 DNS 域名解析 在DNS域名解析服务器计算服务器IP地址 优点:DNS能够根据地理位置域名解析,返回离用户最近的IP地址 缺点:由于DNS有多级结构,每一级域名都可能被缓存,延时生效。 大型网站基本使用了DNS作为第一级的负载均衡手段,然后在内部使用其他方式作为第二季负载均衡,也就是说,域名解析的结果一般是二级负载均衡服务器的IP地址 反向代理服务器 反向代理服务器位于原服务器前,用户请求经过反向代理服务器再能达到原服务器。反向代理服务器用来缓存、日志,同时也可以作为负载均衡服务器。 这种负载均衡转发方式下,客户端不直接请求资源。 网络层 操作系统内核获取网络数据包,根据负载均衡算法计算IP地址并修改IP地址进行转发原服务器返回的请求也及经过负载均衡服务器。在内核中进行,效率较高。 链路层 修改MAC地址进行转发。通过配置服务器虚拟IP和负载均衡IP一直,不需要修改IP地址就可转发。 这是目前大型网站最广泛使用的负载均衡转发方式,在LINUX平台使用的负载均衡服务器为LVS 集群Session管理一个用户的session信息如果存在一个服务器上,那么当负载均衡服务器把用户请求转到另一个服务器,由于用户没有用户的Session信心,用户需要重新登录。 Sticky Session配置负载均衡服务器,是一个用户所有的请求都到同一个路由器,这样吧用户的session放在服务器中 缺点:服务器宕机时,丢失所有的session Session Replication服务器间进行同步操作,所有的服务器都有所有的session信息 缺点:占用内存过多、同步占用带宽、服务器处理时间 Session Server使用一个单独的Session服务器,可使用MySQL或者Redis 优点:为了使得大型网站具有伸缩性,集群中的应用服务器保持无状态。session服务器的存在保证了应用服务器的无状态 大型网站架构常见问题 你使用过哪些组件或者方法来提升网站性能,可用性,及并发量 提高硬件性能,增加系统服务器 使用缓存(本地缓存,分布式缓存Redis、memcache ) 消息队列(解耦、削峰、异步) 分布式开发(不同服务部署在不同的机器上,利用nginx负载均衡访问,大大提高了并发量) 数据库分库(读写分离)分表(水平分表、垂直分表) 采用集群(多台机器提供相同的服务) CDN加速(将一些静态资源放在离用户最近的网络节点) 浏览器缓存 合适的连接池(数据库连接池、线程池) 适当使用多线程开发 高可用系统的常用手段 降级:服务器降级是服务器压力剧增的情况下,根据当前业务对服务、页面策略的降级,以释放服务器资源保证核心任务的运行。降级往往会指定级别 限流:防止恶意请求恶意攻击超出峰值 缓存:避免大量流量直接打到数据库上 超时、重试机制:避免堆积请求 回滚机制:快速修复错误版本 现代互联网系统应该具备的特点 高并发、大流量 高可用,不间断服务 海量数据 用户分布广泛、网络情况复杂、分布范围广、网络情况千差万别 安全环境恶略 需求快速变更 渐进式发展 微服务领域的了解和认识 大公司及未来的趋势都是spring cloud 我们通常把spring cloud理解为一系列开源组件的集合,抽象了一套通用的开发模式。他的目的是通过抽象这套模式,让开发者更快的开发业务,而这套开发的实际载体还是依赖于RPC、网关、服务发现、配置管理、限流、分布式链路 性能测试 性能测试指通过自动化测试模拟多种正常、峰值、以及异常载荷条件来对系统进行各项指标的测试。 基准测试: 给系统压力较低时,查看系统的运行状况 负载测试:对系统不断的增加压力,直至系统多向指标达到安全临界值 压力测试:超过安全负载情况下不断施加压力,直到系统崩溃或无法处理任何请求,依次获得系统的最大压力承受能力。 稳定性测试:在特定的硬件、软件、网络环境下,加载一定的业务压力观察是否稳定 常见的大表优化 表单数据过大时,数据的CRUD性能会下降,常见优化措施如下 限定数据的范围: 无比进制不带任何限制范围限制条件的查询语句 读写分离 经典的数据库拆分方案(主库负责读写,从库负责读) 垂直分区:根据数据表的相关性进行拆分。例如用户表中有登录信息和基本信息,可以把这两种信息拆成两个表,甚至放到单独的库做分库。简单来说垂直拆分是数据表列的拆分。 垂直拆分的优点:可以使行数据变小,查询时减少读取的block数、减少IO次数。此外垂直分区可以简化表结构,易于维护 缺点:主键出现冗余,让某些事务更加的复杂 水平分区,保持数据的表结构不变,通过某种策略进行数据分片,这样每一片数据分到不同的表或库中,达到了分布式的目的,水平拆分可以支持非常大的数据量。 水平拆分最好分库,水平拆分能够支持非常大的数据储存量,但是分片事务难以解决。 消息队列的好处 通过异步提高系统的处理性能 不使用消息队列时,用户的请求直接写入数据库,在高并发的情况下服务器压力剧增。使得响应速度变慢。但是使用消息队列后,用户请求的消息立即返回,再由消息队列的消费者进程从消息队列中获取数据,异步写入数据库。由于消息队列服务器速度快于数据库,响应速度大大改善。 通过以上分析可以得出消息队列有很好的削峰作用–通过异步处理,将短时间高并发事务消息存在消息队列中,从而削平高峰期的并发事务。 数据写入消息队列中就返回用户,但是之后的请求有可能失败。因此消息队列异步处理后需要适当修改业务流程与其配合。 2 降低系统的耦合性 分布式消息队列: 利用发布-订阅者模式工作,生产者发送消息,一个或多个消息接受者(订阅者)订阅消息。从上图可以看出生产者和消费者没有直接耦合,消息发送者将消息发送至分布式消息队列中,接受者只需从后端获取并处理。","categories":[],"tags":[{"name":"后台开发","slug":"后台开发","permalink":"http://yoursite.com/tags/后台开发/"}]},{"title":"[后台开发工程师总结系列] 10. 常用算法及参考实现","slug":"后台开发工程师总结系列-10-常用算法及参考实现","date":"2019-03-09T09:13:22.000Z","updated":"2019-03-09T09:16:38.837Z","comments":true,"path":"2019/03/09/后台开发工程师总结系列-10-常用算法及参考实现/","link":"","permalink":"http://yoursite.com/2019/03/09/后台开发工程师总结系列-10-常用算法及参考实现/","excerpt":"常考算法题设计类问题// 1. 带过期时间LRU // 2. 设计一个Hashmap 基础数据结构及算法// 1. 二叉树的三种非递归遍历 + 层次遍历 // 2. 单例模式 // 3. 并查集及最小生成树 // 4. 最短路(Dijikstra算法) // 5. 拓扑排序 // 6. 先序和中序 构造二叉树 // 7. 字典树 // 8. 合并k个排序的链表、数组 // 9. 二叉树的最长路径(最大路径和) // 10. LCA问题 // 11. 三个经典的进程同步模型// 生产者消费者、读者写者问题、哲学家进餐问题 // 12. 二叉树中序的下一个节点 // 13. 两个排序数组中位数,TOP K // 14. 快排 // 15. 链表排序 // 16. 堆排序 // 17. 几个C语言函数实现(strlen(), strcmp(), strcpy(), memset()) // 18. 字符串全排列","text":"常考算法题设计类问题// 1. 带过期时间LRU // 2. 设计一个Hashmap 基础数据结构及算法// 1. 二叉树的三种非递归遍历 + 层次遍历 // 2. 单例模式 // 3. 并查集及最小生成树 // 4. 最短路(Dijikstra算法) // 5. 拓扑排序 // 6. 先序和中序 构造二叉树 // 7. 字典树 // 8. 合并k个排序的链表、数组 // 9. 二叉树的最长路径(最大路径和) // 10. LCA问题 // 11. 三个经典的进程同步模型// 生产者消费者、读者写者问题、哲学家进餐问题 // 12. 二叉树中序的下一个节点 // 13. 两个排序数组中位数,TOP K // 14. 快排 // 15. 链表排序 // 16. 堆排序 // 17. 几个C语言函数实现(strlen(), strcmp(), strcpy(), memset()) // 18. 字符串全排列 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658// 设计类问题// 1. 带过期时间LRUclass LRU{public: int LRU(int num):cap(num){}; int get(int key){ if(kv[key]){ touch(key); return kv[key]; }else{ return -1; } } void put(int key, int val){ if(node.size()==cap&&!kt[key]){ kv.erase(node.back()); kt.erase(node.back()); node.pop_back(); } touch(key); kv[key] = val; }private: int cap; list<int> node; unordered_map<int, int> kv; unordered_map<int, list<int>::iterator> kt; void touch(int key){ if(kt[key]){ node.erase(kt[key]); } node.push_front(key); kt[key] = node.begin(); }};// 2. 设计一个Hashmapstruct ListNode{ int key; int val; ListNode* next; ListNode(int k, int v):key(k),val(v){};};class HashMap{public: HashMap(int num):cap(num){ ListNode* head = new ListNode(0, 0); pt = vector<ListNode*>(num,head); } void put(int key, int val){ int k = getIndex(hash(key)); ListNode* root = vector[k]; ListNode* node = searchList(root, key); if(node!=NULL){ node->val = val; }else{ ListNode* node = new ListNode(key, val); node->next = root->next; root->next = node; } } int get(int key){ int k = getIndex(hash(key)); ListNode* root = vector[k]; ListNode* node = searchList(root, key); if(node!=NULL){ return node->val; }else{ return -1; } } ListNode* searchList(ListNode* root, int key){ root = root->next; while(root){ if(root->key==key) break; root=root->next; } return root; }private: int cap; vector<ListNode*> pt; int hash(int key){ return (key>>16)^key; } int getIndex(int key){ return key%cap; }};// 基础数据结构// 1. 二叉树的三种非递归遍历 + 层次遍历void preorder(TreeNode* root){ stack<TreeNode*> buf; while(root!=NULL&&!buf.empty()){ buf.push(root); cout<<root->val<<endl; root=root->left; while(root==NULL&&!buf.empty()){ TreeNode temp = buf.top(); buf.pop(); temp = temp->right; } }}void inorder(TreeNode* root){ stack<TreeNode*> buf; while(root!=NULL&&!buf.empty()){ if(root){ buf.push(root); root=root->left; }else{ TreeNode* temp = buf.top(); buf.pop(); cout<<temp->val<<endl; root = temp->right; } }}void postorder(TreeNode* root){ stack<TreeNode*> buf; TreeNode* ref = NULL; if(root) buf.push(root); while(!buf.empty()){ TreeNode* temp = buf.top(); if((temp->left==NULL&&temp->right==NULL)||ref!=NULL&&(temp->left==ref||temp->right==ref)){ cout<<temp->val<<endl; ref = temp; buf.pop(); }else{ if(temp->right) buf.push(temp->right); if(temp->left) buf.push(temp->left); } }}vector<vector<int>> levelorder(TreeNode* root){ queue<TreeNode*> buf; vector<vector<int>> rs; vector<int> level; if(root) buf.push(buf); int curr = 1, next = 0; while(!buf.empty()){ TreeNode* temp = buf.top(); buf.pop(); curr--; level.push_back(temp->val); if(temp->left) {buf.push(temp->left); next++;} if(temp->right) {buf.push(temp->right); next++;} if(curr==0){ rs.push_back(level); level.clear(); curr = next; next = 0; } }}// 2. 单例模式pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;class Singleton{private: Singleton(){}; static Singleton* sin;public: static Singleton* getInstance(){ if(sin==NULL){ pthread_mutex_lock(&mutex); if(sin==NULL){ sin = new Singleton(); } pthread_mutex_unlock(&mutex); } }}Singleton::sin=NULL;// 3. 并查集及最小生成树class UF{private: int cap; int count; vector<int> _size; vector<int> _id;public: UF(int N):cap(N){ count = N; _size = vector<int>(size(), 1); for(int i=0; i<N; i++){ _id.push_back(i); } } int id(int k){ while(_id[k]!=k) k = id[k]; } void union(int left, int right){ int l = _id[left]; int r = _id[right]; if(l==r) return ; count--; if(_size(l)<_size(r)){ _id[l] = r; _size[r] += _size[l]; }else{ _id[r] = l; _size[l] += _size[r]; } } int find(int left, int right){ return id(left)==id(right); }}// 4. 最短路(Dijikstra算法)#define max_len 2147483647int Dijikstra(vector<vector<int>> _map, int start, int end){ int N = _map.size(); vector<int> distance(N, max_len); vector<int> visit(N); distance[start] = 0; visit[start] = 1; for(int count=1; count<N; i++){ int len = max_len, k = -1; for(int i=0; i<N; i++){ if(visit[i]==0&&_map[start][i]<len){ len = _map[start][i]; k = i; } } visit[k] = 1; distance[k] = len; for(int i=0; i<N; i++){ if(distance[start][k]+distance[k][i] < distance[start][i]){ distance[start][i] = distance[start][k]+distance[k][i]; } } } return distance[start][end];}// 5. 拓扑排序bool dfs(vector<unordered_map<int>>& _map, vector<int>& markded, vector<int>& round, stack<int>& con, int i){ if(marked[i]) return fasle; marked[i] = round[i] = 1; for(int num:_map[i]){ if(round[i]||dfs(_map, marked, round, con, num)) return true; } con.push(i); round[i] = 0; return false;}vector<int> Topological(int numCourses, vector<pair<int, int>>& prerequirty){ vector<unordered_map<int>> _map; vector<int> marked(numCourses); vector<int> round(numCourses); vector<int> res; stack<int> con; for(int i=0; i<prerequirty.size(); i++){ _map[prerequirty[i].second].insert(prerequirty[i].first); } for(int i=0; i<numCourses; i++){ if(!marked[i]&&dfs()) return res; } while(!con.empty()){ res.push_back(con.top()); con.pop(); } return res;}// 6. 先序和中序 构造二叉树TreeNoode* Tree(vector<int> preorder, vectr<int> inorder, int pivot, int left, int right){ if(left<right){ int key = preorder[pivot]; int pos = find(inorder.begin(), inorder.end(), key); TreeNode* root = new TreeNode(key); root->left = Tree(preorder, inorder, pivot+1, left, pos-1); root->right = Tree(preorder, inorder, pivot+1+right-pos, pos+1, right); }}TreeNode* piTree(vector<int> preorder, vector<int> inorder){ return Tree(preorder, inorder, 0, 0, inorder.size()-1);}// 7. 字典树class Trie{private: Trie* node[26]; bool _end;public: Trie():end(false){ memset(node, 0, sizeof(node)); } void insert(string s){ Trie* cur = this; for(int i=0; i<s.size(); i++){ int pos = s[i] - 'a'; if(cur->node[pos]==NULL){ Trie* temp = new Trie(); cur->node[pos] = temp; } cur = cur->node[pos]; } cur->_end = true; } bool find(string s){ Trie* cur = this; for(int i=0; i<s.size(); i++){ int pos =s[i]-'a'; if(cur->node[pos]==NULL){ return false; }else{ cur = cur->node[pos]; } } return _end; } bool startWith(string s){ Trie* cur = this; for(int i=0; i<s.size(); i++){ int pos =s[i]-'a'; if(cur->node[pos]==NULL){ return false; }else{ cur = cur->node[pos]; } } return true; }}struct cmp{ bool operater <(Node* a, Node* b){ a->val > b->val; }}// 8. 合并k个排序的链表、数组Node* mergeKarr(vector<Node*> vec){ priority_queue<Node*, vector<Node*>, cmp> pq; Node* root = new Node(0); Node* pt = root; for(int i=0; i<vec.size(); i++){ if(vec[i]!=NULL) pq.push(vec[i]); } while(!pq.empty()){ Node* temp = pq.top(); pq.pop(); pt->next = temp; pt=pt->next; if(temp->next!=NULL){ pq.push(temp->next); } } return root->next;}// 9. 二叉树的最长路径(最大路径和)int LongPath(TreeNode* root, int& sum){ if(root==NULL) return -1; int left = LongPath(root->left, sum)+1; int right = LongPath(root->right, sum)+1; int sum = max(sum, left+right); return max(left, right)+1;}int maxlen(TreeNode* root){ int len = 0; LongPaht(root, len); return len;}int LongSum(TreeNode* root, int& sum){ if(root==NULL) return 0; int left = max(LongSum(root->left, sum),0); int right = max(LongSum(root->right, sum),0); int sum = max(sum, left + right+ root->val); return left>right?left+root->val:right+root->val;}// 10. LCA问题TreeNode* LCA(TreeNode* root, TreeNode* left, TreeNode* right){ if(root==NULL||root==left||root==right) return root; TreeNode* left = LCA(root->left, left, right); TreeNode* right = LCA(root->right, left, right); if(left!=NULL&&right!=NULL) return root; return left!=NULL?left:right;}// 11. 三个经典的进程同步模型// 生产者消费者、读者写者问题、哲学家进餐问题// P() V()#define int sem_t;sem_t mutex = 1;sem_t full = 100;sem_t empty = 0;queue<Item> cache;void producer(){ while(1){ P(&full); P(&mutex); Item it = new Item(); cache.push(it); V(&mutex); V(&empty); }}void consumer(){ while(1){ P(&empty); P(&mutex); Item it = cache.top(); it.pop(); V(&mutex); V(&full); }}sem_t mutex = 1;sem_t read = 1, write = 1;int count = 0;void reader(){ P(&read); count++; if(count==1) P(&write); V(&read); _read(); P(&read); count--; if(count==0) V(&write); V(&read);}void writer(){ P(&write); _write(); V(&write);}sem_t mutex = 1;sem_t chop[] = {1,1,1,1,1};void eat(){ while(1){ P(&mutex); P(chop[i%5]); P(chp[(i+1)%5]); V(&mutex); _eat(); P(&mutex); P(chop[i%5]); P(chp[(i+1)%5]); V(&mutex); }}// 12. 二叉树中序的下一个节点TreeNode* nextNode(TreeNode* root){ if(root->right){ while(root->left) root=root->left; return root; } while(root->parent){ if(root==root->parent->left) return root->parent; else root=root->parent; } return NULL;}// 13. 两个排序数组中位数,TOP K// 两个数组长度相同, 中位数问题int midnum(vector<int> vec1, vector<int> vec2){ int N = vec1.size(); int s1 = 0, e1 = N-1, m1 = 0; int s2 = 0, e2 = N-1, m2 = 0; int offset = N%2==0?1:0; while(s1<s2){ int m1 = (s1 + e1)/2; int m2 = (s2 + e2)/2; if(vec1[m1]==vec2[m2]){ return vec1[m1]; }else if(vec1[m1]<vec2[m2]){ s1 = m1 + 1; e2 = m2; }else{ e1 = m1; s2 = m2 + 1; } } return min(vec1[s1], vec2[s2]);}// 和为sum的序列void dfs(vector<int>& vec, vector<vector<int>>& rs, vector<int>& temp, int k, int tar){ if(tar==0){ rs.push_back(temp); return ; } if(tar<0) return ; for(int i=k; i<vec.size(); i++){ temp.push_back(vec[i]); dfs(vec, rs, temp, k+1, tar-vec[i]); temp.pop_back(); }}vector<vector<int>> getSeq(vector<int> vec, int tar){ vector<vector<int>> rs; vector<int> temp; sort(vec.begin(), vec.end()); dfs(vec, rs, temp, 0, tar); reutrn rs;}// 基础算法// 1. 快排int findPivot(vector<int>& vec, int left, int right){ int pivot = vec[left]; while(left<right){ while(left<right&&vec[right]>pivot) right--; vec[left] = vec[right]; while(left<right&&vec[left]<pivot) left++; vec[right] = vec[left]; } vec[left] = pivot;}void quicksort(vector<int>& vec, int left, int right){ if(left<right){ int pivot = findPivot(vec, left, right); quicksort(vec, left, pivot-1); quicksort(vec, pivot+1, right); }}// 2. 链表排序ListNode* Listsort(ListNode* root){ if(root==NULL) return NULL; if(root->next==NULL) return root; ListNode* p = root, *q = root, *pre = root; while(q!=NULL&&q->next!=NULL){ pre = p; p = p->next; q = q->next->next; } pre->next = NULL; ListNode* left = Listsort(root); ListNode* right = Listsort(p); return merge(left, right);}ListNode* merge(ListNode* left, ListNode* rigth){ // 略}// 3. 堆排序void sink(vector<int> vec, int i, int N){ int pivot = vec[i]; while(2*i+1<N){ int k = 2*i + 1; if(vec[k]<vec[k+1]){ k++; } if(vec[i]>vec[k]){ vec[i] = vec[k]; i = k; }else{ break; } } vec[i] = pivot;}void heapsort(vector<int> vec){ int N = vec.size(); for(int i= N/2; i>=0; i--){ sink(vec, i, N); } swap(vec[0], vec[N-1]); for(int i=vec.size()-1; i>=0; i--){ sink(vec, 0, i); }}// 4. 几个C语言函数实现(strlen(), strcmp(), strcpy(), memset())int strlen(char* s){ assert(s!=NULL); int len = 0; while(*s++!='\\0') len++; return len;}int cmp(char* s1, char* s2){ assert(s1!=NULL&&s2!=NULL); int pt; while((pt=*(unsigned char *)s1++ - *(unsigned char*)s2+=)!=0); if(pt>0) return 1; else if(pt<0) return -1; else return pt;}void strcpy(char* s1, char* s2){ assert(s1!=NULL&&s2!=NULL); char* ori = s1; while(*s1++ != '\\0'); while(*s2!='\\0') s1++ = s2++; s1 = '\\0'; return ori;}void memset(void* a, int c, n){ assert(a!=NULL); byte* pt = (byte*) a; while(n--) *pt++ = c;}// 5. 字符串全排列void dfs(vector<string>& rs, string s, string& temp, vector<int>& visit){ if(s.size()==temp.size()){ rs.push_back(temp); } for(int i=0; i<s.size(); i++){ if(visit[i]==0){ visit[i]++ ; temp.push_back(s[i]); dfs(rs, s, temp, visit); temp.pop_back(); visit[i]-- ; } }}vector<stirng> Permutation(string s){ vector<string> rs; string temp; vector<int> visit(s.size()); dfs(rs, s, temp, visit); return rs;}string nextpermutation(string s){ int left = s.size()-1, right = s.size()-1; while(left>=1&&s[left]<=s[left-1]) left--; int pivot = s[left-1]; while(right>=0&&s[right]<=pivot) right--; swap(s[left-1], s[right]); reverse(s.begin()+left,s.end()); return s;}// 6. 出栈序列是否合法bool valid(vector<int> vec1, vector<int> vec2){ stack<int> cache; for(int i=0, j=0; i<vec1.size(); i++){ cache.push(vec1[i]); while(!cache.empty()&&cache.top()==vec2[j]){ cache.pop(); j++; } } return vec1.empty();}","categories":[],"tags":[{"name":"后台开发","slug":"后台开发","permalink":"http://yoursite.com/tags/后台开发/"}]},{"title":"[后台开发工程师总结系列] 9.Python,Nginx and Django","slug":"后台开发工程师总结系列-9-Python,Nginx-and-Django","date":"2019-03-09T09:11:04.000Z","updated":"2019-03-09T09:16:28.843Z","comments":true,"path":"2019/03/09/后台开发工程师总结系列-9-Python,Nginx-and-Django/","link":"","permalink":"http://yoursite.com/2019/03/09/后台开发工程师总结系列-9-Python,Nginx-and-Django/","excerpt":"pythonPEP8规范 每一行使用四个空格 每一行限制最大字符数为79 推荐从( [ 换行, 与上一行的第一个(错一个字符。 顶层函数和类定义换两行, 类里的方法一个换行 命名、空格规则","text":"pythonPEP8规范 每一行使用四个空格 每一行限制最大字符数为79 推荐从( [ 换行, 与上一行的第一个(错一个字符。 顶层函数和类定义换两行, 类里的方法一个换行 命名、空格规则 如何理解Python Python是一种解释性语言,与C语言及衍生语言不同,Python运行之前不需要编译 Python是动态类型的语言,声明变量时,不需要说明变量的类型。 非常适合面向对象编程(OOP), 没有访问说明符 函数是第一类对象,可以被指定给变量,返回函数类型 Python代码编写块,但是运行慢。Python允许编写C语言的扩展以加快速度 Python Pyhton迭代器与生成器 迭代器是一个抽象的概念,任何对象,如果它有next方法和iter方法返回它本身 iter() 是Python的内置函数,iter()会返回一个定义了next()方法的迭代器对象,在容器中逐级的访问元素,next也是python的内置函数,没有后续元素时会抛异常 生成器是创建迭代器简单而强大的工具,他们写起来就像是其他的函数,只需要在返回数据时使用yeild语句。每次next调用时,他都会脱离当前的位置。 生成器能做迭代器做的所用事情,而且因为创建了iter() 和next 简介而高效 装饰器的作用和功能 引入日志 函数执行时间统计 执行函数前预备处理 执行函数后的清理功能 权限校验等场景 缓存 GIL(Global INterpreter Lock 全局解释器锁) Python代码执行有Python虚拟机控制。Python在设计之初就考虑到在解释器中只有一个线程在执行,即任意时刻只有一个线程在解释器中运行。对Python虚拟机的访问由全局解释器锁来控制。 在多线程环境中,Python虚拟机按照如下方式运行 设置GIL 切换到一个线程去运行 运行: 指定数量的字节码,线程主动让出控制 线程睡眠 解锁GIL 重复上述步骤 Python 的内存管理 垃圾回收:Python 不像C++ java 语言一样,不用事先声明变量而直接对变量进行赋值。对Python来讲,对象和内存都是在运行时确定的。这也是为什么Python被称为动态类型的原因(动态类型可以简单归结为对变量内存地址的分配是在运行时自动判断类型并赋值)。 引用计数:Pyhton采用了类似windows内核对象方式进行内存管理。每一个对象都有一个指针指向其引用计数。当变量绑定该对象时引用计数就是1 内存池机制: Python内存池以金字塔行 0层是 malloc 和free 操作 1、2层是内存池,对象小于256K时直接由该层分配内存 3层是最上层,对Pythton对象直接操作 Python多继承(MRO)C3算法最早提出用于Lisp,应用到Python中是为了解决原来深度优先搜索不满足本地优先级、单调性的问题 本地优先级:声明父类时的顺序,C(A,B),访问父类时应该根据声明顺序优先查找A类然后是B类 单调性:如果在C的解析顺序中,A在B前面,那C的所有子类都必须是这个顺序 经典的方法是深度优先搜索,而新方法(C3)是一种广度优先搜索的方法。 gevent为Python提供了较为全面的协程支持。 gevent 是第三方库,通过greenlet实现协程,基本思想是: 当一个greenlet 遇到IO操作时,比如访问网络,就会切换到其他的greenlet,等到IO操作完成时再切换回来执行。由于IO操作非常耗时,常常使程序处于等待状态,greenlet使得总有其在运行而不是等待IO 12345678910111213from gevent import monkeyimport geventdef f(n): for i in range(n): print(gevent.getcurrent(), i) g1 = gevent.spawn(f, 5)g2 = gevent.spawn(f, 5)g3 = gevent.spawn(f, 5)g1.join()g2.join()g2.join() 协程协程又称为微线程 协程的概念很早就出来了,但是近几年才得到广泛应用 由协程定义的过程可能会交替执行,有点像多线程,协程相比于多线程有以下优势 协程有极高的执行效率,子进程切换不会引起线程切换,没有线程的切换开销 不需要多线程的锁机制,也不存在资源的写变量冲突,执行效率较高 is 和 == 的区别Python中的值有三个基本的要素:id(身份表示)、type(数据类型)、value(值) is和==都是比较判断,但是内容不同 is比较的是id, 而== 比较的是值 xrange 和 rangerange直接返回一个列表 xrange返回一个对象,显示调用list将其转换为一个列表 大数列的话xrange效率高不需要开辟较大的空间,内存空间使用较少 Python元类元类是一个深奥的面向对象的概念,隐藏在激活所有的Python代码后 简单的说,元类就是用来创建类的东西。元类就是类的类 函数type实际上是一个元类。 MRO GIL GC Nginx 概述Nginx是一个高性能的HTTP和反向代理服务器,以及电子邮件代理服务器 跨平台、配置简单 非阻塞,高并发连接 如果作为web服务器 nginx能够支持高达50000的并发连接数 内存消耗小, 10个nginx才占用150M内存, 处理静态文件好,耗费内存少 一个master进程生成多个worker进程 nginx 的负载均衡算法nginx的upstream支持4种方式的分配 轮询(默认) 每个请求按照一定的时间顺序逐一分配到不同的后端服务器 weight 指定轮询的几率, 和访问比例呈正比,用于性能不均的情况。 默认深搜分配权重 IP_HASH 每个请求按照访问的ip地址进行hash分配,可以解决session问题 Nginx如何解决惊群现象惊群是指多个子进程在同一时刻监听同一端口引起的 nginx解决方案:同一时刻只能由唯一一个worker 监听web端口,新事件连接只能唤醒唯一监听端口的子进程。 nginx 事件驱动框架nginx 时间驱动架构:所谓事件驱动架构,由一些事情发生源来产生事件,由一个或多个时间收集器来收集事件(epolld)分发事件,许多时间处理器会主持自己感兴趣的事件,同时会消费这些事件。 传统的web服务器事件局限于TCP连接上,其它时间驱动都不是事件驱动。即前者每一个消费者独占一个进程资源,而后者只是时间分发者短期占用进程。 Nginx的多进程模型Apache 创建多个进程或线程,每个进程、线程都会分配CPU,容易榨干服务器资源 nginx 单线程来异步非阻塞的处理请求(可以配置工作进程数量) Nginx处理请求首先nginx启动时,解析配置文件得到端口与IP地址,然后在master进程中初始化好这个监控的socket并listen 之后master进程fork出多个子进程出来(worker)子进程会竞争accpet连接 此时客户端就可以发起连接,当客户端与nginx三次握手后与nginx建立好连接。某个子进程会accept成功,创建对nginx连接的封装,即ngx_connection_t 结构体。 接着,根据事件调用相应的处理模块,如HTTP进行交换。 最后Nginx或客户端关闭连接。 正向代理一个位于客户端和原始服务器之间的服务器,为了从原始服务器获得内容,客户端向代理发送一个请求并制定目标(原始服务器),然后代理服务器向原始服务器请求并返回内容给客户端。 正向代理服务器代理的是 客户端 反向代理代理服务器接受Internet的连接请求,然后将请求分发给内网上的服务器。并将从服务器上得到的结果返回给客户端,这个服务器对外表现为一个反向代理服务器。 反向代理代理的是 服务端 动静分离软件开发中,有些资源需要后台处理(.jsp do),有些不需要(css、html),不需要经过后台处理的是静态文件,静态、动态资源分离可以进行静态文件的缓存。 Nginx 和 Apachedjango对Django 的认识Django是走大而全的方向,它是出名的自动化管理后台,只需要使用ORM,简单的对象定义,自动生成数据库结构,全功能的管理后台。 Django内置了一个ORM框架,与其其他模块耦合性较高,理论上必须使用该框架 Django 的最大优势是开发效率高,django项目达到一定规模后需要重构才能满足性能需求 适用于中小型网站、或大型网站的雏形 Django模板设计哲学是彻底将代码、样式分离 MVC 模型和MTV模式所谓MVC就是把应用分为模型、控制器、视图三层,以插件式、松耦合的方式放在一起,模型负责对象与数据库的映射(ORM)视图负责与用户的交互(页面)控制器接受用户调入模型和视图完成用户请求 而Django的MTV模式本质上和MVC是一样的,知识定义有些序不同 M代表模型;负责业务对象和数据的关系映射(ORM) T代表模板:负责如何把页面展示给用户 V代表视图:负责业务逻辑,并调用model和Template 简述Django的请求生命周期一般用户通过浏览器发送请求,这个请求访问视图函数,视图函数调用模型,模型去数据库查找数据,然后逐级返回,在填充到模板最后返回给用户 wsgi 请求封装候交给web框架(flask django) 中间件,对请求进行验校活在请求中添加相关数据,如(csrf, session) 路由匹配,根据浏览器发送的不同URL去匹配不同的函数 视图函数 视图函数中进行业务处理,可能涉及到(ORM 和模板渲染) 中间件,对响应函数进行处理 wsgi 将响应内容发给浏览器 什么是FBV和CBVfunction based views 视图函数中使用函数处理 class based views 视图函数使用类处理请求 谈谈对ORM的理解ORM是对象关系映射,是MVC框架的一个重要部分。它实现了数据模型与数据的解耦,即数据模型的设计不依赖特定的数据库,通过简单的配置就可以更换数据库,极大地减轻了开发人员的工作量。 谈谈对restful的理解restful 其实就是一套编写接口的协议,协议规定如何编写及如何设置返回值,状态信息码等。 其实原理和django的CBV类似,便于前后端分离。 提供了许多方便的组件, 序列化器:用户请求验校+queryset 对象序列化为json 解析器:获取用户请求数据 request.data 分页: 从数据库中获取到的数据在页面进行分页展示 认证、权限、访问频率控制 中间件中间件是用来处理Django用来处理请求和相应框架级别的钩子,它是一个轻量,低级别的插件系统,用于在全局范围改变Django的输入和输出,每个中间件负责一些特定功能。 中间件会拦截一部分请求,比如验证session,没有登录的进行跳转。 Django中的一些重要语句1234567891011# 原生SQL语句from django.db import connectioncursor = connection.cursor()cursor.execute(\"select * from auth_user\")# Django ORM批量创建数据querysetlist = []for i in res: querysetlist.append(Account(name=i))Account.objects.bulk_create(querylist) Django 查询的特性 惰性执行、缓存 创建查询集不会访问数据库,直到使用数据时才会访问数据库,调用数据的情况包括迭代、序列化、与if合用 什么是wsgi、uwsgi、uWSGIWSGI: web服务器的网关接口,是一套协议,用于接受用户请求并进行初次封装,然后交给web服务器 实现wsgi协议的模块: wsgiref 本质上是一个socket服务器,用于接受用户请求(django) werkzeug 本质上是一个socket服务器,用于接受用户请求(flask) 而uwsgi 也是一种通信协议 uWSGI是一个web服务器,实现了上述协议 Django 中的csrf (防止跨域请求伪造)实现机制第一步: Django 第一次相应客户端请求时,随机产生一个token,把这个token保存在session中,同时把其发送给前端 第二步:下次前端发起请求(比如发帖时)把这个token带入请求头中一起传给后端cokie:(csrftoken:…) 第三步:后端检验前端传入的token与session是否一致 AjaxAjax(Asynchronous javascript and xml)能够局部刷新网页数据,而不是重新加载 第一步:创建xmlhettprequest对象,val xmlhttp = new XMLHttpRequest() 第二步:使用xml对象的open和send方法发送资源请求给服务器 第三步:使用xml对象的responstest 和 response XML 获得服务器的响应 第四步:onreadystagechange Ajax是一种快速创建动态网页的技术 通过在后台与服务器进行少量的服务交换,AJAX可以实现异步更新。这意外着它不需要重新加载整个网页的情况下对网页的某部分进行更新。 传统的网页如果更新内容必须重新加载整个页面 12345678910111213function loadXML(){ var xmlhttp; if(windows.XMLHttpRequset){ xmlhttp = new XMLHttpRequest(); } xmlhttp.onreadystatechange=function(){ if(xmlhttp.readyState==4 && xmlhttp.states==200){ document.getElementById(\"MyDiv\").innerHTML = xmlhttp.responseText; } } xml.open(\"GET\", \"/something\", true); xmlhtthp.send();}","categories":[],"tags":[{"name":"后台开发","slug":"后台开发","permalink":"http://yoursite.com/tags/后台开发/"}]},{"title":"[后台开发工程师总结系列] 8.STL概论","slug":"后台开发工程师总结系列-8-STL概论","date":"2019-03-09T09:08:01.000Z","updated":"2019-03-09T09:16:35.328Z","comments":true,"path":"2019/03/09/后台开发工程师总结系列-8-STL概论/","link":"","permalink":"http://yoursite.com/2019/03/09/后台开发工程师总结系列-8-STL概论/","excerpt":"STL概论 长久以来软件届一直希望建立一种可复用的东西,以及一种得以造出“可重复运用东西”的方法。 子程序、程序、函数、类别、函数库、类别库、组件、结构模块化设计、模式、面向对象 … 都是为了 复用性的提升 复用性必须建立在某种标准之上,但是在许多环境下开发最基本的算法和数据结构还迟迟不能有标准。大量程序员从事重复劳动,完成前人完成而自己不拥有的代码。","text":"STL概论 长久以来软件届一直希望建立一种可复用的东西,以及一种得以造出“可重复运用东西”的方法。 子程序、程序、函数、类别、函数库、类别库、组件、结构模块化设计、模式、面向对象 … 都是为了 复用性的提升 复用性必须建立在某种标准之上,但是在许多环境下开发最基本的算法和数据结构还迟迟不能有标准。大量程序员从事重复劳动,完成前人完成而自己不拥有的代码。 例子 现在需要编写一个排序函数,这涉及到一个问题,从流中读入数据事先并不知道数据的长度 ,然而C/C++ 数组只能申请定长数组,超过数组上限就会出现越界。为了弥补该缺陷,就必须采用下列方案的一种: 采用大容量的静态数据分配 限定输入的数据个数 采用动态的内存分配 显然,前两种方法都有缺陷。只能使用指针及动态内存来妥善解决上述问题,使程序具有较好的灵活性。这需要New()、delete() 或者 malloc() realloc() free() 等函数,这样程序就会相当的不简洁。 为了建立数据结构算法的标准,降低耦关系,提升独立性、弹性,交互操作性,诞生了STL 六大组件简单介绍 空间配置器:内存池实现小块内存分配,对应到设计模式–单例模式(工具类,提供服务,一个程序只需要一个空间配置器即可),享元模式(小块内存统一由内存池进行管理) 迭代器:迭代器模式,模板方法 容器:STL的核心之一,其他组件围绕容器进行工作:迭代器提供访问方式,空间配置器提供容器内存分配,算法对容器中数据进行处理,仿函数伪算法提供具体的策略,类型萃取 实现对自定义类型内部类型提取。保证算法覆盖性。其中涉及到的设计模式:组合模式(树形结构),门面模式(外部接口提供),适配器模式(stack,queue通过deque适配得到),建造者模式(不同类型树的建立过程)。 类型萃取:基于范型编程的内部类型解析,通过typename获取。可以获取迭代器内部类型value_type,Poter,Reference等。(算法) 仿函数:一种类似于函数指针的可回调机制,用于算法中的决策处理。涉及:策略模式,模板方法。 适配器:STL中的stack,queue通过双端队列deque适配实现,map,set通过RB-Tree适配实现。涉及适配器模式。 空间配置器 allocator软件开发中,不免于程序需求可能使用很多小块内存,在程序中动态的申请和释放。这个过程不一定能控制好,所以可能出现以下问题: 内存碎片问题 因为一直申请小块内存,malloc系统产生调用性能问题 内部碎片:因为内存对齐、访问效率(CPU取址次数)而产生,比如申请3字节而得到4字节或8字节 外部碎片:系统中该内存中总量足够,但是不连续而造成的浪费。 下面是STL空间配置器的细节 12345if(n > (size_t)_MAX_BYTES){ return malloc_alloc::allocate(n); // 一级空间配置器}else{ // 二级空间配置器} 大致实现: 一级空间配置器直接封装malloc,free 进行处理,增加了C++中的set_handler机制,增加内存分配时客户端可选处理机制。 一级空间配置器12345678910111213141516171819202122232425262728static void *Allocate(size_t n){ ___TRACE(\"__MallocAllocTemplate to get n = %u\\n\",n); void *result = malloc(n); if (0 == result) { result = OomMalloc(n); } return result;}void *__MallocAllocTemplate::OomMalloc(size_t n){ ___TRACE(\"一级空间配置器,不足进入Oo中n = %u\\n\",n); void(*my_malloc_handler)(); void* result; for (;;) // 不断尝试释放、配置、再释放、再配置 { my_malloc_handler = __malloc_alloc_oom_handler; if (0 == my_malloc_handler) { __THROW_BAD_ALLOC; } (*__malloc_alloc_oom_handler)(); //调用处理历程,企图释放内存 result = malloc(n); if (result) return result; }} 第一级配置器以malloc()、free()、realloc() 等C函数执行实际的内存配置和释放,类似于C++ 的new-handler机制,但是他不能直接使用(未使用::operator new来配置内存)。 所谓new-handler 机制是,要求系统在内存配置无法满足时,调用你指定的函数,一旦new无法完成任务,在丢出异常之前,调用客户端的历程,这就被称为new-handler。 请注意,第一级配置器都是在malloc和realloc调用不成功后,改为调用oom_malloc,后两者内有循环,不断调用 “内存不足例程”,但是如果这个例程未被设定,便老实的 __THROW_BAD_ALLOC; 二级空间配置器二级空间配置器避免了太多额外的小区造成的内存碎片。二级配置器的做法是,如果区块足够大(超过128字节)则以内存池管理,此法又被称为层次管理:每次配置一块大内存,并且维护自由链表。下次如果还有相同的内存需求,就从free-list中拨出,如果客户端释放,就回收到free-list中,配置器配置也回收,为了方便起见,二级配置器主动把内存的需求量上调至8的倍数。 123456789101112131415161718192021222324 static size_t _FreeListIndex(size_t __bytes) { return (((__bytes) + (size_t)_ALIGN-1)/(size_t)_ALIGN - 1); }static void* Allocate(size_t n) { void * ret = 0; ___TRACE(\"二级空间配置器申请n = %u\\n\",n); if(n>_MAX_BYTES) ret = MallocAlloc::Allocate(n); _Obj* volatile * __my_free_list = _freeList + _FreeListIndex(n); _Obj* __result = *__my_free_list; if (__result == 0) ret = _Refill(RoundUp(n)); else { *__my_free_list = __result -> _freeListLink; ret = __result; } return ret; } chunk_alloc() 函数以 end_free - start_free 来判断内存池。 如果容量充足,就一次性调20个区块给free-list, 但是如果剩余区块不够20 但是大于1, 就拨出剩余能拨出的区块。最后如果剩余不到1个区块了 ,那就重新从堆中配置内存。新的配置量是一个两倍的需求加一个逐渐增大的附加量。 迭代器迭代器是一种抽象的设计概念,现实程序语言中并没有直接对应这个概念的实物。 《设计模式》中对于迭代器的定义如下:提供一种方法,使之能够依次巡防某个聚合物容器所含的各个元素,而又无需包括该聚合物内部的表述方式。 不论是泛型思维或STL的实际应用,迭代器都扮演着重要角色。STL的中心思想在于,将数据容器和算法分开,彼此独立设计,最后再将他们撮合在一起,而迭代器就扮演了这个黏胶角色。 迭代器是一种行为类似指针的对象,而指针行为中最常见的内容是提领(dereference)和成员访问(member access)因此迭代器最重要的工作就是对opreator* 和 opreator-> 进行重载工作。关于这一点C++有智能指针。 STL 容器 容器 底层数据结构 时间复杂度 有无序 可不可重复 其他 array 数组 随机读改 O(1) 无序 可重复 支持快速随机访问 vector 数组 随机读改、尾部插入、尾部删除 O(1) 头部插入、头部删除 O(n) 无序 可重复 支持快速随机访问 list 双向链表 插入、删除 O(1) 随机读改 O(n) 无序 可重复 支持快速增删 deque 双端队列 头尾插入、头尾删除 O(1) 无序 可重复 一个中央控制器 + 多个缓冲区,支持首尾快速增删,支持随机访问 stack deque / list 顶部插入、顶部删除 O(1) 无序 可重复 deque 或 list 封闭头端开口,不用 vector 的原因应该是容量大小有限制,扩容耗时 queue deque / list 尾部插入、头部删除 O(1) 无序 可重复 deque 或 list 封闭头端开口,不用 vector 的原因应该是容量大小有限制,扩容耗时 priority_queue vector + max-heap 插入、删除 O(log2n) 有序 可重复 vector容器+heap处理规则 set 红黑树 插入、删除、查找 O(log2n) 有序 不可重复 multiset 红黑树 插入、删除、查找 O(log2n) 有序 可重复 map 红黑树 插入、删除、查找 O(log2n) 有序 不可重复 multimap 红黑树 插入、删除、查找 O(log2n) 有序 可重复 hash_set 哈希表 插入、删除、查找 O(1) 最差 O(n) 无序 不可重复 hash_multiset 哈希表 插入、删除、查找 O(1) 最差 O(n) 无序 可重复 hash_map 哈希表 插入、删除、查找 O(1) 最差 O(n) 无序 不可重复 hash_multimap 哈希表 插入、删除、查找 O(1) 最差 O(n) 无序 可重复 vectorvector 是线性容器,它的元素按照线性排列,和动态数组类似。和数组类似的是,它的元素存在一块连续的内存空间中,这意味着不仅可以通过迭代器访问,还可以使用指针偏移访问元素。和数组不一样的是,vector能够自动增长或缩短储存空间。 vector的优点如下所示: 可以使用下标访问个别元素 迭代器可以按照不同的顺序遍历容器 可以在容器的默认增加、删除元素 关于STl容器,只要不超过最大值,其可以自动增长到足以容纳用户放进去的数据大小。(这个容量值,可以同过调用maxsize()来获得) 对于vector和string,如果需要更多的空间,类似realloc思想增长,vector支持随机访问,因此提高了效率,内部通过动态数组实现。 123456789101112131415161718192021//vector 查找vector<int>::iterator iter;iter = find(vec.begin(),vec.end(),3);//vector 删除iterator erase(iterator position) // 删除后指向删除元素的下一个位置iterator erase(iterator frist, iterator last)vector<int>::iterator iter = vec.begin();while(iter!=vec.end()){ if(*iter==target){ iter = erase(iter); }else{ iter++; }}//vector 增加iterator insert(iterator loc, const TYPE &val); // 指定位置loc 插入值为val的元素,返回指向这个元素的迭代器void insert(itrator loc, size_type num, const TYPE &val); // 同上,插入num 个元素 void insert(itrator loc, itrator start, itrator end); // 插入迭代器区间的元素 vector内存管理 使用reverse() 提前设定容量大小 STL最令人称赞的特性之一就是只要不超过最大值,其可以自动增长。如果需要更多空间就会类似realloc的思想来增长大小。vector容器支持随机访问,因此效率较高。 如果有大量的元素需要push_back,应提前使用reverse函数提前设定,避免多次的扩容操作,介绍一下只有vector与string提供的几个函数 函数名 介绍 size() 获得容器中的元素个数,但不能获得容器为他分配的容器大小 capacity() 获得容器已经分配的内存容纳多少元素。 resize() 用来强制把容器改为容纳n个元素。如果n小于当前大小,尾部元素会被销毁;如果n大于当前大小,默认构造元素添加到元素尾;如果大于当前容量,触发重新分配 reverse() 强制把容量改为不小于n,n不小于当前大小 交换技巧来修整内存 有一种方法将它的最大容量修正到其当前容量,这种方法被称为“收缩到合适”,只需要一条语句 1vector<int>(ivec).swap(ivec); swap强行释放内存 12345template<class T> void VlearVector(vector<T>& v){ vector<T> vTemp; vTemp.swap(v);} vector类简单实现第二版参考《stl源码》 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103template<class T, class Alloc=alloc>class vector{public: typedef T value_type; typedef value_type* pointer; typedef value_type* iterator; typedef value_type& reference; typedef size_t size_type; typedef ptrdiff_t difference_type;protected: typedef simple_alloc<value_type, Alloc> data_allocator; iterator start; iterator finish; iterator end_of_storage; void insert_aux(iterator posotion, const T& x); void deallocate(){ if(start) data_allocator::deallocate(start,end_of_storage-start); } void fill_initialize(size_type n, const T& vlaue){ start = allocate_and_fill(n, value); finish = start + n; end_of_storage = finish; }public: iterator begin(){return start;} iterator end(){return finish;} size_type size() const {return size_type(end()-begin());} size_type capacity() const{return size_type(end_of_storage-begin());} bool empty() const {return engin()==end()} reference operator[](size_type n){return *(begin()+n);} vector():start(0),finish(0),end_of_storage(0){} vector(size_type n,const T& value){fill_initialize(n,value);} vector(int n,const T& value){fill_initialize(n,value);} vector(long n,const T& value){fill_initialize(n,value);} explicit vector(size_type n) {fill_initlialize(n,T());} ~vector(){ destroy(start,finish); deallocate(); } reference front(){ return *begin();} reference back() { return *(end()-1);} void push_back(const T& x){ if(finish!=end_of_storage){ construct(finish, x); ++finish; }else insert_aux(end(),x); } void pop_back(){ --finish; destory(finish); } iterator erase(iterator position){ if(position+1 != end()) copy(position+1,finish,position); --finish; destory(finish); return position; } void resize(size_type new_size, const T& x){ if(new_size<size()) erase(begin()+new_size(),end()); else insert(end(),newsize()-size(),x); } void resize(size_type new_size){resize(new_size,T());} void clear(){erase(begin(),end());}protected:iterator allocate_and_fill(size_type n, const T& x){ iterator result = data_allocatpr:: }}template<class T, class Alloc>void vector<T, Alloc>::insert_aux(iterator position, const T& x){ if(finish != end_of_storage){ construct(finish,*(finish-1)); ++finish; T x_copy = x; copy_backword(position, finish-2,finish-1); *position = x_copy; }else{ const size_type old_size = size(); const size_type len = old_size != 0?2*oldsize:1; iterator new_start = data_allocator::allocate(len); iterator new_finish = new_start; try { new_finish = uninitialized_copy(start,posotion,new_start); construct(new_finish, x); ++new_finish; new_finish = uninitialized_copy(positon,finish,new_finish); } catch(...){ destroy(new_start,new_finish); data_allocator::deallocate(new_start,len); thow; } destroy(begin(),end()); deallocate(); start = new_start; finish = new_finish; end_of_storage = new_start+len; }} map、set map的本质 map本质是一类关联式容器,属于模板类关联的本质在于元素的值与某个特点的键相关联,而并非通过元素在数组的位置获取。它的特点是增加和删除节点对迭代器的影响很小,除了要操作的节点,对其他节点都没什么影响。对迭代器来数不能修改键值,只能修改实值。map内部自建一颗红黑树(非严格意义的平衡二叉树),该树内部数据有序 12345678910111213141516171819定义: map<T_key, T_value> mymap;插入元素: mymap.insert(pair<T_key, T_value>(key, value)); //同key不插入 mymap.insert(map<T_key, T_value>::value_type(key, value)); //同key不插入 mymap[key] = value; //同key覆盖删除元素: mymap.erase(key); //按值删 mymap.erase(iterator); //按迭代器删修改元素: mymap[key] = new_value;遍历容器: for(auto it = mymap.begin(); it != mymap.end(); ++it){ cout<< it->first << \" => \" << it->second << '\\n'; } 基于红黑树实现的map结构(实际上是map, set, multimap,multiset底层均是红黑树),不仅增删数据时不需要移动数据,其所有操作都可以在O(logn)时间范围内完成。另外,基于红黑树的map在通过迭代器遍历时,得到的是key按序排列后的结果,这点特性在很多操作中非常方便。 红黑树的特性 它是一颗二叉排序树(继承二叉排序树的特点): 若左子树不空,则左子树的上所有节点的值均小于等于它根节点的值 若右子树不空, 则右子树上所有节点的值均大于等于它根节点的值 左右子树分别也是二叉排序树。 红黑树的要求 树中所有节点非红即黑 根节点必为黑节点 红节点的子节点必须为黑 从根到任何发路径上的的黑节点数相同 查找时间一定可以控制在O(log N)之内 红黑树定义如下 12345678910enum Color { RED = 0, BLACK = 1};struct RBTreeNode { struct RBTreeNode*left, *right, *parent; int key; int data; Color color;}; 所以对红黑树的操作需要满足两点:1.满足二叉排序树的要求;2.满足红黑树自身要求。通常在找到节点通过和根节点比较找到插入位置之后,还需要结合红黑树自身限制条件对子树进行左旋和右旋。 相比于AVL树,红黑树平衡性要稍微差一些,不过创建红黑树时所需的旋转操作也会少很多。相比于最简单的BST,BST最差情况下查找的时间复杂度会上升至O(n),而红黑树最坏情况下查找效率依旧是O(logn)。所以说红黑树之所以能够在STL及Linux内核中被广泛应用就是因为其折中了两种方案,既减少了树高,又减少了建树时旋转的次数。 从红黑树的定义来看,红黑树从根到NULL的每条路径拥有相同的黑节点数(假设为n),所以最短的路径长度为n(全为黑节点情况)。因为红节点不能连续出现,所以路径最长的情况就是插入最多的红色节点,在黑节点数一致的情况下,最可观的情况就是黑红黑红排列……最长路径不会大于2n,这里路径长就是树高。 hashtablehashtable被视为一种字典结构,提供对于任何有名项的存取操作和删除操作。 如何避免array过大?是用某种映射函数,使得元素映射至“大小可以接受的索引”,这个函数被称为散列函数。使用散列函数必然会带来一个问题:可能有不同的元素被映射到相同的位置,这便是所谓的碰撞问题。 碰撞问题一般有两种方案:拉链法、线性探测法 线性探测提出一个名词:负载系数,元素个数除以表格大小。负载系数永远在0~1之间, 当 散列函数 计算某个位置,而该位置不可用该怎么办? 最简单的方法就是循序接着往下找,只要表格足够大总会找到一个空间。而删除必须采取惰性删除(标记删除序号,实际删除待表格整理时再进行) 两个假设 1.表格足够大,2 每个元素独立 但实际情况往往不乐观,往往需要不断解决碰撞问题 二次探测1234567891011121314151617181920212223static int prime[28] ={ 57, 97, 193, 389, 769, 1543, 3079, 6151, 12289, 24593, 49157, 98317, 196613, 393241, 786433, 1572869, 3145739, 6291469, 12582917, 25165843, 50331653, 100663319, 201326611, 402653189, 805306457, 1610612741};class HashMapUtil{public: static int find_NextPrimeNumber(int current) { //Find the next prime number by searching in the prime number list int i = 0; for( ; i < 28 ; i++ ) if(current < prime[i]) break; return prime[i]; //return the next larger prime. }}; 二次探测主要用来解决 主集团问题, 该方法描述很简单,碰撞时尝试 H+1*1,H+2*2, H+ 3*3来代替H+1, H+2,H+3 等。幸运的是,假设表格大小为质数,永远保持0.5以下的负载系数,每个新元素的插入查找不大于2 另外,二次探测有个简便的计算技巧 12345hi = h0 + i\\*i (MOD M)hi-1 = h0 + (i-1) *(i-1) (MOD M)整理得,hi = hi-1 + 2*i-1 (MOD M) 这样每一次迭代并不需要太大的计算资源。 开链拉链法,在《STL源码》中被称为开链,及对于冲突的元素维护一个链表 从上图我们可以发现哈希表是由数组+链表组成的,一个长度为16的数组中,每个元素存储的是一个链表的头结点。那么这些元素是按照什么样的规则存储到数组中呢。一般情况是通过hash(key)%len获得,也就是元素的key的哈希值对数组长度取模得到。比如上述哈希表中,12%16=12,28%16=12,108%16=12,140%16=12。所以12、28、108以及140都存储在数组下标为12的位置。 HashMap其实也是一个线性的数组实现的,所以可以理解为其存储数据的容器就是一个线性数组。这可能让我们很不解,一个线性的数组怎么实现按键值对来存取数据呢?这里HashMap有做一些处理。 首先HashMap里面实现一个静态内部类Entry,其重要的属性有 key , value, next,从属性key,value我们就能很明显的看出来Entry就是HashMap键值对实现的一个基础bean,我们上面说到HashMap的基础就是一个线性数组,这个数组就是Entry[],Map里面的内容都保存在Entry[]里面。 hash算法的存取123456789// 存储时:int hash = key.hashCode(); int index = hash % Entry[].length;Entry[index] = value;// 取值时:int hash = key.hashCode();int index = hash % Entry[].length;return Entry[index]; 面试常考题1. vector 的结构 123456789101112131415161718192021222324252627282930public: typedef T value_type; typedef value_type* pointer; typedef value_type* iterator; typedef value_type& reference; typedef size_t size_type; typedef ptrdiff_t difference_type;protected: typedef simple_alloc<value_type, Alloc> data_allocator; iterator start; iterator finish; iterator end_of_storage; void insert_aux(iterator posotion, const T& x); void deallocate(){} void fill_initialize(size_type n, const T& vlaue){}public: iterator begin(){return start;} iterator end(){return finish;} size_type size() const {return size_type(end()-begin());} size_type capacity() const{return size_type(end_of_storage-begin());} bool empty() const {return engin()==end()} reference operator[](size_type n){return *(begin()+n);} vector():start(0),finish(0),end_of_storage(0){} vector(size_type n,const T& value){fill_initialize(n,value);} vector(int n,const T& value){fill_initialize(n,value);} vector(long n,const T& value){fill_initialize(n,value);} explicit vector(size_type n) {fill_initlialize(n,T());} ~vector() reference front(){ return *begin();} reference back() { return *(end()-1);} 2. vector 和 list 的区别和应用场景 vector 是地址空间连续,支持随机存取的数组。优点是存取方便,缺点是非末尾的增、删复杂度较高。 list 是不支持随机的存取的链表,优点是增、删复杂度O(1),缺点是查找复杂度O(n) vector拥有一段连续的内存空间,因此支持随机访问,如果需要高效的随即访问,而不在乎插入和删除的效率,使用vector。list拥有一段不连续的内存空间,如果需要高效的插入和删除,而不关心随机访问,则应使用list。 3. vector怎么增加内存 通过源码对比回答, 当vector因为增加元素而超出容量,即(finish==capacity时),进行以下操作 保存原指针(start, finish, capacity) 计算新数组的容量大小(为原大小的两倍),并申请这样大小的空间,记录新的数组指针(new_start, new finish , new_capacity) 在新的数组中复制原数组, 然后完成当下的增加元素操作。 释放原数组空间 用新数组指针替换原指针。 4. 遍历vector的几种写法 123456789101112// 1. [] 数组遍历for(int i=0; i<vec.size(); i++){ cout<<vec[i]<<endl;}// 2. 迭代器遍历for(vector<int>::iterator it = vec.begin(); it!=vec.end(); it++){ cout<<*it<<endl;}// 3. C++11 的新特性冒号遍历(不支持修改)for(int num:vec){ cout<<num<<endl;} 5. 对STL内存分配的理解,为什要有空间配置器 软件开发中,不免于程序需求可能使用很多小块内存,在程序中动态的申请和释放。这个过程不一定能控制好,所以可能出现以下问题:内存碎片问题、因为一直申请小块内存,malloc系统产生调用性能问题。 为了解决以上问题,而且为了方便管理malloc申请空间不足的情况,STL模拟了new_handler 提供用户错误处理接口。 6. hashtable java实现 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283package prepare;/** * @author : Menghui Chen * @version :2018年3月20日 下午7:14:04 * @Description:*/public class HashMap<T, V> { static class Node<T, V> { Node<T, V> next; private T key; private V value; public void setKey(T k) { this.key = k; } public T getKey() { return key; } public void setValue(V value) { this.value = value; } public V getValue() { return value; } public Node(T k, V v) { this.key = k; this.value = v; } } Node[] tab; public HashMap(int capacity) { tab = new Node[capacity]; } public V put(T key, V value) { int index = getIndex(key); Node node = new Node(key, value); if (tab[index] == null) { tab[index] = node; return null; } else { Node head = tab[index]; Node pre = searchList(head, key); if (pre.next == null) { pre.next = node; return null; } else { V old = (V) pre.next.value;//here pre.next.value = value; return old; } } } public Node searchList(Node node, T k) { Node pre = null; while (node != null) { if (hash((T)node.key) == hash(k) && (node.key == k || node.key.equals(k))) { break; } pre = node; node = node.next; } return pre; } public V get(T key) { int index = getIndex(key); if (tab[index] == null) { return null; } else { Node head = tab[index]; head = searchList(head, key); if (head == null && head.next == null) { return null; } else { return (V)head.next.value; } } } public int hash(T key) { return (key.hashCode() >> 16) ^ key.hashCode(); } public int getIndex(T key) { return hash(key) % tab.length; }} 7. map 与 unordered_map 的区别和应用场景 map本质是一类关联式容器,属于模板类关联的本质在于元素的值与某个特点的键相关联,而并非通过元素在数组的位置获取。它是一种通过内建红黑树实现了O(logN) 时间复杂度内实现 增删改查的数据结构,且其内部元素有序。 unordered_map被视为一种字典结构,提供对于任何有名项的存取操作和删除操作。利用哈希映射表实现了元素的增、删、查、改操作 二者的主要区别在于map有序, 而unordered_map无序, 他们的应用场景也主要取决于是否要求元素有序。 STL中有几种map unordered_map 哈希表 不可重复 无序 map 红黑树 不可重复 有序 multi-map 红黑树 有序","categories":[],"tags":[{"name":"后台开发","slug":"后台开发","permalink":"http://yoursite.com/tags/后台开发/"}]},{"title":"[后台开发工程师总结系列] 7.Redis简介","slug":"后台开发工程师总结系列-7-Redis简介","date":"2019-03-09T09:06:44.000Z","updated":"2019-03-09T09:20:15.332Z","comments":true,"path":"2019/03/09/后台开发工程师总结系列-7-Redis简介/","link":"","permalink":"http://yoursite.com/2019/03/09/后台开发工程师总结系列-7-Redis简介/","excerpt":"Redis特点Redis 本质上是一个Key-value类型的内存数据库,很像memcached, 整个数据库在内存中进行操作,定期通过异步操作把数据库数据flush到硬盘上进行保护,因为是存内存操作,Redis性能十分出色,每秒可以处理10万次的读写操作,是已知性能最快的DB。 Redis性能出色之处不仅仅是性能,Redis最大魅力是支持多种数据结构,Redis单个value的最大限制是1GB, 而memcached只能保证1MB的数据。Redis 可以用LIST做双向链表实现一个轻量级的高性能消息服务队列。 Redis的主要缺点是数据库容量收到物理内存的限制,不能用做海量数据的高性能读写","text":"Redis特点Redis 本质上是一个Key-value类型的内存数据库,很像memcached, 整个数据库在内存中进行操作,定期通过异步操作把数据库数据flush到硬盘上进行保护,因为是存内存操作,Redis性能十分出色,每秒可以处理10万次的读写操作,是已知性能最快的DB。 Redis性能出色之处不仅仅是性能,Redis最大魅力是支持多种数据结构,Redis单个value的最大限制是1GB, 而memcached只能保证1MB的数据。Redis 可以用LIST做双向链表实现一个轻量级的高性能消息服务队列。 Redis的主要缺点是数据库容量收到物理内存的限制,不能用做海量数据的高性能读写 redis的好处 速度快,数据存在内存中,类似hashmap, 查找时间复杂度O(1) 支持丰富的数据类型 string、lsit、set、zset、hash 支持事务,操作都是原子性的 丰富的特性,可以用于缓存、消息 memchached 与 Redis 的区别 储存方式,Memched数据都在内存中,断电后会挂掉。Redis有数据的持久性 数据支持类型上:Memcached 对数据类型支持简单,而Redis有复杂的数据类型 底层的模型不同,与客户端的通信协议不同,Redis 直接构建了VM机制 跳跃表跳跃表是一种有序的数据结构,它在每个节点中维护多个指向其他节点的指针,从而达到快速访问节点的目的。跳跃表支持平均(logN)最坏(N)复杂度的节点查找,还可以通过顺序操作来批量处理节点。 在大部分情况下,跳跃表效率可以和平衡树媲美,而且其实现比平衡树简单,很多程序用跳表代替平衡树。 Redis跳表用zskiplistNode 和 zskiplist 来定义 最左边是一个zskiplit结构,它包含了以下部分: header: 指向跳跃表的头结点 tail:指向跳跃表的尾节点 level:目前跳表中,层数最大节点的层数 length:跳跃表所含节点的数量 而每一个zskiplistNode含有如下结构 层:节点中L1、L2、L3标记各个层。每个层带有两个属性,前进和跨度。 后退指针:指向前一个节点,逆序遍历使用 分值:各个节点中1、2、3都是分值,从小到大排列 各个节点保存了成员对象 Redis的 RDB 和 AOFRedis是内存数据库,他自己的数据库状态在内存中,如果不想办法从内存保存到磁盘,一旦服务器退出,服务器中的数据库状态也会消失不见。 为了解决这个问题,Redis提供了RDB持久化功能,将内存中的数据保存在磁盘中。 RDB可以手动执行,或根据服务器配置定期执行。 RDB文件的创建命令:SAVE(阻塞)和 BGSAVE(非阻塞) 另外,如果服务器开启了AOF持久化的功能,服务器优先使用AOF还原数据 只有AOF关闭时,才使用RDB来还原数据库状态 RDB是一个压缩的二进制文件。对于不同的键值对,RDB用不同的方式保存。 AOF持久化除了RDB持久化以外,Redis还提供了 AOF 持久化功能。 与RDB持久化通过保存数据中的键值对记录来记录数据库状态不同,AOF持久化是通过保存Redis服务器记录的写命令来记录数据库状态的。 简单的来说,RDB保存的是 a:1 这个键值对, 而AOF 保存的是write a 1这个命令 AOF持久化分为追加、文件写入、文件同步 三个步骤 命令追加 当AOF持久化功能打开时,服务器执行完一个写命令以后,会以协议格式将被执行的命令追加到服务器状态的 aof_buf 缓冲区末尾。 AOF文件的写入与同步 Redis的服务进程是一个事件循环,这个事件循环中文件事件负责接收客户端的命令请求,以及向客户端发送命令,而时间事件就是想servercron这样定时函数 为了提高文件的写入效率,现代操作系统中用户调用write函数写文件时常常将其保存到一个缓冲区中,等到缓冲区被填满时、或者超过了指定的时限后,才真正的将缓冲区数据写入磁盘 这种写法虽然高效,但是也为写入数据带来了安全性问题,因为如果计算机发生停机,缓冲区数据会消失。为此系统提供了Fsync函数,强制写缓冲区数据 AOF持久化的效率和安全性 取决于appendfsync 当其为always时, 服务器在每个事件循环都将缓冲区写入文件,并同步AOF文件。everysec 每个事件循环都写文件,但是1s同步一次 no 每个事件循环写文件,同步等待系统决定 执行 BGwriteaof 命令时, Redis服务器会维护一个AOF重写缓冲区,该缓冲区会在子进程创建AOF文件期间,记录服务器所有的写命令。当子进程完成创建新的AOF工作后,服务器会将重写缓冲区中的内容追加到AOF文件末尾,使得AOF 保存数据库状态一致。最后用新的AOF文件完成AOF文件的重写操作。","categories":[],"tags":[{"name":"后台开发","slug":"后台开发","permalink":"http://yoursite.com/tags/后台开发/"}]},{"title":"[后台开发工程师总结系列] 6.数据库原理及MySQL","slug":"后台开发工程师总结系列-6-数据库原理及MySQL","date":"2019-03-09T08:59:49.000Z","updated":"2019-03-09T09:07:17.164Z","comments":true,"path":"2019/03/09/后台开发工程师总结系列-6-数据库原理及MySQL/","link":"","permalink":"http://yoursite.com/2019/03/09/后台开发工程师总结系列-6-数据库原理及MySQL/","excerpt":"数据库系统原理事务事务指满足ACID特性的一组操作,可以通过commit 提交,也可以通过rollback回滚 事务的ACID特性 原子性(Atomicity) 事务被视为不可分割的最小单元,事务的所有操作要么全部成功提交,要么全部失败回滚,回滚可以使用回滚日志来实现,回滚事务记录着执行的修改操作,回滚时反向执行即可 一致性(Consistency) 数据在事务的执行前后都保持一致性状态,在一致性状态下,所有事务对一个数据的读取结构都是相同的 隔离性(Isolation) 一个事务所做的修改在最终提交以前对其他事务不可见 持久性(Durability) 一旦事务提交,则其所做修改会永远保存到数据库中,即使系统崩溃事务执行结果也不能丢失,使用重做日志来保障持久性 事务的ACID特性比较简单,但是并不容易理解,他们并不平级 只有满足一致性,事务的执行结果才是正确的的 在无并发的情况下,事务串行执行,隔离性一定能满足。此时只要满足原子性,一致性即可满足 并发的情况下,多个事务并行执行,事务不仅满足原子性,还要满足隔离性 才能满足一致性 事务满足持久性是为了维护数据库崩溃的情况","text":"数据库系统原理事务事务指满足ACID特性的一组操作,可以通过commit 提交,也可以通过rollback回滚 事务的ACID特性 原子性(Atomicity) 事务被视为不可分割的最小单元,事务的所有操作要么全部成功提交,要么全部失败回滚,回滚可以使用回滚日志来实现,回滚事务记录着执行的修改操作,回滚时反向执行即可 一致性(Consistency) 数据在事务的执行前后都保持一致性状态,在一致性状态下,所有事务对一个数据的读取结构都是相同的 隔离性(Isolation) 一个事务所做的修改在最终提交以前对其他事务不可见 持久性(Durability) 一旦事务提交,则其所做修改会永远保存到数据库中,即使系统崩溃事务执行结果也不能丢失,使用重做日志来保障持久性 事务的ACID特性比较简单,但是并不容易理解,他们并不平级 只有满足一致性,事务的执行结果才是正确的的 在无并发的情况下,事务串行执行,隔离性一定能满足。此时只要满足原子性,一致性即可满足 并发的情况下,多个事务并行执行,事务不仅满足原子性,还要满足隔离性 才能满足一致性 事务满足持久性是为了维护数据库崩溃的情况 事务的自动提交 AUTO_COMMITMySQL默认采用自动的提交模式,即,如果不显示使用START TRANSACTION来开始一个事务,每个查询都会被当做一个事务自动提交 事务的模型抽象成功完成的事务称为已提交,而非成功完成的事务被中止了,为了确保数据的原子性,中止事务对数据库状态不可以造成影响,因此这些影响必须被撤销。一旦中止事务的影响被撤销,我们称事务已回滚 ,数据库通过日志来支持回滚操作 一个简单事务抽象模型包括: 活动的:初始状态,事务执行时处于这个状态 部分提交的:最后一条语句执行后 失败的:发现正常的执行不能继续后 中止的:事务回滚并恢复到事务开始执行的前后 提交的:成功完成后 事务的隔离性事务处理系统通常允许多个事务并发的执行,并发有两条无法拒绝的理由 提高吞吐量和资源利用率 减少等待时间 数据库中并发的动机和多道程序设计的动机是相同的,而为提升效率的同时,我们必须控制事务的交互,来保证数据库的一致性,这被称为系统的并发控制 在并发执行时,通过保证所执行的任何调度效果都与没有并发效果一样,我们可以保持数据库的一致性。调度在某种意义上等价于一个串行调度,这种调度被称为可串行化调度 事务的并发可串行化串行化顺序可以通过拓扑排序得到。 对于优先图来说,读写、写读、写写被称为一条边,考察这个有向图中是否有环,无环的优先图被称为可串行化调度 并发一致性问题在并发环境下,事务的隔离性很难保证,因此可能出现并发一致性问题 问题 原因 图例 丢失修改 T1和T2 两个事务都对一个事务进行修改,T1先修改T2随后修改,T2的修改覆盖了T1的修改 读脏数据 T1修改了一个数据,T2最后读取了这个数据。如果T1撤销了这次修改,那么T2读取的数据是脏数据 不可重复读 T2读取了一个数据,T1对其进行了修改,如果T2再次读取这个数据,此时读取结果和第一次不同 幻影读 T1读取某个范围内的数据,T2 在这个范围内插入新的数据,T1再次读取这个范围内的数据,此时读取的结果和第一次读取的不同 事务的隔离级别 隔离级别 简介 可串行化 即可串行化调度 可重复读 只允许读取已提交数据,事务两次读取的间隙其他事务不得更新 已提交读 只允许读取已提交的数据,允许同一事务读数据的前后不一致 未提交读 允许读取未提交数据 事务的隔离级别与对应的并发问题 隔离级别/并发问题 脏读 不可重复读 幻影读 加锁读 未提交读 √ √ √ × 提交读 × √ √ × 可重复读 × × √ × 可串行化 × × × √ 隔离级别的实现锁通过封锁来实保证事务的可串行化。通过共享、排他锁及两阶段封锁协议来保证串行化下的并发读 时间戳另一类用来实现隔离性的技术为为每一个事务分配一个时间戳,系统维护两个时间戳来保证冲突情况下按照顺序访问数据项 多版本和快照隔离快照隔离中,我们可以想象每个事务开始时尤其自身的数据库版本或快照,它从这个私有的版本中读取数据,因此它和其他事务的更新隔开。事务的更新只在私有数据库中进行,只有提交时才将信息保存,写入数据库。 并发控制读写锁锁一般被分为两种 共享锁:简称为S锁,又称为读锁 排他锁:简称为X锁,又称为写锁 这两种锁有以下规定 一个事务数据对象加了X锁,就可以对数据A进行读取和更新,加锁期间其他事务不能获得A的锁 一个事务数据对象加了S锁,可以对A进行读取操作,加锁期间其他事务可以对其加S锁,但是不能加X锁 意向锁使用意向锁来支持多粒度的封锁 在行级锁、表级锁的情况下,事务想要对表A加X锁,就要检测其他事务是否对表A和表A的任意一行加了锁,那么就需要对A的每一行都检测,这非常耗时 在X/S锁之外引入了IX、IS,IX和IS都是表锁,用来表示一个事务想在某个表上加X或S锁,有以下规定: 一个事务在获得某个数据行的S锁之前,必须先获得表的IS锁或更强的锁 一个数据在获得某个数据航的X锁之前,必须获得表的IX锁 通过引入意向锁,输入想要对某个表A加锁,只需检测事务是否对表A加了x/Ix/S/IS锁 - X IX S IS X × × × × IX × √ × √ S × × √ √ IS × √ √ √ 任意IX/IS 之间都是相容的,因为它只表示要加锁,并没有真正的加锁 S锁只与S和IS兼容 两阶段封锁协议保证事务可串行化的一个协议是两阶段封锁协议,该协议分两个阶段 增长阶段:事务可以获得锁,但是不能释放锁 缩减阶段:事务可以释放锁,但是不能获得新锁 MySQL 隐式与显示锁定MySQL 的 InnoDB 存储引擎采用两段锁协议,会根据隔离级别在需要的时候自动加锁,并且所有的锁都是在同一时刻被释放,这被称为隐式锁定。 InnoDB 也可以使用特定的语句进行显示锁定: 12SELECT ... LOCK In SHARE MODE;SELECT ... FOR UPDATE; MySQL事务事务的概念、隔离级别、死锁见「数据库系统原理」 事务日志事务日志可以帮助提高事务的效率,使用事务日志,在储存引擎上修改表时数据只需要修改其内存拷贝,再把修改行为记录到硬盘上的持久事务中,而不用每次都将修改的数据本身持久到磁盘 事务日志采用追加方式,因此日志操作是顺序I/O。采用事务日志速度快。事务日志持久后,内存中被修改的数据主键被刷回磁盘,大多数储存引擎都是这么实现的。这被称为预写日志,修改数据两次写磁盘 如果数据的修改已经持久化,而数据本身没被写入磁盘,系统崩溃后储存引擎在重启时自动恢复修改的数据。 MySQL自动提交MySQL默认采用自动提交,不是显示的开始一个事务,每个查询都被当成一个事务提交。 有一些命令会强制提交当前的活动事务,如Alter MySQL可以设置所有的四个隔离级别 1set session transaction isolation level read committed 事务中混用储存引擎MySQL不管理事务,事务由引擎实现,所以在一个中混用引擎是不可靠的 事务回滚对于非事务型表(如MyISAM)无法撤销,数据库会失去一致性 隐式和显示锁定InnoDB采用两阶段锁定协议,事务执行过程中随时可以执行锁定,只有在commit和rollback时释放,并且所有的锁在同一时刻释放,这些属于隐式锁,InnoDB在需要时自动加锁 同样InnoDB支持显示加锁,一般来说显示使用LOCK TABLES语句不但没有必要还会严重影响性能,实际上InnoDB行锁性能更好 MySQL并发控制读写锁详见「数据库系统原理」 锁粒度一种提高资源并发的方式是让锁的对象更有选择性。尽量只锁需要修改的部分数据,而不是所有的资源。任何时候,互相之间不发生冲突,锁的数量越少并发程度越高。 加锁也需要消耗资源,获得锁、检查锁、释放锁都会增大开销。所谓锁策略,就是在所的开销和安全性之间寻求一种平衡。大多数商业数据库没有提供更多的选择,一般是在表上施加行级锁。而MySQL提供可多种选择,每种储存引擎都可以实现自己的锁策略和锁粒度 表锁 表锁是MySQL中最基本的锁策略,并且是开销最小的锁。它锁定整张表,用户对表的写操作都需要获得整个锁 尽管储存引擎管理自己的锁,MySQL本身还是会使用有效的表锁,例如Alter table语句使用表锁 行级锁 行级锁可以最大程度的支持并发处理(最大的锁开销),InnoDB及XtraDB实现了行级锁,行级锁只在引擎层面实现,而服务器层面并不了解锁的情况 MySQL多版本并发控制(MVCC)MVCC,Multi-Version Concurrency Control,多版本并发控制。MVCC 是一种并发控制的方法,一般在数据库管理系统中,实现对数据库的并发访问;在编程语言中实现事务内存。 如果有人从数据库中读数据的同时,有另外的人写入数据,有可能读数据的人会看到『半写』或者不一致的数据。有很多种方法来解决这个问题,叫做并发控制方法。最简单的方法,通过加锁,让所有的读者等待写者工作完成,但是这样效率会很差。MVCC 使用了一种不同的手段,每个连接到数据库的读者,在某个瞬间看到的是数据库的一个快照,写者写操作造成的变化在写操作完成之前(或者数据库事务提交之前)对于其他的读者来说是不可见的。 当一个 MVCC 数据库需要更一个一条数据记录的时候,它不会直接用新数据覆盖旧数据,而是将旧数据标记为过时(obsolete)并在别处增加新版本的数据。这样就会有存储多个版本的数据,但是只有一个是最新的。这种方式允许读者读取在他读之前已经存在的数据,即使这些在读的过程中半路被别人修改、删除了,也对先前正在读的用户没有影响。这种多版本的方式避免了填充删除操作在内存和磁盘存储结构造成的空洞的开销,但是需要系统周期性整理(sweep through)以真实删除老的、过时的数据。对于面向文档的数据库(Document-oriented database,也即半结构化数据库)来说,这种方式允许系统将整个文档写到磁盘的一块连续区域上,当需要更新的时候,直接重写一个版本,而不是对文档的某些比特位、分片切除,或者维护一个链式的、非连续的数据库结构。 MVCC 提供了时点(point in time)一致性视图。MVCC 并发控制下的读事务一般使用时间戳或者事务 ID去标记当前读的数据库的状态(版本),读取这个版本的数据。读、写事务相互隔离,不需要加锁。读写并存的时候,写操作会根据目前数据库的状态,创建一个新版本,并发的读则依旧访问旧版本的数据。 MySQL实现了自己的多版本并发控制(MVCC),可以认为MVCC是行级锁的一个变种,它在很多情况下避免了加锁操作,因此开销更低。 MVCC的实现是通过保存数据在某个时间点的快照来实现的。不管需要执行多长时间,每个事务看到的数据都是一致的。根据事务开始的时间不同,事务对同一张表,同一时刻数据可能是不一样的 InnoDB的MVCC是通过在每个记录后面保存两个隐藏的列来实现的。这两个列一个保存了行的创建时间,一个保存了行的过期时间。当然储存的是系统版本号。每开始一个事务,系统版本号都会递增,事务开始时刻的系统版本号作为事务的版本号。 SELECT InnoDB会根据以下两个条件检查记录 A. InnoDB只查找版本早于当前事务版本的数据行,这样可以确保事务读取的行要么在开始前存在,要么自身插入或修改 B. 行的删除版本要么未定义,要么大于当前事务版本号。这样可以确保事务读到的行在事务开始前未被删除 INSERT InnoDB为新插入的每一行保存当前系统的版本号 DELETE InnoDB为每一行保存当前系统的版本号作为删除标识 UPDATE InnoDB插入一行新的记录,保存当前系统的版本号,同时保存当前系统到原来的行作为删除标识 这样通过版本号的方法使得大多数读操作不需要加锁。 MVCC只在可重复读和已提交读两个隔离级别工作,其他的隔离级别与MVCC不兼容 Next-Key LocksNext-Key Locks 是 MySQL 的 InnoDB 存储引擎的一种锁实现。 MVCC 不能解决幻读的问题,Next-Key Locks 就是为了解决这个问题而存在的。在可重复读(REPEATABLE READ)隔离级别下,使用 MVCC + Next-Key Locks 可以解决幻读问题。 Record Locks锁定一个记录上的索引,而不是记录本身。 如果表没有设置索引,InnoDB 会自动在主键上创建隐藏的聚簇索引,因此 Record Locks 依然可以使用。 Gap Locks锁定索引之间的间隙,但是不包含索引本身。例如当一个事务执行以下语句,其它事务就不能在 t.c 中插入 15。 1SELECT c FROM t WHERE c BETWEEN 10 and 20 FOR UPDATE; Next-Key Locks它是 Record Locks 和 Gap Locks 的结合,不仅锁定一个记录上的索引,也锁定索引之间的间隙。例如一个索引包含以下值:10, 11, 13, and 20,那么就需要锁定以下区间: 12345(negative infinity, 10](10, 11](11, 13](13, 20](20, positive infinity) MySQL引擎B+ 树原理1. 数据结构B Tree 指的是 Balance Tree,也就是平衡树。平衡树是一颗查找树,并且所有叶子节点位于同一层。 B+ Tree 是基于 B Tree 和叶子节点顺序访问指针进行实现,它具有 B Tree 的平衡性,并且通过顺序访问指针来提高区间查询的性能。 在 B+ Tree 中,一个节点中的 key 从左到右非递减排列,如果某个指针的左右相邻 key 分别是 keyi 和 keyi+1,且不为 null,则该指针指向节点的所有 key 大于等于 keyi 且小于等于 keyi+1。 2. 操作进行查找操作时,首先在根节点进行二分查找,找到一个 key 所在的指针,然后递归地在指针所指向的节点进行查找。直到查找到叶子节点,然后在叶子节点上进行二分查找,找出 key 所对应的 data。 插入删除操作会破坏平衡树的平衡性,因此在插入删除操作之后,需要对树进行一个分裂、合并、旋转等操作来维护平衡性。 3. 与红黑树的比较红黑树等平衡树也可以用来实现索引,但是文件系统及数据库系统普遍采用 B+ Tree 作为索引结构,主要有以下两个原因: (一)更少的查找次数 平衡树查找操作的时间复杂度和树高 h 相关,O(h)=O(logdN),其中 d 为每个节点的出度。 红黑树的出度为 2,而 B+ Tree 的出度一般都非常大,所以红黑树的树高 h 很明显比 B+ Tree 大非常多,查找的次数也就更多。 (二)利用磁盘预读特性 为了减少磁盘 I/O 操作,磁盘往往不是严格按需读取,而是每次都会预读。预读过程中,磁盘进行顺序读取,顺序读取不需要进行磁盘寻道,并且只需要很短的旋转时间,速度会非常快。 操作系统一般将内存和磁盘分割成固定大小的块,每一块称为一页,内存与磁盘以页为单位交换数据。数据库系统将索引的一个节点的大小设置为页的大小,使得一次 I/O 就能完全载入一个节点。并且可以利用预读特性,相邻的节点也能够被预先载入。 InnoDB和MyISAM的数据分布对比MyISAM 数据分布很简单,数据按照插入数据储存在磁盘上 它很容易创建索引,并且,索引没有什么不同 这是索引1号 Col1 这是索引2号Col2 而InnoDB支持聚簇数据,使用非常不同的方式储存同样的数据。 该图显示整个表,而非只有索引,InnoDB中聚簇索引就是表 InnoDB是 MySQL 默认的事务型存储引擎,只有在需要它不支持的特性时,才考虑使用其它存储引擎。 实现了四个标准的隔离级别,默认级别是可重复读(REPEATABLE READ)。在可重复读隔离级别下,通过多版本并发控制(MVCC)+ 间隙锁(Next-Key Locking)防止幻影读。 主索引是聚簇索引,在索引中保存了数据,从而避免直接读取磁盘,因此对查询性能有很大的提升。 内部做了很多优化,包括从磁盘读取数据时采用的可预测性读、能够加快读操作并且自动创建的自适应哈希索引、能够加速插入操作的插入缓冲区等。 支持真正的在线热备份。其它存储引擎不支持在线热备份,要获取一致性视图需要停止对所有表的写入,而在读写混合场景中,停止写入可能也意味着停止读取。 MyISAM设计简单,数据以紧密格式存储。对于只读数据,或者表比较小、可以容忍修复操作,则依然可以使用它。 提供了大量的特性,包括压缩表、空间数据索引等。 不支持事务。 不支持行级锁,只能对整张表加锁,读取时会对需要读到的所有表加共享锁,写入时则对表加排它锁。但在表有读取操作的同时,也可以往表中插入新的记录,这被称为并发插入(CONCURRENT INSERT)。 可以手工或者自动执行检查和修复操作,但是和事务恢复以及崩溃恢复不同,可能导致一些数据丢失,而且修复操作是非常慢的。 如果指定了 DELAY_KEY_WRITE 选项,在每次修改执行完成时,不会立即将修改的索引数据写入磁盘,而是会写到内存中的键缓冲区,只有在清理键缓冲区或者关闭表的时候才会将对应的索引块写入磁盘。这种方式可以极大的提升写入性能,但是在数据库或者主机崩溃时会造成索引损坏,需要执行修复操作。 事务:InnoDB 是事务型的,可以使用 Commit 和 Rollback 语句。 并发:MyISAM 只支持表级锁,而 InnoDB 还支持行级锁。 外键:InnoDB 支持外键。 备份:InnoDB 支持在线热备份。 崩溃恢复:MyISAM 崩溃后发生损坏的概率比 InnoDB 高很多,而且恢复的速度也更慢。 其它特性:MyISAM 支持压缩表和空间数据索引。 InnoDB 储存引擎InnoDB是MySQL默认的事务型引擎,也是最重要,最广泛使用的储存引擎。它被设计用来处理大量的短期事务,短期事务大部分情况下都是正常提交的,很少被回滚。InnoDB的性能和崩溃自动回复的特性使其在非事务型储存的需求中也很流行。 InnoDB数据储存在表空间中,表空间是InnoDB管理的一个黑盒子,由一系列数据文件组成。 InnoDB通过MVCC来支持高并发,并且实现了四个标准的隔离级别,其默认级别是可重复读,并且通过间隙锁策略防止幻读的出现。间隙锁使得InnoDB不仅仅锁定查询涉及的行,还会对索引的间隙进行锁定。 InnoDB表基于聚簇索引建立。InnoDB和MySQL其他引擎有很大不同,聚簇索引对主键的查询有很高的性能。不过他的二级索引必须包含主键列,所以如果主键很大的话,其他的二级索引就会很大。所以索引较多的话主键应尽可能的小 InnoDB内部有很多优化,从磁盘读数据的可预测性读,自动创建hash索引,插入缓冲器操作。 MyISAM引擎在MySQL5.1之前的版本,MyISAM都是默认的储存引擎,MySQL有几个致命的缺点 不支持事务和行级锁 崩溃后无法安全恢复 但其优点在于对于只读数据,速度较快 MyISAM将表储存在两个文件中,数据文件和索引文件,采用非聚簇索引(详见索引部分) 比较 事务: I 是事务型的,支持提交和回滚 并发:M 只支持表级锁, I 还支持行级锁 外键: I 支持外键 备份: I支持在线热备份 崩溃恢复: M崩溃后损坏的几率较高,I 有完善的日志恢复机制 MySQL索引索引(在MySQL中也称键)是储存引擎用于快速找到记录的一种数据结构。索引对于良好的性能非常关键。 索引优化是对查询性能优化最有效的手段,能够轻易将查询性能提升若干个数量级。 索引基础B-树索引B-树索引是人们经过长期探索发展出目前最适合数据库系统的数据结构,它高效的利用了索引以及机械磁盘的空间。在大多数情况下爱,其不需要全表扫描来获取需要的数据,取而代之的是从根节点进行搜索,依次根据指针向下查找。B树索引有以下特点: 全值匹配 与所有列匹配 匹配最左前缀 索引查找性为“Allen”的人 匹配列前缀 like “J%” 匹配范围值 精确匹配某一列与并范围匹配某一列 只访问索引查询(覆盖索引) B树索引的不足 如果不是从最左列开始,则无法使用查找 不能跳过索引中的列 范围列的右侧无法再使用索引 可以发现,索引列的顺序十分重要 hash索引hash索引基于哈希表实现,只有精确匹配所有列的查询才有效。每一行数据索引都会计算一个hash码 hash查找速度很快,但也有以下限制: hash索引只包含hash值和行指针,而不储存字段的值 hash索引并不是按照索引值储存,无法排序 hash索引不支持部分索引的匹配查找,只支持等值查找 值得注意的是,InnoDB有一个特殊的功能叫自适应哈希索引,当InnoDB某些值使用非常频繁时,他会在内存中基于B树再创建一个hash索引 索引的优点 索引大大减小了服务器需要扫描的数据量 索引可以帮助服务器避免排序和临时表 索引可以将随机IO变为顺序IO 聚簇索引聚簇索引并不是一种单独的索引类型,而是一种数据储存方式。当表有聚簇索引时,表示数据行和相邻的键值紧凑的储存在一起。一个表只能有一个聚簇索引。 InnoDB默认通过主键来聚集数据 如果没有定义主键、InnoDB会选择一个唯一的非空索引,如果没有这样的索引,InnoDB会隐式的定义一个主键作为聚簇索引的主键 聚簇索引的优点 可以把相关数据保存在一起,例如根据用户ID来聚集数据,这样可以最小化磁盘读取数据页 数据访问更快,举措索引将索引和数据保存在统一个B树中,因此聚簇比非聚簇索引的查找更快 覆盖索引世界使用主键值 聚簇索引的缺点 聚簇索引最大限度提高IO密集应用的性能,但是数据如果都在内存中(例如缓存)访问顺序就不重要了 插入速度依赖于插入顺序 更新聚簇索引列的代价很高,强制每个更新移动到位 页分裂问题 聚簇索引可能导致全表扫描变慢(尤其是稀疏表时) 二级索引可能比想象的大,因为二级索引叶子节点包含了主键列 覆盖索引索引是一种查找数据的方式,但是MySQL也可以使用索引来直接获取列的数据,这样就不需要读取数据的行。 如果一个索引包含(或者说覆盖)所有要查询字段的值,我们就称之为“覆盖索引。!!覆盖索引从辅助索引中即可得到查询数据,简单的说就是辅助索引就包含了所要查询的值!! 例如创建某个辅助索引(name、age)查询数据时,select username,age from user where username=’Java’ and age = 22 要查询的列叶子节点都存在,不需要回表索引若干问 索引若干问使用索引的原因?通过创建唯一性索引,可以保证数据库表中每一行数据的唯一性; 大大加快数据的检索速度(这是创建索引最重要的原因); 帮助服务器避免排序和临时表,将随机IO变为顺序IO; 加速表与表之间的连接,在实现数据参考完整性方面有重要意义; 为什么不对每一列创建索引?当数据增加、删除和修改时,索引也需要动态维护,这样就降低了数据的维护速度 除了数据表占数据空间之外,索引还要需要占据一定的物理空间,如果建立聚簇索引,空间占用更大 创建、维护索引耗费时间、这种耗费随数据量增加而增加 索引提高查询速度的原因?将无序的数据变成相对有序的数据(像查目录一样) 常用索引使用的数据结构? 哈希索引 对于哈希索引来说,底层函数就是一个hash表,因此在大多数需求为单条记录的查询时,可以选择hash索引,查询性能较快 BTree索引 Mysql使用的是B+树索引,但是对于两种引擎的实现不同 MyISAM和InnoDB实现B+树索引的区别MyISAM: B+树叶节点data域存放的是数据记录的地址,在索引检索的时候,首先按照B+树 算法搜索索引,如果指定的Key存在,则取出其data域的值,然后以data域的值为地址读取相应的数据记录,被称为”非聚簇索引” InnoDB: 数据文件本身就是索引文件,相比于MyISAM 索引文件和数据文件是分离的,其表结构本身就是按B+Tree组织的一个索引结构,树的叶节点data域保存了完整的整条数据记录,这个索引的key正是数据表的主键,因此InnoDB本身就是主索引,这被称为“聚簇索引(聚集索引)”,而其余的索引被作为辅助索引,辅助索引data域储存记录主键的值而不是地址。这样根据辅助索引查找时,先取出主键的值,再走一遍主索引。因此设计表时主键的选择十分重要,主键不宜过长也不宜非单调。 索引的注意事项 在经常需要索引的列上加快搜索速度 在经常使用where子句上索引加快条件判断速度 在经常需要排序的列上创建索引,因为索引已经排序,这样加快排序的查询时间 中、大型表的排序都是有效的,但是特大型表不适合建索引 在经常用到的连接上建索引,这些外键索引加快速度 避免where子句对字段加函数,这样无法命中索引 在InnoDB中使用与业务无关的自增主键作为主键,使用逻辑主键,不使用业务主键 索引列 NOT NULL,否者引擎将放弃索引 删除长期不使用索引、不使用索引的存在会造成比必要的性能损耗 索引原则(高性能Mysql) 单行访问很慢,特别是机械硬盘。如果从数据中读取的一个数据库只获取一行,浪费很多工作,最好读取尽可能多的行。 顺序访问范围数据是很快的。第一,顺序I/O不需要多次的磁盘寻道,比随机I/O快的多。第二、服务器按顺序读取数据,不需要额外的排序操作,group by 也无需再次排序 索引的覆盖查询时很快的,如果一个索引包含了查询所有的列,那么储存引擎就不需要再回表查询行,这避免了大量的单行访问 MYSQL优化为什么要优化 系统的吞吐量瓶颈往往处在数据库的访问速度上 随着引用程序的运行,数据库中的数据会越来越多 数据放在磁盘上,读取速度无法和内存比 如何优化 设计数据库:数据库表、字段设计、储存引擎 利用好MySQL自身提供的功能,如索引 横向扩展:MySQL集群、负载均衡、读写分离 SQL语句的优化 字段设计原则 尽量使用整形表示字符串 尽可能选择小的数据类型和指定短的长度 尽可能使用 not null 字段注释完整、见名知意 单表字段不要过多 范式第一范式:字段原子性如果数据库表中的所有字段值都是不可分解的原子值,就说明该数据库表满足了第一范式 关系型数据库,默认满足第一范式 第二范式:消除对主键的部分依赖满足第一范式,并且每一个非主属性完全函数依赖于码。也就是说要确保每一列都与主键相关,而不能只与主键部分相关(主要针对联合主键) 依赖:A字段可以确定B字段,则B字段依赖A字段。比如知道了教室号 和 上课时间,就能确定任课老师是谁。于是教室号和上课时间和就能构成复合主键,能够确定去哪个教室上课,任课老师是谁等。但我们常常增加一个id作为主键,而消除对主键的部分依赖。 对主键的部分依赖:某个字段依赖复合主键中的一部分。 解决方案:新增一个独立字段作为主键。 第三范式: 消除对主键的传递依赖并且消除传递依赖。数据库表中的每一列都要和主键直接相关,而不是间接相关 id weekday course_class course_id 1001 周一 教育大楼1521 3546 course_id course_name course_teacher 3546 Java 张三 水平分割和垂直分割水平分割: 通过建立结构相同的几张表分别储存数据 水平切分又称为 Sharding,它是将同一个表中的记录拆分到多个结构相同的表中。 当一个表的数据不断增多时,Sharding 是必然的选择,它可以将数据分布到集群的不同节点上,从而缓存单个数据库的压力。 垂直分割:将经常使用的字段放在一个单独的表中,分割后表记录之间是意义对应的关系 垂直切分是将一张表按列切分成多个表,通常是按照列的关系密集程度进行切分,也可以利用垂直切分将经常被使用的列和不经常被使用的列切分到不同的表中。 在数据库的层面使用垂直切分将按数据库中表的密集程度部署到不同的库中,例如将原来的电商数据库垂直切分成商品数据库、用户数据库等。 复制从主复制主要涉及三个线程:binlog 线程、I/O 线程和 SQL 线程。 binlog 线程 :负责将主服务器上的数据更改写入二进制日志(Binary log)中。 I/O 线程 :负责从主服务器上读取二进制日志,并写入从服务器的重放日志(Replay log)中。 SQL 线程 :负责读取重放日志并重放其中的 SQL 语句。 读写分离主服务器处理写操作以及实时性要求比较高的读操作,而从服务器处理读操作。 读写分离能提高性能的原因在于: 主从服务器负责各自的读和写,极大程度缓解了锁的争用; 从服务器可以使用 MyISAM,提升查询性能以及节约系统开销; 增加冗余,提高可用性。 读写分离常用代理方式来实现,代理服务器接收应用层传来的读写请求,然后决定转发到哪个服务器。 悲观锁与乐观锁乐观锁对应于生活中乐观的人总是想着事情往好的方向发展,悲观锁对应于生活中悲观的人总是想着事情往坏的方向发展。这两种人各有优缺点,不能不以场景而定说一种人好于另外一种人。 悲观锁总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁(共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程)。传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。Java中synchronized和ReentrantLock等独占锁就是悲观锁思想的实现。 乐观锁总是假设最好的情况,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号机制和CAS算法实现。乐观锁适用于多读的应用类型,这样可以提高吞吐量,像数据库提供的类似于write_condition机制,其实都是提供的乐观锁。在Java中java.util.concurrent.atomic包下面的原子变量类就是使用了乐观锁的一种实现方式CAS实现的。 两种锁的使用场景 从上面对两种锁的介绍,我们知道两种锁各有优缺点,不可认为一种好于另一种,像乐观锁适用于写比较少的情况下(多读场景),即冲突真的很少发生的时候,这样可以省去了锁的开销,加大了系统的整个吞吐量。但如果是多写的情况,一般会经常产生冲突,这就会导致上层应用会不断的进行retry,这样反倒是降低了性能,所以一般多写的场景下用悲观锁就比较合适。 面试常见问题系统原理★★★ ACID 的作用以及实现原理。★★★ 四大隔离级别,以及不可重复读和幻影读的出现原因。★★☆ 封锁的类型以及粒度,两段锁协议,隐式和显示锁定。★★★ 乐观锁与悲观锁。★★★ MVCC 原理,当前读以及快照读,Next-Key Locks 解决幻影读。★★☆ 范式理论。★★★ SQL 与 NoSQL 的比较。 MySQL★★★ B+ Tree 原理,与其它查找树的比较。★★★ MySQL 索引以及优化。★★★ 查询优化。★★★ InnoDB 与 MyISAM 比较。★★☆ 水平切分与垂直切分。★★☆ 主从复制原理、作用、实现。★☆☆ redo、undo、binlog 日志的作用。","categories":[],"tags":[{"name":"后台开发","slug":"后台开发","permalink":"http://yoursite.com/tags/后台开发/"}]},{"title":"[后台开发工程师总结系列] 5.网络IO模型","slug":"后台开发工程师总结系列-5-网络IO模型","date":"2019-03-09T08:56:49.000Z","updated":"2019-03-09T09:07:28.178Z","comments":true,"path":"2019/03/09/后台开发工程师总结系列-5-网络IO模型/","link":"","permalink":"http://yoursite.com/2019/03/09/后台开发工程师总结系列-5-网络IO模型/","excerpt":"网络IO模型IO是计算机体系中的重要部分,IO外设有打印机、键盘、复印机等;储存设备有硬盘、磁盘、U盘等;通信设备有网卡,路由器等。不同的IO设备通信很难统一。 IO有两种操作,同步IO和异步IO,同步IO必须等IO操作完成后控制权才返回给用户进程,而异步IO无需等待IO操作完成,就将控制权返回给用户进程。 当一个IO发生时,它涉及两个系统对象,一个是调用IO的进程,一个是系统内核。一个read操作两个阶段,1等待数据准备 2 数据从内核拷贝到进程。 下面针对网络IO的四种模型分别讲解:阻塞IO、非阻塞IO、多路IO复用、异步IO","text":"网络IO模型IO是计算机体系中的重要部分,IO外设有打印机、键盘、复印机等;储存设备有硬盘、磁盘、U盘等;通信设备有网卡,路由器等。不同的IO设备通信很难统一。 IO有两种操作,同步IO和异步IO,同步IO必须等IO操作完成后控制权才返回给用户进程,而异步IO无需等待IO操作完成,就将控制权返回给用户进程。 当一个IO发生时,它涉及两个系统对象,一个是调用IO的进程,一个是系统内核。一个read操作两个阶段,1等待数据准备 2 数据从内核拷贝到进程。 下面针对网络IO的四种模型分别讲解:阻塞IO、非阻塞IO、多路IO复用、异步IO 阻塞IO在linux中, 默认情况下所有的socket都是阻塞的,典型的流程如下: 阻塞和非阻塞的概念是描述用户调用内核IO的方式:阻塞是指IO操作彻底完成后才返回用户空间;而非阻塞IO是IO操作调用后立即给用户一个返回值,不需要IO操作彻底完成。 当进程调用了 recvfrom 这个系统调用后,系统内核就开始了IO的第一阶段:准备数据。对于网络IO来说,很多时候数据还没到达(还没收到完整的TCP包)系统等待足够的数据到来。而用户这边整个进程会被阻塞。当系统等待数据准备好了,他就会从系统内核拷贝到用户内存中,然后才返回结果,用户进程接触阻塞状态。阻塞IO的特点是IO的两个阶段(准备数据和拷贝数据)都阻塞。 阻塞IO只适用于小规模的相应,其相应改进例如多进程、多线程或线程池都难以完成大规模响应的任务。 非阻塞IOLinux下可以设置使得socket变为非阻塞状态。一个非阻塞的socket流程如图 当用户发出read操作时, 如果内核中的数据还没准备好,它不会block用户进程,而是立即返回一个错误。从用户进程的角度来讲,它发起一个read操作后不需要等待,而是马上得到一个错误。当用户进程得到错误后,它就知道数据还没准备好,它就可以再次发起read操作,一旦内核中的数据准备好了,并又收到了用户的系统调用,它就可以将数据复制到用户内存中,然后返回正确的返回值。 所以在非阻塞IO中,用户需要不断的询问kernel数据是否准备好。非阻塞的接口相对于阻塞IO显著差异在于调用后立即返回。在非阻塞状态下recv接口调用后立即返回,返回值有不同的含义: recv() 返回值大于0, 表示接受数据完毕,返回值即是字节数 recv() 返回0, 表示连接正常断开 recv() 返回-1, 且errono 等于EAGAIN, 表示操作尚未完成 recv() 返回-1, 且errono 不等于EAGAIN, 表示recv操作遇到系统错误 可以看到服务器可以循环调用recv 接口,在单个线程内实现对所有连接数据的接收工作。但是上述模型并不推荐,因为循环盗用recv将大幅占用CPU使用率, 而且recv更多的检测“操作是否完成”的工作,实际操作系统中提供更为完善的接口,例如select() 多路复用模式。 多路IO复用多路IO复用,也被称为事件驱动IO, 它的基本原理是有个函数(比如select)会不断的轮询所有的socket,当有某个socket的数据到达了,就通知用户进程,其流程如图所示 当用户进程掉用了select, 那么整个进程会被阻塞,同时内核会监视所有的select负责的socket, 任何一个准备好了其就会返回, 用户进程便可以再次调用 read 从内核拷贝数据 到用户进程。 这个模型和阻塞IO没有太大的不同,事实上还更差一些。涉及到两个系统调用(select 和 recvfrom), 但是select解决 了阻塞IO连接数的问题。 多路IO复用中,每个socket一般都设置为非阻塞的,但是真个用户进程都是阻塞的,不过是被select阻塞,而不是被socketIO阻塞,因此selcet效果与非阻塞IO类似。 异步IO模型当用户发起read操作后,立刻就可以去做其他事;另一方名从内核角度,当他收到read后会理解返回,它不会对用户进程产生任何阻塞。然后内核会等待数据准备完成,之后将数据拷贝到用户内存中,当着一切都完成后,内核给用户进程发一个信号返回read操作完成的信息。 调用阻塞IO会一直阻塞对应的进程直到操作完成,而阻塞IO在内核准备数据的情况下会立即返回。两者的区别在于同步IO进程IO操作时会阻塞进程。按照这个定义,之前的阻塞IO、非阻塞IO、多路IO复用都属于同步IO,实际上,真生的IO操作,即 recvfrom 系统调用,在数据没准备好时不阻塞进程,而数据准备好拷贝进程时被阻塞。 但是异步IO不一样,进程发起IO后直接返回,知道内核发送信号,整个过程IO不阻塞。 经过上述介绍,发现阻塞IO和异步IO复用区别很明显,非阻塞IO大部分时间都不阻塞,但是它仍然要求进程去主动检查,并且数据准备完成后需要主动调用 recvfrom , 而异步IO完全不同,它将IO交给了内核,并被通知,在此期间不需要主动的拷贝数据。 常用函数selectselect函数在socket编程中非常重要,select是典型的多路IO复用的原型程序,几乎所有的平台都提供。select原型是 : 1int select(int maxfpd, fd_set *readfds, fd_set *writefds, fd_set *errorfds, struct timeval *timeout); 这里用到了两个结构体,fd_set 和timeval, 结构体fd_set 可以理解为一个集合,这个集合中存放的是文件描述符(即文件句柄)这可以被认为是普通的文件。所以一个socket就是一个文件。结构体timeval 是一个常用代表时间的数据结构,秒和毫秒数。 下面讲解select各个参数的含义, maxfdp 是一个整数值,是指所有文件描述符的范围,即所有文件描述符的最大值加1 readfds 是指向 fd_set 结构的指针,这个集合中应该包括文件描述符。若集合中有文件可读, select就会返回大于0的值;如果没有可读文件,则根据tiemout 再判断超时,若超出timeout 时间,返回0 writefds, errorfds 同 readfds, 分别用了写和监视文件错误异常。 timeout 是select的超时时间,这个参数至关重要,若传入NULL, select就会一直阻塞, 一直等到监视文件描述符某个文件描述符变化为止;若将其设为0,就变成一个存粹的非阻塞函数,不管文件描述符是否变化都返回立即执行;最后一种就是设置一个整数,这样其在timeout时间内 等待事件到来,超时过后一定返回。 poll和select函数一样,poll函数也用于执行多路IO复用, 12345678#include <poll.h>int poll(struct pollfd* fds, unisgned int nfds, int timeout);struct pollfd{ int fd; //文件描述符 short events; //等待的事件 short revents; // 实际发生的事件} 每一个pollfd 结构体指定一个被监视的文件描述符,可以传递给多个结构体,指示poll监视多个文件描述符。每个结构体的events域是监视事件掩码,由用户来设置。poll() 不需要显示的请求异常情况报告。 Poll函数利用这些事件代码代替了select的读、写、错误事件 poll() 函数的timeout机制同 select函数基本一样 epollepoll是在linux2.6内核中提出的, 是select和poll的增强版本。相对于select和poll来说,epoll更加的灵活,没有描述符的限制。epoll使用一个文件描述符管理多个描述符,将用户的福安息文件描述符放到内核一个事件表中,这样用户空间和内核空间的数据拷贝只需要一次。 epoll函数接口 1234#include <sys/epoll.h>int epoll_create(int size);int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);int epoll_wait(int epfd, struct epoll_event* events, int maxevents, int timeout); epoll_create 创建一个epoll句柄,size用来告诉内核监听的数目。这个不同于select中的第一个参数。需要注意的是,创建好epoll句柄后,他会占用一个fd值,所以使用完后要关闭 epoll_ctl 事件注册函数,不同于select() 监听事件时告诉内核监听什么样的事件, 而是先注册监听事件的类型。第一个参数是epoll_create()的返回值,第二个参数表示动作。(CTL的增删改)第三个参数是需要监听的参数,第四个参数告诉内核需要监听什么事。events是几个宏 的集合(类似poll) select 、poll、epoll 的区别select、poll、epoll 都是多路IO复用机制,多路IO复用通过一种机制可以监视多个描述符,一旦某个描述符就绪(一般是读或写就绪)就能够通知程序进行相应的读写操作。但select、poll、epoll 本质上都是同步IO,以为他们都需要在读写时间就绪后自己负责读写,即使是阻塞的,而异步IO 无需自己负责读写,异步IO的实现会把负责数据从内核拷贝到用户空间,下面将这几种IO做对比 首先看常见的select和poll,对于网络编程来说,一般认为poll() 比 select() 要高级一些。这主要是由于以下原因。 poll不要求开发者计算最大文件描述符时+1操作 poll应对大数目文件描述符时速度更快,应为select对于内核来说需要检测fd_set中的每一个比特位,比较费时 select可以监控文件描述符的数据是固定的(1024,2048)如果监控数值较大的文件描述符,或是分布稀疏的描述符,效率也会很低。而对于poll() 函数来说可以创建特定大小的数组开保存描述符,不受文件描述符值的限制,poll可以监控的数据量远大于select 对于select来说,fd_set 在select返回后会发生变换,所以下次进入select之前都需要初始化fd_set, 而poll函数间监控将输入输出事件分开,不需要重新初始化 select 函数超时参数在返回时也是未定义的,每次进入都需重新设置 select 优点 select的可移植性好, 某些UNIX系统不支持poll select对于超时精度较好 epoll 的优点 支持一个进程打开最大数目的socket描述符(FD) select最不能忍的一个地方是 一个进程打开的FD是有一定限制的,由于FD_SETSIZE的默认大小是1024/2048 对于支持上万连接的IM服务器来说太少了。这时候可以选择修改内核然后重新编译。不过epoll没有这个限制,它所支持的FD上限是最大可以打开的文件数目,一般远大于2048。举例而言,1G内存空间这个数目一般是10万左右。 IO效率不随FD数目增加而线性下降 传统的select、poll 有一个致命弱点是当有一个很大的socket集合时, 由于网络延迟,只有一部分socket是活跃的,但是 select、poll 每次调用都会扫描全局,导致效率下降。而epoll不会出现这个问题,它只会对活跃的socket 进行操作 – 这是因为epoll是根据每个fd上的回调函数实现的,只有活跃的socket才会调用回调函数,epoll实现了一个伪AIO 使用mmap 是加速内核与空间的消息传递 三种方法都需要把fd三种消息传递到用户空间,如何避免不必要的内存拷贝尤为重要,这点上epoll通过内核与用户空间mmap同处一块内存实现。 例题 Linux io模型(select, poll, epoll的区别,水平触发和边缘触发的区别) 水平触发:如果文件描述符已经就绪可以非阻塞的执行IO操作了,此时会触发通知.允许在任意时刻重复检测IO的状态.select,poll就属于水平触发. 边缘触发:如果文件描述符自上次状态改变后有新的IO活动到来,此时会触发通知.在收到一个IO事件通知后要尽可能多的执行IO操作,因为如果在一次通知中没有执行完IO那么就需要等到下一次新的IO活动到来才能获取到就绪的描述符.信号驱动式IO就属于边缘触发. 写过单片机的人可以从另一方理解水平触发和边缘触发的区别: 水平触发:就是只有高电平(1)或低电平(0)时才触发通知,只要在这两种状态就能得到通知.上面提到的只要有数据可读(描述符就绪)那么水平触发的epoll就立即返回. 边缘触发:只有电平发生变化(高电平到低电平,或者低电平到高电平)的时候才触发通知.上面提到即使有数据可读,但是io状态没有变化epoll也不会立即返回. epoll既可以采用水平触发,也可以采用边缘触发. EPOLL事件有两种模型: Edge Triggered (ET) 边缘触发只有数据到来,才触发,不管缓存区中是否还有数据。 Level Triggered (LT) 水平触发只要有数据都会触发。 首先介绍一下LT工作模式: LT(level triggered)是缺省的工作方式,并且同时支持block和no-block socket.在这种做法中,内核告诉你一个文件描述符是否就绪了,然后你可以对这个就绪的fd进行IO操作。如果你不作任何操作,内核还是会继续通知你的,所以,这种模式编程出错误可能性要小一点。传统的select/poll都是这种模型的代表. 优点:当进行socket通信的时候,保证了数据的完整输出,进行IO操作的时候,如果还有数据,就会一直的通知你。 缺点:由于只要还有数据,内核就会不停的从内核空间转到用户空间,所有占用了大量内核资源,试想一下当有大量数据到来的时候,每次读取一个字节,这样就会不停的进行切换。内核资源的浪费严重。效率来讲也是很低的。 ET: ET(edge-triggered)是高速工作方式,只支持no-block socket。在这种模式下,当描述符从未就绪变为就绪时,内核通过epoll告诉你。然后它会假设你知道文件描述符已经就绪,并且不会再为那个文件描述符发送更多的就绪通知。请注意,如果一直不对这个fd作IO操作(从而导致它再次变成未就绪),内核不会发送更多的通知(only once). 优点:每次内核只会通知一次,大大减少了内核资源的浪费,提高效率。 缺点:不能保证数据的完整。不能及时的取出所有的数据。 网络IO模型?什么是多路复用IO?select和epoll的差别?select具体过程? 更多关于epollepoll是Linux内核为了处理大量句柄而做出相对于poll的改进,是linux下IO接口select、poll的增强版本,它能显著的较少程序在大量并发接口中有少量活跃链接情况下系统CPU的利用率。 epoll的优点 支持进程打开大数目的socket描述符 IO数据不随FD数目而线性下降(传统的selcet、poll每次调用都会线性扫描全部结合,导致效率下降) 使用mmap加速内核与用户空间的消息传递,无论是哪种方法都需要内核把FD消息通知给用户空间,如何避免不必要的内存拷贝很重要,epoll使用了内核、用户公用内存实现。 select 和 poll 的缺点 每次调用都要重复读取参数 每次调用重复扫描文件描述符 每次开始时放入等待队列,调用结束后再从队列中删除。","categories":[],"tags":[{"name":"后台开发","slug":"后台开发","permalink":"http://yoursite.com/tags/后台开发/"}]},{"title":"[后台开发工程师总结系列] 4..计算机网络","slug":"后台开发工程师总结系列-4-计算机网络","date":"2019-03-09T08:45:05.000Z","updated":"2019-03-09T08:54:32.459Z","comments":true,"path":"2019/03/09/后台开发工程师总结系列-4-计算机网络/","link":"","permalink":"http://yoursite.com/2019/03/09/后台开发工程师总结系列-4-计算机网络/","excerpt":"计算机网络 复习七层网络模型国际标注化组织ISO于1981年 正式推荐了一个网络体系结构–七层参考模型,也被叫做开放系统互连模型 这七层网络模型在传输过程中还会对数据进行封装,过程如图所示 在ISO七层网络模型中,当一台主机需要传送用户数据时,数据先进入应用层。在应用层中,数据被加上应用层报头(AH),形成应用层协议数据单元(PDU),然后被递交到表示层。表示层不关心上层应用数据格式而是把整个数据包看成一个整体(应用层数据)进行封装,及加上表示层报头(PH)。下层分别加上自己的报头,其中数据链路层还会封装一个链尾,形成一帧数据。 当一帧数据通过物理层传输到目标主机物理层时,主机递交到数据链路层,同样经历上述相反的过程一层一层解包拿到数据。","text":"计算机网络 复习七层网络模型国际标注化组织ISO于1981年 正式推荐了一个网络体系结构–七层参考模型,也被叫做开放系统互连模型 这七层网络模型在传输过程中还会对数据进行封装,过程如图所示 在ISO七层网络模型中,当一台主机需要传送用户数据时,数据先进入应用层。在应用层中,数据被加上应用层报头(AH),形成应用层协议数据单元(PDU),然后被递交到表示层。表示层不关心上层应用数据格式而是把整个数据包看成一个整体(应用层数据)进行封装,及加上表示层报头(PH)。下层分别加上自己的报头,其中数据链路层还会封装一个链尾,形成一帧数据。 当一帧数据通过物理层传输到目标主机物理层时,主机递交到数据链路层,同样经历上述相反的过程一层一层解包拿到数据。 2 五层网络模型大学教科书一般采用了一个五层的网络模型 1 应用层 : 确定进程之间的通信性质以满足用户需求。 2 运输层:负责主机间不同进程的通信。即TCP与UDP 3 网络层:负责分组交换不同主机间的通信 IP数据报 4 数据链路层:负责将网络层的IP数据报组装成帧 5 物理层:透明的传输比特流 3 四层网络模型不论是七层还是五层,都是学术上的概念,实际应用的网络模型是一个四层的TCP/IP模型。 TCP|IP 被组织成四个概念层,其中三个与ISO中模型对应。TCP、IP协议簇不包含链路层和物理层,其需要与其他协议协同工作。 网络接口层 网络接口层包括用于IP数据在已有网络介质上的传输协议。实际上TCP、IP并不定义与ISO链路物理层相对应的功能,相反它定义了ARP这样的协议,提供TCP、IP与硬件间的接口 网间层 网间层对应网络层,本层包含IP、RIP协议,负责数据包装、寻址、路由 传输层 网间层对应OSI参考模型的传输层,它提供两种端到端的通信服务。其中TCP提供可靠的数据流传输服务,UDP提供不可靠的数据报服务。 应用层 应用层对应应用层和表示层。 TCP头部 TCP头部里每一个字段都为管理TCP连接和控制起了重要作用 16位端口号:告知报文段来自哪里,传到哪里。进行TCP通信时,客户端通常使用临时端口号,服务器则使用知名端口号 32位序号:一次TCP通信(TCP连接建立到断开)过程中传输字节流中字节的标号,序号值被初始化为某随机值,之后该传输方向上TCP报文的序号值加上该随机值的偏移 32位确认号:用作对一方发来TCP报文段的相应。其值是TCP报文段值加1 4位头部长度 标识TCP头部有多少(4字节),这样TCP首部最大是60字节 6位标志位包含如下几项 1)URG 紧急指针是否有效 2)ACK 确认号是否有效 3)PSH 提示接收端立即从缓冲区读走数据 4)RST 表示要求重新建立连接 5) SYN 标识请求开始一个连接 (同步报文段) 6)FIN标志 表示通知对方要关闭连接了 (结束报文段) 16位窗口大小 TCP控制流量的手段,接受窗口(告诉对方TCP缓冲区还能容纳的字节数) 16位验校和 检验TCP报文段在传输过程中是否损坏 16位紧急指针 正偏移量 有几点需要注意 TCP包没有IP地址,这属于网络层、一个TCP连接需要4个元组(略)来保证是同一连接。 SequenceNubmer是包序号,来解决网络包乱序问题ACK用于确认不丢包 Window是滑动窗口用来解决流控问题 TCP状态流转首先了解注明的三次握手与四次挥手 TCP建立连接 1) 第一次握手:建立连接时,客户端发送SYN包(SYN=J)到服务器,并且进入SYN_SEND状态等待服务器确认 2) 第二次握手:服务器收到SYN包,确认客户端的SYN(ACK=J+1)同时也发送一个自己的SYN包(SYN=K)即SYN+ACK包,服务器进入SYN_RECV状态 3) 第三次握手:客户端收到SYN+ACK包,向服务器发送确认包ACK(ACK=K+1)发送完毕客户端和服务器进入ESTABLISHED状态 TCP断开连接 TCP有个特殊的概念叫半关闭,这个概念是说TCP连接是全双工连接,因此关闭连接必须关闭两个方向上的连接。客户机给服务器发送一个FIN的TCP报文,然后服务器回一个ACK报文,并且发送一个FIN报文,之后客户端再回ACK就结束了。 在建立连接的时候,通信双方要确认最大报文长度,一般这个SYN是MTU减去IP和TCP的首部长度,对于以太网一般可以达到1460字节,当然非本地IP只有536字节,中间传输的MSS更小的话这个值更小 2MSL 等待状态 (TIMEWAIT)发送了最后一个ACK后,防止最后一次的数据报没有达到对方那里。这个状态保证了双方都可以正常结束,由于socket(IP和端口对)使得应用程序在这个时间(2MSL)无法使用同一个服务。这对于客户端无所谓,但是服务程序(httpd)总要用一个端口来服务。而这个时间,启动httpd会出现错误(插口被使用)。为了避免这个错误,服务器给出了平静时间的概念,虽然可以重新启动服务器,但是要平静的等待2MSL才能进行下一次连接 FIN_WAIT_2 状态 这是注明的半关闭状态,这个状态应用程序还可可以接受数据但是不能发送数据。还有一种可能是 客户端一直 FIN_WAIT_2 ,服务器一直 CLOSE_WAIT 状态 直到应用层来关闭这个状态 RST 同时打开和同时关闭,概率很小 TCP超时重传本节讨论异常网络状况下,TCP如何控制数据保证其承诺的可靠服务 数据顺利到对端,对端顺利响应ACK 数据包中途丢失 数据包顺利达到,但是ACK报文丢失 数据报数据达到,但是对异常未响应 出现这些异常情况时,TCP就会超时重传。TCP每发一个报文段,就对这个报文段设一个计时器,如果确认的时间到了而没有收到确认,就会重传报文段,这被称为超时重传 利用tcpdump调试出以下信息 可以看出重传时间为2、4、8、6(可能是被终止了) 客户端一直没有得到ACK报文,客户端会一直重传,影响重传效率的是RTO。RTO指发送数据后,传送数据等待ACK的时间。RTO非常重要。 设长了,重发慢,没有效率 设短了,重发快,网络拥塞 如果底层传输特性已知,则重传相对简单,但是TCP体层完全异构,因此TCP必须适应时延差异。 RFC973规定了经典的自适应算法 12SRTT = α * SRTT + (1 - α) * RTTRTO = min(UBOUND, max(LBOUND,β * SRTT)) 但是这个算法有个问题,主要是由于ACK传输导致的RTT多义性问题 1987年出现了一种carn算法, 忽略重传,不采样重传的RTT。 该算法规定,一但发生重传,就对现有的RTO翻倍。 当不发生重传时,才根据上式计算平均往返时间RTT和重传时间 TCP滑动窗口TCP滑动窗口主要有两个作用 1. 提供TCP的可靠性 2. 提供TCP的流控特性 同时滑动窗口还体现了TCP面向字节流的设计 对于TCP会话的发送方,任何时候其缓存数据可以分为四类: 已经发送并受到对方的ACK 已经发送但未收到ACK 未发送但是对方允许发送 不允许发送 其中,已经发送还未收到ACK和未发送但是对方允许发送的部分称为发送窗口 当对方接收到ACK后续的确认字节时,便会滑动 对于TCP的接收方,某一时刻其有三种状态 1 已接收 2 未接受准备接受 3 未准备接受 TCP是双工的协议,会话的双方可以同时接受、发送数据。TCP会话双方都各自维护一个发送窗口和接收窗口。滑动窗口实现面向流的可靠性来源于“确认重传机制”,TCP滑动窗口的可靠性也来源于确认重传。发送窗口只有收到对方对于本段ACK的确认,才会移动左边界。前面还有字节未接受的情况下,窗口不会移动。 TCP拥塞控制计算机中带宽、交换节点中的缓存、处理机都是网络资源。某段时间,网络需求超过了可用部分,网络性能就会变坏,这被称为拥塞。拥塞控制就是防止过多的网络流量注入到网络中。TCP拥塞控制由四个核心算法组成:慢开始、拥塞避免、快速重传和快速恢复 慢开始和拥塞避免发送方维持一个拥塞窗口的状态变量,拥塞窗口取决于网络的拥塞程度,发送方让自己的发送窗口等于拥塞窗口 慢开始的思路就是一开始不发送大量的数据,先探测网络的拥塞程度。由小至大的增加拥塞窗口 当主机发送数据时,如果将较大的发送窗口全部注入到网络中,可能引起拥塞 可以试探一下,由小至大增大拥塞窗口的数量 (这里用报文段说明,实际上拥塞窗口以字节为单位) 慢开始从1开始指数增长,为了防止其增长过大, 设置一个门限,当其达到门限时,变为拥塞避免算法 拥塞避免算法是使得拥塞窗口缓慢增长,每经过一个RTT就将 拥塞窗口加一 TCP连接初始化,拥塞窗口设为1 执行慢开始算法,cwind指数增长,直到cwind=ssthress时,开始拥塞避免算法 当网络拥塞时, 将ssthress设为当前的一半,cwind重新设为1 开始 快重传和快恢复快重传要求接收方收到一个失序的报文段后立即发出重复确认(为使发送方尽早知道报文段未传到对方)快重传规定只有一连收到3个重复确认就立即重传对方尚未收到的报文段,而不必继续等待。 快重传还配合有快恢复,主要思想包括 一旦收到三个重复确认,执行乘法减小,ssthresh门限减半,但是并不执行慢开始 将cwind设为ssthresh, 然后执行拥塞避免算法 整体上,TCP拥塞窗口的原则是加法增大、乘法减小。可以看出TCP较好的保证了流之间的公平性,一旦丢包就减半退让。 为什么TCP进行三次握手?本质上TCP协议的三次握手需要解决这样一个问题:在不可靠的信道上(IP数据报尽最大努力交付)完成可靠的数据传输。而全双工的连接建立需要双方的连接请求和确认,这样最少需要使用三次握手才能建立连接 至于为什么三次是最少,客户端服务器二者最少都需要向对方发送一个同步报文(SYN),但是如果只有这两次握手,服务器就只能接受连接,无法确认连接;设想如果服务器接受一个SYN报文就建立连接,那客户端因为阻塞原因重发了N个SYN同步报文 ,服务器每接受到一个就需要建立一次连接,这是不堪设想的。所以只有当服务器接收到客户端第二次的ack确认报文后才会建立连接 为什么需要四次握手TCP是全双工的协议,通信中双方都需要知道对方的存在,而在结束时,双方也同时需要发送断开与确认对方的断开信息。当主机1发送FIN希望断开连接时,主机1已经没有要发送的数据了,但是其还是有可能接受主机2发送的数据,此时单方面的连接断开了,这时处于半连接状态。而只有主机2也向主机1发送断开请求并确认,双方才完全的断开。 TCP的半打开状态如果TCP连接中一方已经关闭或异常终止另一方还不知道,这样的连接称为半打开状态。任何一端的主机都可能检测到这一情况,如果双方没有在半打开的连接上传输数据,双方就无法获悉异常。 半打开的一个常见原因是一方程序的非正常结束(断电、断网)如果A已经没有向B发送的数据,则B永远无法获悉A是否已经消失了。而当一方获取到异常的数据连接后(比如重启)直接进行复位(RST)处理 同时打开与同时关闭两个程序的同时打开与同时关闭是有可能的,例如A:port1 向B:port2 发送SYN同步信息的同时,B:port2 也向A:port1发送了一个SYN同步信息,此时双发收到对方的SYN后各自向对方回一个ack表示,确认,连接就正常建立了,这样一个打开需要四个报文段。 而同时关闭同理,也是双方同时发送FIN报文段,双方在ack确认,这样还是使用4个报文段双方完成了连接的关闭只不过此时双方都跳过了FINWAIT2阶段 大量的Time-wait怎么办Time-wait 状态不能消除 只能快速的回收或重用。 reuse: 处于timewait状态的字段可以被后续连接重用 recycle 开启服务器对于time-wait的快速回收 time-out 修改系统默认的TIMEOUT时间 UDPUDP简介UDP只做传输协议能做的最少工作,只在IP数据服务上增加了两个最基本的服务:复用和分用 以及差错检测 UDP首部只有8个字节,分为四个字段:源端口、目的端口、UDP长度、UDP校验和 UDP无需建立连接 UDP不维护连接状态 UDP分组首部开销小 UDP没有拥塞控制,适合容许一些数据丢失但是不允许有较大时延的应用 TCP和UDP的区别TCP和UDP协议特性的区别,主要从连接性、可靠性、有序性、拥塞控制、传输速度、头部大小来讲 TCP面向连接,UDP无连接。TCP3次握手建立连接,UDP发送前不需要建立连接 TCP可靠,UDP不可靠,TCP丢包有确认重传机制,UDP不会 TCP有序,会对报文进行重排;而UDP无序,后发送信息可能先到达 TCP必须进行数据验校,UDP的校验可选 TCP有流量控制(滑动窗口)和拥塞控制,UDP没有 TCP传输慢,UDP传输快,因为TCP要建立连接、保证可靠有序,还有流量、拥塞控制 TCP包头较大(20字节)UDP较小(8字节) 基于TCP协议:HTTP/HTTPS、Telnet、FTP、SMTP 基于UDP的协议 DHCP、DNS、SNMP、TFTP、BOOTP TCP 网络编程API网络通信概要网络进程如何通信?首要解决如何标识一个进程,本地可以用PID来解决,而网络中用IP+端口号来唯一标识一个进程。这样利用(IP、端口号、PID)可以唯一标识一个网络中的进程。 socket起源于UNINX,UNINX哲学之一就是一切皆文件,有一个打开、读写、关闭的模式来操作。socket就是该模式的一个实现,socket就是一种特殊的文件。 使用TCP/IP的协议应用程序采用应用编程接口 UNINX BSD关键字来实现网络进程通信,目前几乎所有的应用程序都是使用socket,网络通信无处不在。其基本模式包括: 服务器根据地址类型(IPV4、IPV6)创建socket 服务器为socket绑定IP地址和端口号 服务器socket监听端口号请求,随时准备接受客户端的连接,这时候服务器socket并未打开 客户端创建socket 客户端打开socket,根据服务器IP和端口号试图连接服务器socket 服务器socket接收到客户端socket请求,被动打开开始接受客户端请求,直到客户端返回连接信息。这时候socket进入阻塞状态 客户端连接成功,向服务器发送连接状态信息 服务器accept方法返回,连接成功。 服务器向socket写入信息 服务器读取信息 客户端关闭 服务器关闭 仔细研究会发现,服务器和客户端连接的部分就是三次握手。 网络编程API1. socket函数1int socket(int domain, int type, int protocol) socket 函数对应于普通文件的打开操作,普通文件返回一个描述字,而socket创建一个socket描述符,它唯一标识一个socket。 当调用socket时, 返回的描述他存在与协议族空间中,但是没有具体地址,如果想要赋予地址需要使用bind()函数。如果不绑定,系统会在使用时随机生成一个。 2. bind函数bind() 函数把一个地址族中特定的地址赋给socket 1int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen) 3. listen 和 connect 函数这两个函数相对应,服务器调用listen函数来监听端口,而客户端调用connect函数去发出连接请求 12int listen(int sockfd, int backlog)int connect(int sockfd, const struct sockaddr *addr, docklen_t addrlen) 4. accept 函数TCP 服务器依次调用socket()、bind()、listen()之后,就会监指定的socket地址了。而TCP客户端调用socket()、connect()之后就会向TCP服务器发送一个连接请求。TCP连接接收到这个请求之后,就会调用accept()函数接受请求,这样连接就建立了。 1int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen) 注意:accept第一个参数为socket描述字,服务器调用socket生成的,称为监听socket关键字;而accept返回的是已连接socket关键字。一个服务器通常只创建一个监听socket描述字,这个描述子在服务器生命周期一直存在。内核为每个服务器接受的客户创建一个已连接socket关键字,当完成服务后就会关闭。 5. read 和 write 函数read()函数从fd中读取内容,write写入内容。成功时返回字节数 6. close函数1int close(int fd); 实例123456789101112131415161718192021222324252627282930313233343536373839404142434445464748#include <stdio.h>#include <stdlib.h>#include <string.h>#include <errno.h>#include <sys/types.h>#include <sys/socket.h>#include <netinet/in.h>#include <unistd.h>#define MAXLINE 4096int main(int argc, char** argv){ int listenfd, connfd; struct sockaddr_in servaddr; char buff[4096]; int n; if((listenfd = socket(AF_INET, SOCK_STREAM, 0)) == -1) { printf(\"create socket error: %s(errno: %d)\\n\", strerror(errno),errno); return 0; } memset(&servaddr, 0, sizeof(servaddr)); servaddr.sin_family = AF_INET; servaddr.sin_addr.s_addr = htonl(INADDR_ANY); servaddr.sin_port = htons(6666); if(bind(listenfd, (struct sockaddr*)&servaddr, sizeof(servaddr)) == -1){ printf(\"bind socket error: %s(errno: %d)\\n\", strerror(errno),errno); return 0; }; if(listen(listenfd, 10)==-1){ printf(\"listen error: %s(errno: %d)\\n\", strerror(errno),errno); return 0; } printf(\"=====waiting for client's request=====\\n\"); while(1){ if(connfd = accept(listenfd, (struct sockaddr*)NULL, NULL) == -1){ printf(\"accept socket error: %s(errno: %d)\\n\", strerror(errno),errno); continue; } n = recv(connfd, buff, MAXLINE, 0); buff[n] = '\\0'; printf(\"recv msg from client: %s\\n\", buff); close(connfd); }} 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950#include <stdio.h>#include <stdlib.h>#include <string.h>#include <errno.h>#include <sys/types.h>#include <sys/socket.h>#include <netinet/in.h>#include <arpa/inet.h>#include <unistd.h>#define MAXLINE 4096int main(int argc, char** argv){ int sockfd, n; struct sockaddr_in servaddr; char recvline[4096], sendline[4096]; if(argc != 2){ printf(\"usage: ./client <ip address>\\n\"); return 0; } if((sockfd = socket(AF_INET, SOCK_STREAM, 0)) < 0) { printf(\"create socket error: %s(errno: %d)\\n\", strerror(errno),errno); return 0; } memset(&servaddr, 0, sizeof(servaddr)); servaddr.sin_family = AF_INET; servaddr.sin_port = htons(6666); if(inet_pton(AF_INET, argv[1], &servaddr.sin_addr) <= 0){ printf(\"inet_pton error for %s\\n\", argv[1]); return 0; } if(connect(sockfd, (struct sockaddr*)&servaddr, sizeof(servaddr)) < 0){ printf(\"connect error: %s(errno: %d)\\n\", strerror(errno),errno); return 0; } printf(\"send msg to server: \\n\"); fgets(sendline, 4096, stdin); if(send(sockfd, sendline, strlen(sendline),0) < 0){ printf(\"send msg error: %s(errno: %d)\\n\", strerror(errno),errno); return 0; } close(sockfd); return 0;} TCP 协议选项1. SO_REUSEADDR一般来说一个端口释放后,大约两分钟才能再次使用(处于TIME_WAIT状态),而使用该选项可以使端口被释放后立即被使用(处于TIME_WAIT状态的端口)。 2. TCP_NODELAY/TCP_CHORK网络拥塞领域,有一个非常著名的算法叫做Nagle算法, 这是以其发明人的名字命名的。John Nagle 首次用该算法解决福特公司的网络拥塞问题。该问题具体描述是:如果应用程序每次产生1Byte的数据,而这个1Byte的数据又以数据报的形式发送给网络服务器,那么很容易使得网络过载。所以传送1Byte的包却要花费40Byte的包头(IP20字节、TCP20字节)这种有效载荷利用低下的情况被称为愚蠢窗后症候群。 针对这问题,Nagle算法改进:如果发送少量字符包(小于MSS的包被称为小包,大于MSS的包被称为大包)发送端只会发送第一个小包,将后面的小包缓存起来。直到接收到前一个数据报的ACK为止,或当前字符较紧急,积攒了较多的数据。 TCP中Nagle算法默认启用,但是不使用任何情况。而TCP_NODELAY/TCP_CHORK字段控制了包的Nagle化。例如TCP_NODELAY便是直接把包发出去,这样用户体验会更好。 3. SO_LINGERlinger是延缓的意思,这里的延缓指close操作。默认close立即返回,但是当缓冲区还有部分数据时,系统会尝试将数据发送给对方。 4. TCP_DEAFER_ACCPET实际上是接收到第一个数据包后,才会创建连接. 常被用来防御空连接攻击。 5.SO_KEEPALIVESO_KEEPALIVE用来检测对方主机是否崩溃,避免服务器永远阻塞于TCP连接的输入 设置该选项后,如果2h内任何一方没有数据交换,TCP就会自动向对方发送一个保持存活探测, 对方接受正常,以ACK回应 对方已崩溃且重新启动,以RST响应,套接口错误置为restart,套接口本身被关闭 对方无响应,再次发送8个探测分节,11min15s 后无响应就放弃。 网络字节序与主机序关于字节序再次讨论。不同的CPU有不同的字节序类型,这些字节序是整数在内存中的保存顺序,称为主机序。最常见的两种 1 小端, 低序的字节存在起始位置 2 大端 , 高字节的存在起始位置 小端法 地址低位存在值的低位,高位存值的高位。这种方式符合人的思维方式。 大端法 地址低位存高位的值。它很直观不需要考虑对应关系,只需要内存地址写出去即可 为什么要注意字节问题呢?C++编译平台的储存是由编译平台的CPU确定的,而JAVA编写的程序唯一采用大端法。所有网络协议都是大端法。其也被称为网络字节序。 封包和解包TCP是一个流协议,所谓流就行没有界限的一串数据。但是通信程序是需要独立的数据包发送。 设想这样几种情况 先接收到data1 后接到 data2 先接收到data1 的部分数据,后接到data1的余下部分与 data2全部 先接收到了data1全部数据和部分data2数据,后接受到data余下数据 一次接受data1和data2全部数据 1是理想情况 而2、3、4即使粘包的情况,这时就需要拆包将受到的数据拆成独立的数据包。这种情况可能有这么几种原因。 1 由Nagle算法造成的粘包 2 接收端不及时接受造成的粘包 具体的解决办法便是封包和拆包。封包是给一段数据加上包头,这样数据就分为包头、包体两部分。包头包含了一个结构体成员变量表示了包体的长度,这样另一方便可以通过这个长度进行拆包; HTTP 复习协议是计算机通信网络中两台计算机之间进行通信所必须共同遵守的规定或规则。HTTP协议是一种详细规定了浏览器和万维网服务器之间相互通信的规则,通过因特网传送万维网文档和数据的协议。HTTP协议可以使浏览器更加高效的运行,使网络的传输效率更高。它不仅保证计算机快速正确的传输超文本文档,还确定哪一部分先显示。 HTTP由于其灵活简单快速的特点,应用非常广泛。浏览器网页是HTTP主要的应用,但其不只是用于网页浏览。只要通信的双方都使用他就可以通信。比如QQ软件就用到了HTTP协议。 HTTP工作流程在网路的七层模型中,HTTP在应用层也就是传输层以上,其基于TCP协议。 HTTP的默认端口号为80, 而HTTPS的默认端口号是443 HTTP是基于传输层的TCP协议,而TCP是一个端到端面向连接的协议。所谓的端到端可以理解为进程到进程的通信,所以HTTP在开始传输之前,首先要建立TCP连接,而TCP连接的过程需要三次握手。在TCP的三次握手后,建立了TCP连接。此时HTTP就可以进行传输了。一个HTTP操作称为一个事务,可以分为以下几步: 首先客户机与服务器需要建立连接。只要单机某个超级链接,HTTP工作即开始 建立连接后,客户机发送一个请求给服务器,请求的格式为:统一资源标识符(URL),协议版本号,后面是MIME信息,包括(请求修饰符、客户机信息和可能的内容) 服务器接到请求后,给与一个相应的响应信息,其格式为一个状态行、包括信息的协议版本号、一个成功或错误的状态码、后面是MIME信息(服务器信息、实体信息、可能的内容) 客户端接受服务器返回的信息通过浏览器返回在用户显示屏上,然后客户机与服务器断开连接。 如果上述几步中某一步出现错误,那么产生的错误信息返回客户端,由显示屏输出。HTTP协议永远都是客户端发起请求,服务器响应。 HTTP 协议结构HTTP 协议无论是请求报文还是回应报文,都包含以下四个部分。 报文头: GET http://www.baidu.com HTTP/1.1 只占一行 请求头 Accept-Language: en 空行 可选的消息体 HTTP协议是基于行的协议,每一行以\\r\\n 为分隔符。报文头通常表明报文的类型,且报文头只占一行;请求头附带一些特殊信息,每一个请求头占一行 name:value HTTP请求方法HTTP/1.1 协议中定了 9 种方法来表明Request-URL 指定资源的不同的操作方式 OPTIONS 返回服务器针对特定资源的HTTP请求方法 HEAD 向服务器索要与GET请求一致的相应,而响应体不会被返回 GET 向特定资源发出请求(GET可能会被爬虫随意访问) POST 向指定资源提交数据处理的请求(提交表单、上传文件)数据被包含在请求体中。POST可能会导致新建资源或已有资源的修改 PUT 向指定资源上传最新内容 DELETE 请求服务器删除 REQUEST_URL 所标识的资源 TRACE 回显服务器收到的请求 CONNECT 预留给连接方式改为管道的代理服务器 PATCH 局部修改某一资源 当请求的资源不支持请求方法时, 服务器返回405(Method Not Allowed)服务器不认识或不支持方法时, 返回501(Not Implemented) 常见的请求头Host:(发送请求,请求头是必须的)主要用于执行被请求资源的Internet主机和端口号,它从HTTP的URL中提取出来, Connection: 它的值通常有两个,keep-alive 和close。 HTTP 是一个请求响应模式的典型范例,即客户端向服务器发送一个请求信息,服务器来响应这个信息。在之前的HTTP版本中,每个请求被创建一个新的到服务器端的连接,在这个连接上发送请求,然后接受请求。keep-alive被用来解决效率了低的问题。keep-alive 使客户端到服务端的连接持续有效,当出现了后续请求时, keep-alive避免了建立或者重新建立。对于市面上大部分服务器keep-alive都被支持,但是对于负担较重的网站,保留的客户端会影响性能。 Accept:浏览器可以接受的MIME类型。 Accept:text/html, 如果不能接受将返回406错误 Cache-control: 指定请求和响应遵循缓存机制。缓存指令是单向的,且独立。 Accept-Encoding:浏览器生命可接受的编码方法,通常说明是否支持压缩。 Accept-Language:浏览器声明接受语言 Accept-Charset:浏览器可以接受的字符集,默认任何字符集都可以接收 User-agent:用于告诉HTTP服务器,客户端使用操作系统浏览器的名称和版本。 HTTP回应报文第一行是报头,第一个字段表明HTTP协议版本们可以直接以请求报文为准;第二个字段是status code, 也就是返回码,相当于请求结果 HTTPS在网络上,两个实体间的通信容易被窃听。 当你学习网络安全后、你发现网络再也不安全了 初级的防御手段,是双方都约定一个加密的算法,把加密后的数据进行传输,收到数据方再进行解密。这里涉及一个概念,对称加密和非对称加密 对称加密:对称加密是指加密秘钥和解密秘钥是一样的,通常有AES和TEA加密算法,它的特点是计算量小,有又一定的破解门槛。 非对称加密:加密的秘钥和解密的秘钥不一样,秘钥成对出现。加密解密使用不同的秘钥(公钥加密需要私钥解密,私钥加密需要公钥加密)它的特点是计算量大,常用的有RSA、ECC等算法。基于性能考虑,一般使用非对称加密得到秘钥, 再用对称秘钥进行加密。 HTTP协议可以轻松抓包并获取协议,是不安全的协议。而HTTPS是安全为目标的HTTP通道,简单来说是HTTP的安全版。 1.client向server发送请求https://baidu.com,然后连接到server的443端口。 2.服务端必须要有一套数字证书,可以自己制作,也可以向组织申请。区别就是自己颁发的证书需要客户端验证通过,才可以继续访问,而使用受信任的公司申请的证书则不会弹出提示页面,这套证书其实就是一对公钥和私钥。 3.送证书这个证书其实就是公钥,只是包含了很多信息,如证书的颁发机构,过期时间、服务端的公钥,第三方证书认证机构(CA)的签名,服务端的域名信息等内容。 4.客户端解析证书这部分工作是由客户端的TLS来完成的,首先会验证公钥是否有效,比如颁发机构,过期时间等等,如果发现异常,则会弹出一个警告框,提示证书存在问题。如果证书没有问题,那么就生成一个随即值(秘钥)。然后用证书对该随机值进行加密。 5.传送加密信息这部分传送的是用证书加密后的秘钥,目的就是让服务端得到这个秘钥,以后客户端和服务端的通信就可以通过这个随机值来进行加密解密了。 6.服务段加密信息服务端用私钥解密秘密秘钥,得到了客户端传过来的私钥,然后把内容通过该值进行对称加密。 7.传输加密后的信息这部分信息是服务端用私钥加密后的信息,可以在客户端被还原。 8.客户端解密信息 客户端用之前生成的私钥解密服务端传过来的信息,于是获取了解密后的内容。 HTTP和HTTPS有以下区别 HTTPS协议需要CA申请证书,需要交费 HTTP是超文本传输协议,明文传输, HTTPS是ssl加密传输 80 和443 HTTP连接很简单,无状态, HTTPS经过SSL+HTTP协议构建的,加密传输、身份认证。 HTTPS耗性能,安全性要求低用HTTP CGICGI 是HTTP 中重要的技术之一,有着不可替代的作用。CGI是一个web服务器的标注接口。通过CGI接口Web服务器就能获取客户端提交的信息转交给服务器端的CGI程序处理,最后结果返回给客户端。 服务器和客户端之间的通信,是浏览器和服务端web服务器的HTTP通信,所以只需知道浏览器执行哪个CGI程序即可。 GET与POST的区别作用GET用于获取资源,POST用于传输实体主体 参数GET和POST都能使用额外的参数,但是GET参数以查询字符串的形式出现在URL中,如http://127.0.0.1/Test/login.action?name=admin&password=admin 这个过程用户可见。而POST的参数存储在实体主体中。通过HTTP的POST机制。POST参数可以通过一些抓包工具(Fiddler)查看 因为URL只支持ASCLL码,因此GET参数中文会被编码,空格被编码为%20 安全安全的HTTP方法不会改变服务器状态,可以说他是只读的 GET方法安全的,POST不是,POST方法传输主体内容,这个内容可能是某个表单数据,服务器可能把其存入数据库中,这样状态就发生了改变 幂等性幂等性的HTTP方法,同样的请求被执行一次和连续执行多次的效果是一样的,服务器状态也是一样的。 所有安全的方法是幂等的,GET、HEAD、PUT、DELETE方法都是幂等的 而POST方法不是幂等的 XMLHTTPRequestXMLHTTP Request是一个API,它为客户端和服务器之间传输数据的功能。他提供了一个URL来获取数据的简单方式,而且不会使整个页面刷新。这使得网页只更新一部分不会打扰到用户,在AJAX中被大量使用 在xmlhttprequest中POST会先发送header再发送data(当然也和浏览器的做法相关) 而get方法会一起发送 其他Get传输数据量小,因为受URL的限制,但是效率高;POST可以传输大量数据,文件只能通过POST传递 Get方式只支持ASCII字符,向服务器传输的中文字符可能会乱码。POST支持标准字符集,可以正确的传递中文字符 HTTP 首部 Request Header 解释 某度首页示例 Accept 客户端能够接受的内容类型 text/html Accept-Encoding 浏览器可以支持的web服务器返回内容压缩编码类型 gzip Accept-Language 浏览器可以接受的语言 zh-CN Cache-Control 指定请求和相应遵循的缓存机制 max-age=0 Connection 表示是否需要持久连接 keep-alive Cookie HTTP请求发送时,会把保存在请求域名下的所有cookie值一起发送给web服务器 键值对 Host 请求服务器的域名和端口号 www.baidu.com Upgrade-Insecure-Requests 浏览器可以处理HTTPS协议 1 User-Agent 发出请求的用户信息 Mozilla/5.0 HTTP 首部 Response Header 解释 某度首页示例 Cache-Control 告诉所有的缓存机制是否可以缓存及缓存哪种类型 private Connection 是否保持持久连接 keep-alive Content-Encoding 返回内容压缩编码类型 gzip Content-type 返回内容的MIME类型 text/html charset=utf-8 Date 原始服务器消息发出时间 Wed 03 Oct2018 12:04:45 GMT Expires 响应过期的时间 Wed 03 Oct2018 12:04:45 GMT Server Web服务器软件名称 BWS1.1 Set-Cookie 设置浏览器缓存 BDSVRTM=114; path=/ Transfer-Encoding 文件传输编码 chunked HTTP状态码HTTP中状态码大致分为五大类: 100-199 信息性状态码 100 continue: 收到了请求的初始部分,请客户端继续 200-299 成功状态码 200 OK:请求被正常处理 204 No Content: 请求被接受,但是响应报文只有首部和状态行,没有实体部分 206 Partial Content: 客户端只请求了一部分的资源,服务器只针对请求的部分资源进行返回 300-399 重定向状态码 301 Moved Permanently: 永久重定向 302 Found: 临时重定向,资源被临时移动了 303 See Other: 表示用户请求的资源代表着另一个URI,应该使用GET去获取资源 304 Not Modified: 当客户端发送的请求附带条件时,服务器找到资源但未符合请求 307 Temporary Redirect: 同302,临时重定向 400-499 客户端错误状态码 400 Bad Request: 告知客户端它发送了一个错误的请求 401 Unauthorized: 请求需进行认证 403 Forbidden: 请求被服务器拒绝了 404 Not Found:服务器无法找到对应资源 405 Method Not Allowed:请求中带有不支持的方法 500-599 服务器错误状态码 500 Internet Server Error: 服务器内部错误 502 Bad GateWay: 代理或网关服务器从下一条链路收到了伪响应 503 Server Unavailable: 服务器正忙 504 GateWay Timeout: 一个代理网关等待另一服务器时超时了 持续性连接和非持续性连接1. 定义非持续连接:每个请求、相应都经一个而单独的TCP连接发送 持续连接:所有的请求相应通过相同的TCP连接发送 2. 区别如果采用非持续连接,打开包含一个HTML文件和10个内联对象网页,HTTP需要建立11次TCP连接才能把文件从服务器传到客户机。而如果采用持续连接,HTTP建立一次TCP就把文件从服务器传到客户机 每次TCP连接必需建立和断开(通过三次握手建立、四次挥手断开),这都需要占用CPU资源,这样占用客户机和服务器的CPU时间大大减少 每次连接,客户端和服务器都必须分配发送和接收缓存,这意味着影响服务器和客户机的资源,着同样要占用CPU时间 对于大量对象组成的文件,TCP低速启动算法会限制服务机向客户机传送对象的速度。使用HTTP/1.1后,大多数对象以最大速率传输 3. HTTP/1.0 + keep-aliveHTTP/1.1 中默认保持持久连接,但是1.0版本的HTTP需要设置 connection:keep-live connection:keep-live 是HTTP1.0 浏览器和服务器的实验性扩展 Cookie 与 Session由于HTTP是无状态协议,为了保持客户端与服务器的一些关系,便有了cookie和session 1.cookiecookie是服务器在本地机器上存储的小段文本并随每一个请求发送至同一个服务器。网络服务器用HTTP头想客户端发送cookies, 在客户终端,浏览器解析这些cookies 并将它们保存为一个本地文件,它们在会在下一次对服务器的请求时附上这些cookies。 内容过期时间会话cookie:若不设置过期时间,则表示这个cookie的生命周期为浏览器会话期间,若关闭浏览器,cookie就会消失。这种生命周期的cookie被称为会话cookie 持久cookie:若设置了过期时间,浏览器会把cookie存储到硬盘上(当然用户可以选择拒绝),关闭后再打开这些cookie仍然有效 2.sessionsession机制是一种服务端的机制,服务器利用一种类似于散列表的结构来保存信息 当程序需要为某个客户端的请求创建session时,服务器检查这个客户端是否包含了一个session标志,称为session_id, 如果检测到说明该客户曾创建过ID,服务器会把这个ID检索出来使用(或者未检测到新建一个),session_id 既不会重复也不容易被找到仿造。 session_id的储存 保存这个session_id可以采用cookie,这样交互过程中浏览器可以把这个标志返回给服务器。一般该变量名与session有关,如github的session ID即名为user_session 由于cookie可以被认为的禁止,必须有其他机制保证session_id传回服务器,经常使用的一种方法是URL重写,即直接把session_ID附在URL后面。作为路径的附加信息或查询字符 另一种技术是表单隐藏字段,服务器自动修改表单加入一个隐藏字段,便于传回session_id 3. cookie与session的区别存取方式不同cookie只能保存ASCII字符,Unicode及二进制数据需要编码,cookie不能直接存取java对象,存储略微复杂的信息较为艰难。而session中能够存取任何类型的数据,包括不限于string、integer、list、map,而且可以直接存取java的类对象,十分方便 隐私策略不同cookie存储在客户端阅读器中,对客户端可见,客户端可以窥探甚至是修改cookie内容。而session存储在服务器上对用户透明,不存在泄露风险。cookie可以像google及baidu一样将敏感信息加密后保存,在服务器上进行解密。 有效时间不同由于session依赖于session_id的cookie,而session_id过期时间默许为-1,关闭浏览器即消失。而cookie可以设置长期的保存 服务器压力不同由于不同的储存方式,储存在客户点的cookie不会给服务器造成压力,而session由于存在服务器上,对服务器压力较大 浏览器支持不同cookie是需要客户端浏览器支持的,假如客户端禁用了cookie或者不支持cookie,则会话跟踪会失效。 假如客户端不支持cookie,就需要运用session及url地址重写。需要注意的是一切用到session的程序url都需要进行重写,否则session会话还是会失效 跨域支持不同cookie支持跨域名访问,一切以相同后缀的域名均可以访问该cookie,跨域名cookie被广泛应用 session仅在当前域名有效 一个web页面的请求过程1.DHCP配置主机信息(找本机IP) 主机生成一个DHCP请求报文,并将这个报文放入目的端口67和源端口68的UDP报文段中 该报文段放在一个广播IP地址(255.255.255.255)和源IP地址(0.0.0.0)的IP数据报中 该数据报被放在MAC帧中,目的地址FF:FF:FF:FF:FF:FF,广播到交换机连接的所有设备 交换机的DHCP服务器收到广播帧后,不断向上解析得到IP、UDP、DHCP报文,之后生成DHCP的ACK报文,该报文包括 IP地址、DNS服务器IP地址、默认网关路由器的IP地址和子网掩码,再经过层层封装到MAC帧中 该帧的目的地址是主机的mac地址,主机收到该帧后分解得DHCP报文,之后配置IP地址,子网掩码、DNS服务器IP地址,安装默认网关 2.ARP解析网关MAC地址(找网关MAC地址) 主机通过浏览器生成一个TCP套接字,为了发送HTTP请求,需要知道网站对应的IP地址 生成一个DNS查询报文,端口53(DNS服务器) DNS查询报文放入目的地址为DNS服务器IP地址的IP数据报中 IP数据报放入一个以太网帧中,发送至网关路由器 DHCP过程只知道网关的IP地址,为了获取网关的MAC地址,需要用ARP协议 主机生成一个目的地址为网关路由器IP的ARP查询报文,放入一个广播帧中,并发送这个以太网帧,交换机将其发送给所有的连接设备 网关接收到该帧后,分解得到ARP报文,发现IP地址与自己想匹配,发送一个ACK报文回应自己的MAC地址 3.DNS解析域名(找服务器IP) 知道了DNS的MAC地址后就可以继续DNS解析过程 网关接收到DNS查询报文后,抽出IP数据报,并根据该表选择该转发的路由器 路由器根据内部网关协议(RIP、OSPF)和外部网关协议(BGP)配置路由器到DNS的路由表项 之前的DNS报文到DNS服务器后,照常依次抽出报文,在DNS库中查找解析域名 找到DNS记录后发送DNS回答报文,然后将其放入UDP报文段、IP数据报,通过路由器反转发回网关路由器,经过交换机到主机 4. HTTP请求页面 有了HTTP服务器的IP地址后,主机便可以生成TCP套接字,向web服务器发送HTTP get报文 建立HTTP连接前需要进行TCP连接,进行三次握手,过程略 建立连接后发送HTTP的GET报文,交付给HTTP服务器 HTTP服务器从TCP中读出报文,生成HTTP相应报文,将web页面放入HTTP报文主体中发挥主机 浏览器收到HTTP相应报文后抽取WEB页面内容进行渲染,显示web页面 1 DNS解析什么是DNS解析?当用户输入一个网址并按下回车键的时候,浏览器得到了一个域名。而在实际通信过程中,我们需要的是一个IP地址。因此我们需要先把域名转换成相应的IP地址,这个过程称作DNS解析。 1) 浏览器首先搜索浏览器自身缓存的DNS记录。 2) 如果浏览器缓存中没有找到需要的记录或记录已经过期,则搜索hosts文件和操作系统缓存。 3) 如果在hosts文件和操作系统缓存中没有找到需要的记录或记录已经过期,则向域名解析服务器发送解析请求。 4) 如果域名解析服务器也没有该域名的记录,则开始递归+迭代解析。 5) 获取域名对应的IP后,一步步向上返回,直到返回给浏览器。 至此,浏览器就得到了url的IP地址。 2 发起TCP请求建立TCP连接的过程就是三次握手过程。 这里简述一下三次握手的过程: 客户端向服务器端发送连接请求的报文; 服务器端收到请求后,同意建立连接,向客户端发送确认报文; 客户端收到服务器端的确认报文后,再次向服务器端发出报文,确认已收到确认报文。 至此,浏览器与服务器已经建立了TCP连接,开始进行通信。 3 建立TCP连接后,浏览器向服务器发送http请求例如:浏览器发出取文件指令GET 4 负载均衡什么是负载均衡?当一台服务器无法支持大量的用户访问时,将用户分摊到两个或多个服务器上的方法叫负载均衡。 1) 一般,如果我们的平台配备了负载均衡的话,前一步DNS解析获得的IP地址应该是我们Nginx负载均衡服务器的IP地址。所以,我们的浏览器将我们的网页请求发送到了Nginx负载均衡服务器上。 2) Nginx根据我们设定的分配算法和规则,选择一台后端的真实Web服务器,与之建立TCP连接、并转发我们浏览器发出去的网页请求。 3) Web服务器收到请求,产生响应,并将网页发送给Nginx负载均衡服务器。 4) Nginx负载均衡服务器将网页传递给filters链处理,之后发回给我们的浏览器。 5 服务器响应http请求,将请求的指定资源发送给浏览器6 浏览器释放TCP连接建立TCP连接的过程就是四次挥手过程。 这里简述一下四次挥手过程: 1.浏览器向服务器发送释放连接报文; 2.服务器收到释放报文后,发出确认报文,然后将服务器上未传送完的数据发送完; 3.服务器数据传输完成后,向浏览器发送释放连接请求; 4.浏览器收到报文后,发出确认,然后等待一段时间后,释放TCP连接。 7 浏览器渲染1) 浏览器根据页面内容,生成DOM Tree。根据CSS内容,生成CSS Rule Tree(规则树)。调用JS执行引擎执行JS代码。 2) 根据DOM Tree和CSS Rule Tree生成Render Tree(呈现树)3) 根据Render Tree渲染网页","categories":[],"tags":[{"name":"后台开发","slug":"后台开发","permalink":"http://yoursite.com/tags/后台开发/"}]},{"title":"[后台开发工程师总结系列] 3.操作系统之线程","slug":"后台开发工程师总结系列-3-操作系统之线程","date":"2019-03-09T08:40:36.000Z","updated":"2019-03-09T08:43:21.647Z","comments":true,"path":"2019/03/09/后台开发工程师总结系列-3-操作系统之线程/","link":"","permalink":"http://yoursite.com/2019/03/09/后台开发工程师总结系列-3-操作系统之线程/","excerpt":"多线程为了更好的理解线程的概念,先对进程、线程的背景做介绍。 早期的计算机都只允许一个程序独占系统资源,一次只能执行一个程序。 这种背景下,一台计算机支持多个程序并发执行的需求就变得迫切,由此产生了进程的概念。进程在多数早期多任务操作系统中是执行工作的基本单元。进程是包含程序指令和相关资源的集合,每个进程和其他进程一起参与调度,竞争CPU、内存。每次进程的切换都存在进程资源的保护和恢复动作,这称为上下文切换。进程的引入可以解决多用户支持的问题,但也引入了新的问题:进程频繁切换可能严重影响性能。 同一个进程内部可能有多个线程,共享同一个京城的所有资源。通过线程支持了同一应用程序内部的并发,免去了进程频繁切换的开销,另外并发任务也更简单。 网络具有天生的并发性,比如数据库可能同时需要处理数以千计的请求。而由于网络连接的不确定性和不可靠性,等待网络交互时,可以让当前的线程进入睡眠、退出调度,处理其他线程,这样能够充分利用系统资源,发挥系统实时处理能力。","text":"多线程为了更好的理解线程的概念,先对进程、线程的背景做介绍。 早期的计算机都只允许一个程序独占系统资源,一次只能执行一个程序。 这种背景下,一台计算机支持多个程序并发执行的需求就变得迫切,由此产生了进程的概念。进程在多数早期多任务操作系统中是执行工作的基本单元。进程是包含程序指令和相关资源的集合,每个进程和其他进程一起参与调度,竞争CPU、内存。每次进程的切换都存在进程资源的保护和恢复动作,这称为上下文切换。进程的引入可以解决多用户支持的问题,但也引入了新的问题:进程频繁切换可能严重影响性能。 同一个进程内部可能有多个线程,共享同一个京城的所有资源。通过线程支持了同一应用程序内部的并发,免去了进程频繁切换的开销,另外并发任务也更简单。 网络具有天生的并发性,比如数据库可能同时需要处理数以千计的请求。而由于网络连接的不确定性和不可靠性,等待网络交互时,可以让当前的线程进入睡眠、退出调度,处理其他线程,这样能够充分利用系统资源,发挥系统实时处理能力。 多线程是什么一个程序运行的过程中,只有一个控制权存在,当函数调用时,函数获得控制权,成为激活函数;同时其他函数不运行。 多线程程序就是允许一个进程内存在多个控制权,以便让多个函数处于激活状态,让多个函数同时运行,即便是单核计算机也可以通过指令切换,实现多个线程同时运行的效果。 回忆栈的 功能和用途:一个栈中只有最下方的栈可被读写,相应的也只有这个栈对应的函数被激活,处于工作状态。为了实现多线程,则必须绕开栈的限制。这样多个线程就有了多个函数栈,每个栈对应一个线程。 多线程的创建与结束1234567891011121314151617181920212223#include <stdio.h>#include <pthread.h>void* say_hello(void* args){ printf(\"hello from thread\\n\"); pthread_exit((void*)1);}int main(){ pthread_t tid; int iRet = pthread_create(&tid, NULL, sayhello, NULL); if(iRet){ printf(\"create error\"); return iRet; } void* retval; iRet = pthread_join(tid, &reval); if(iRet){ printf(\"join error\"); return iRet; } printf(\"retval %ld\", retval); return 0;} 线程的属性线程有一组属性是可以在创建时被指定的。该属性被封装在一个对象中,该对象可以用来设置一个或一组线程。线程属性对象类型为 pthread_t . 默认属性为 : 非绑定、非分离、默认1MB大小的栈 分离状态 若线程终止时,线程处于分离状态,系统将不保留线程的终止状态;不需要这个状态可一直设置 栈地址 可以设置线程的栈地址 栈大小 系统中有很多线程时,可能需要减小每个线程栈的大小,防止空间不够;如果调用函数很深时,需要加大线程栈的大小。 栈保护区大小 (防止栈溢出,进入错误提示) 线程优先级 继承父进程优先级 调度策略; 新线程的调度时一旦开始运行,直到被抢占或调度停止 争用范围 并发级别 多线程同步互斥锁互斥锁是一个特殊的变量,它有上锁和解锁两个状态。互斥锁一般被设置为全局变量。打开互斥锁可以由某个线程获得,一旦获得这个互斥锁就会被锁上,只有该线程有权打开这个,其他线程需要等待 12345678910111213141516171819202122232425262728293031323334353637pthread_mutex_t mutex_x = PTHREAD_MUTEX_INITIALIZERint total_ticket_num = 20;void *sell_ticket(void *arg){ for(int i=0; i<20; i++){ pthread_mutex_lock(&mutex); if(total_ticket_num>0){ sleep(1); printf(\"sell ticket %d\", 20 - total_ticket_num); total_ticket_num--; } pthread_mutex_unlock(&mutex); } reutrn 0;}int main(){ int iRet; pthread_t tids[4]; int i = 0; for(int i=0; i<4; i++){ int iRet = pthread_create(&tids[i], NULL, &sell_ticket, NULL); if(iRet){ printf(\"Error\"); return iRet; } } sleep(30); void* retval; for(int i=0; i<4; i++){ iRet = pthread_join(tids[i], &retval); if(iRet){ printf(\"Error\"); return iRet; } } return 0;} 条件变量条件变量允许线程阻塞和等待另一线程发送信号的方法弥补互斥锁的不足,他常常和互斥锁一起使用。使用时,条件变量用来阻塞一个线程,当条件不满足时,线程往往解开互斥锁等待条件变化。而当其他的线程改变了条件变量时,它将通知相应被这个条件变量阻塞的一个或多个线程,对这些线程获得锁并测试是否瞒住。 读写锁一些程序中存在读者写着问题,某些资源的访问可能出现两种情况,一种是排他性的(独占),另一种操作访问是可以共享的,可以有多个线程同时去访问某个资源。这就是读操作,这个模型从读写操作中引申出来。 读写锁也叫共享-独占锁。处理读者写者的常见策略是强读者同步或强写者同步,即总是有限读和优先写 信号量线程可以通过信号量实现通信,信号量和互斥锁的区别:互斥锁只允许一个线程进入临界区,而信号量允许多个线程进入临界区。要使用信号量同步,需要包含头文件semaphore.h 12345678910111213141516171819202122232425sem_t t;void get_service(void *thread_id){ int customer_id = *((int*)thread_id); if(sem_wait(&sem) == 0){ usleep(100); printf(\"%d customer service\", customer_id); sem_post(&sem); }}int main(){ sem_init(&sem,0, 2); pthread_t customers[10]; int i, iRet; for(int =0; i<10; i++){ int customer_id = i; iRet = pthread_create(&customers[i],NULL, get_service, &customer_id); if(iRet){...} } int j; for(int j=0; j<10;i++){ pthread_join(customers[i], NULL); } sem_destory(&sem); return 0;} 多线程重入前面介绍了很多方式都是为了解决 “函数不可重入的问题”。 所谓“可重入函数”是指可以由多个函数并发使用而不担心错误的函数。相反,不可重入函数是指只能由一个任务占有,除非能确保函数互斥。可重入函数可以在任意时刻被中断,稍后在继续运行,且不会丢失数据。 可重入函数 不为连续调用持有静态数据 不返回指向静态数据的指针 所有数据由函数调用者提供 使用本地数据、制作全局数据副本来保护全局数据 如果必须访问全局数据,利用互斥锁、信号量 不调用任何不可重用函数 不可重入函数 使用静态变量 返回静态变量 调用了不可重入函数 使用静态数据结构 调用了malloc或free函数 调用了其他IO函数","categories":[],"tags":[{"name":"后台开发","slug":"后台开发","permalink":"http://yoursite.com/tags/后台开发/"}]},{"title":"[后台开发工程师总结系列] 2.操作系统之进程","slug":"后台开发工程师总结系列-2-操作系统之进程","date":"2019-03-09T08:35:08.000Z","updated":"2019-03-09T08:40:01.340Z","comments":true,"path":"2019/03/09/后台开发工程师总结系列-2-操作系统之进程/","link":"","permalink":"http://yoursite.com/2019/03/09/后台开发工程师总结系列-2-操作系统之进程/","excerpt":"进程进程的概念和特征进程结构一般由三部分组成:代码段、数据段和堆栈段。代码段用于存放程序代码数据,数个进程可以共享一个代码段。而数据段存放程序的全局变量、常量和静态变量。堆栈段中栈用于函数调用,它存放着函数参数、函数内部定义的局部变量。对斩断还包含了进程控制块(PCB)。PCB处于进程核心堆栈底部,不需要额外分配空间。PCB是进程存在的唯一标识。系统通过PCB的存在而感知进程的存在。 进程是程序的一次执行 进程是一个程序及数据在处理机上执行时所发生的活动 进程是系统进行资源分配和调度的独立单位。进程的独立运行由进程控制块PCB控制和管理。程序段、相关数据、PCB三部分构成了进程映像。进程映像是静态的进程。 进程具有动态性(创建、活动、暂停、终止过程、生命周期),并发性(多个进程在一段时间同时运行),独立性(进程是一个独立运行获得和调度资源的独立单位)、异步性(进程按照独自不可预知的速度前进)、结构性(每个进程都有一个PCB描述) linux 系统的进程启动过程 整个linux是一个树形结构。树是系统自动构造的,即内核下的0号进程,它是所有进程的祖先。由0号进程创建1号进程(内核态),1 号进程负责执行内核的部分初始化工作及系统配置,并创建若干个用于高数储存和虚拟管理的内核线程。 随后1号进程调用execve()运行可执行程序init , 并演变成用户态1号进程,它按照系统配置文件/etc/initab 的要求,完成系统的启动工作。并创建编号 1、2 和若干终端注册进程 getty。 当getty检测到来自终端的信号时, getty将通过 execve执行注册程序 login, 此时就可以通过用户名、密码登录。如果登录成功,login() 程序执行shell, shell进程接替 getty 进程的pid 取代getty进程。后续进程再通过shell产生 0号进程–》1号内核进程–》1号内核线程–》1号用户进程–》getty进程–》shell进程","text":"进程进程的概念和特征进程结构一般由三部分组成:代码段、数据段和堆栈段。代码段用于存放程序代码数据,数个进程可以共享一个代码段。而数据段存放程序的全局变量、常量和静态变量。堆栈段中栈用于函数调用,它存放着函数参数、函数内部定义的局部变量。对斩断还包含了进程控制块(PCB)。PCB处于进程核心堆栈底部,不需要额外分配空间。PCB是进程存在的唯一标识。系统通过PCB的存在而感知进程的存在。 进程是程序的一次执行 进程是一个程序及数据在处理机上执行时所发生的活动 进程是系统进行资源分配和调度的独立单位。进程的独立运行由进程控制块PCB控制和管理。程序段、相关数据、PCB三部分构成了进程映像。进程映像是静态的进程。 进程具有动态性(创建、活动、暂停、终止过程、生命周期),并发性(多个进程在一段时间同时运行),独立性(进程是一个独立运行获得和调度资源的独立单位)、异步性(进程按照独自不可预知的速度前进)、结构性(每个进程都有一个PCB描述) linux 系统的进程启动过程 整个linux是一个树形结构。树是系统自动构造的,即内核下的0号进程,它是所有进程的祖先。由0号进程创建1号进程(内核态),1 号进程负责执行内核的部分初始化工作及系统配置,并创建若干个用于高数储存和虚拟管理的内核线程。 随后1号进程调用execve()运行可执行程序init , 并演变成用户态1号进程,它按照系统配置文件/etc/initab 的要求,完成系统的启动工作。并创建编号 1、2 和若干终端注册进程 getty。 当getty检测到来自终端的信号时, getty将通过 execve执行注册程序 login, 此时就可以通过用户名、密码登录。如果登录成功,login() 程序执行shell, shell进程接替 getty 进程的pid 取代getty进程。后续进程再通过shell产生 0号进程–》1号内核进程–》1号内核线程–》1号用户进程–》getty进程–》shell进程 进程状态及轮转 进程创建创建状态:进程正在创建尚未就绪,创建经过几个步骤:申请空白PCB、向PCB写入控制和管理信息,然后为进程分配所需资源,最后转入就绪状态 引起进程创建的事件系统创建(用户登录:分时系统中每个用户登录可以被看成一个新的进程。系统为该终端建立一个进程并插入就绪队列, 作业调度:批处理作业中,当系统按照一定算法调度作业时,将该作业调入内存为其分配资源,提供服务) 应用请求 (用户可以基于自己的需求创建新的进程) 进程创建的过程 为进程申请一个唯一的进程识别号与空白PCB 为进程分配资源,为新进程的程序、数据、用户栈分配内存空间 初始化PCB,主要包括标志信息、状态信息、处理机信息 如果就绪队列能够接受新进程,就将进程插入就绪队列中 Linux下的 进程创建父进程和子进程:除了0号进程,linux系统中其他任何一个进程都是其他进程创建的。而相对的 ,fork 函数的调用方是父进程, 而创建的新进程是子进程。 fork 函数不需要参数,返回值是一个进程标识符 1 对于父进程,fork函数返回创建子进程ID 2 子进程 fork 函数返回0 3 创建出错的话 fork 函数返回 -1 fork函数创建一个新的进程,并从内核中为其分配一个可用的进程标识符PID,之后为其分配进程空间,并将父进程空间的内容中复制到子进程空间, 包括数据段和堆栈段,和父进程共享代码段。这时候系统中多了一个进程父进程和子进程都接受系统的调度,fork函数返回两次(分别在父进程和子进程中返回)。 12345678910111213141516#include <stdio.h>#include <stdlib.h>#include <unistd.h>int main(void){ pid_t pid; pid = fork(); if(pid<0){ perror(\"fail to fork\"); exit(-1); }else if(pid ==0){ printf(\"Subprocess, PID: %u\", getpid()); }else{ printf(\"Parentprocess, PID: %u\", getpid()); } return 0;} 子进程和父进程共享数据段和堆栈段中的内容。例子略 事实上,子进程完全复制了父进程的地址空间,包括堆栈段和数据段。但是,子进程并未复制代码段,而是公用代码段。 进程终止结束状态:进程从系统中消失,这可能是因为正常结束或其他原因中断退出。进程结束时,系统首先标志进程结束,然后进一步释放和回收资源。 进程终止的事件 正常结束 异常结束:出现某种错误导致无法运行,:越界、非法指令、运行超时等等 外界干预:进程应外界请求而终止 进程的终止过程 根据被终止的标识符,从PCB结合中检索出PCB并读取进程状态 若进程处于执行状态,立即终止并置标志为真 若进程还有子孙进程,则终止子孙进程防止其不可控 将终止进程的所有资源释放给系统或父进程 将被终止进程移出队列 运行状态: 进程在处理机上运行 就绪状态:进程已处于准备运行的状态,即进程获得了除处理机以外的一切所需资源,只需得到处理机即可执行 阻塞状态(等待,封锁状态):进程等待某一时间而暂停运行,即使处理机空闲也不嗯呢该运行 特殊的进程僵尸进程和孤儿进程在linux中 正常情况下子进程是通过父进程创建的, 子进程和父进程的运行是一个异步的过程。父进程无法预料子进程在何时结束,于是就产生了孤儿进程和僵尸进程。 孤儿进程,是指一个父进程退出后,它的一个或多个子进程还在运行,那么这些子进程将成为孤儿进程。孤儿进程将被init进程(进程号为1) 到进程所收养,并由init进程完成状态收集工作。 僵尸进程,是指一个进程使用fork创建子进程,如果子进程退出,而父进程没有用wait或waitpid调用子进程的状态信息,子进程的进程描述符仍在系统中,这种进程被称为僵尸进程。 简单理解为,孤儿是父进程已退出而子进程未退出;而僵尸进程是父进程未退出而子进程先退出。 为了避免僵尸进程,需要父进程通过wait函数来回收子进程 守护进程linux系统中在系统引导时会开启很多服务,这些服务就叫做守护进程。为了增加灵活性,root可以选择开启的模式,这些模式叫做运行级别。守护进程是脱离于终端在后台运行的进程,守护进程脱离终端是为了避免进程在执行过程中在终端上显示并且不会被终端的信息打断。 守护进程是一个生存期较长的进程,通常独立于控制终端并且周期性的执行某种任务或等待处理某些发生的事件。说话进程常常在系统引导装入时启动,linux系统有很多的守护进程,大多数服务都是通过守护进程实现的。如作业规划进程、打印进程。 在 linux 中每一个与用户交流的界面称为终端,每一个终端开始的进程都会依附于该终端,这个终端就被称为 进程的控制终端,当控制终端被关闭时,相应的进程都会被自动关闭。但是守护进程可以突破这种限制,它从被执行时开始运转,整个系统关闭时才推出。如果想让某个进程不因为用户或终端等变化受到影响,那么就需把一个进程变成一个守护进程。 创建一个守护进程的步骤如下所示: 1 创建子进程,父进程退出。 这是编写守护进程的第一步。由于守护进程脱离终端控制,因此在第一步完成后就会在终端里造成程序已经运行完毕的假象。之后所有的工作都在子进程完成,而与用户终端脱钩。 2 子进程中创建会话 这个步骤是创建守护进程最重要的一步,虽然他的实现十分简单,但是他的意义重大。这里使用的系统函数setid, 这里有两个概念:进程组和会话期 1) 进程组: 一个或多个进程的集合。进程组由进程组ID来唯一标识,除了进程号以外,进程组ID也是一个进程的必备属性,每个进程组都有一个组长进程,组长进程号等于进程组ID,而且进程组ID不会因为组成进程的退出而受到影响。 2) 会话周期: 会话期是一个或多个进程组的集合。通常一个会话开始于用户登录,终止于用户退出,再次期间运行的所有进程都输入这个会话期。 setid 函数用于创建一个新的会话,并担任该会话组的组长,调用 setid 有三个作用: 1 让进程摆脱原会话的控制 2 让进程摆脱原进程组的控制 3 让进程摆脱原控制终端的控制。 那么 创建守护进程为什么需要setid函数? 这是由于创建守护进程的第一步掉用了fork函数来创建子进程,再将父进程退出。由于调用fork函数时子进程全盘拷贝了父进程的会话期、进程组、控制终端等,虽然父进程退出了,但是会话期、进程组、控制终端等还没有改变,所以还不是真正意义上的独立。 3) 改变当前目录为根目录 这一步骤也是必要的步骤,使用 fork 创建的子进程集成了父进程的工作目录,由于进程的运行过程中当前的目录是不能卸载的,这对于以后的使用造成诸多麻烦。通常的做法是将 “/” 变为守护进程的当前工作目录,这样就可以避免上述问题。 4) 重设文件权限掩码 文件权限掩码是指屏蔽掉文件权限中的对应位。 5) 关闭文件描述符 同文件权限码一样, 用fork函数创建的子进程会从父进程哪里继承一些已经打开的文件,这些文件节能永远不会被守护进程读写,但是他们一样消耗系统资源。所以需要关闭来自继承的文件描述符。 1234567891011121314151617181920212223242526272829303132333435363738#include <stdio.h>#include <stdlib.h>#include <string.h>#include <fcntl.h>#include <unistd.h>#include <sys/wait.h>#include <sys/types.h>#include <sys/stat.h>#define MAXFILE 65535int main(){ pid_t pc; int i, fd, len; char* buf = \"this is a Dameon\\n\"; len = strlen(buf); pc = fork(); if(pc<0){ printf(\"error fock\\n\"); exit(1); }else if(pc>0){ exit(0); } setsid(); chdir(\"/\"); umask(0); for(int i=0; i<MAXFILE; i++){ close(i); } while(1){ if(fd = open('/temp/demeon.log',O_CREAT|O_WRONLY|O_APPEND,0600))<0){ perror(\"open\"); exit(1); } write(fd, buf, len+1); close(fd); sleep(10); } return 0;} 进程通信进程间通信就是不同进程间传播或交换信息。 首先进程间可以通过传送、打开文件来实现,不同的进程通过一个或多个文件来传递信息。一般来说进程间通信不包括这种低级的通信方式。Linux操作系统几乎支持所有的UNIX系统进程通信方法:管道、消息队列、共享内存、信号量、套接字。 管道父子进程通过管道通信,管道是一种两个进程间单向通信的机制。因为管道传递数据的单向性,管道又被称为半双工管道,管道这一特点决定了其使用的局限性。管道是最原始的一种通信方式。 数据只能由一个进程流向另一个进程(一个读管道和一个写管道);如果要进行双工通信,则需要建立两个管道。 管道只能用于父子通信或兄弟进程通信(有亲缘关系的进程)。 除了上述局限性,管道还有一个不足,比如管道没有名字(匿名管道);管道的缓冲区大小受限(linux 下一般是4KB);管道传输的是无格式的字节流。这就需要管道的输入方和输出方事先约定好数据格式。使用管道通信时,两端的进程向管道读写数据是通过创建管道时,系统设置的文件描述符进行的。本质上说管道也是一种文件,但是它又和一般的文件不同,可以克服文件通信的一些问题。 通过管道通信的两个进程,一个向管道写数据,一个从中读数据。写入的数据每次都添加到管道缓冲区的末尾,读数据都是从缓冲区的头部读出。 123456789101112131415161718192021222324252627282930#include <unistd.h>#include <stdio.h>#include <stdlib.h>#include <string.h>#define INPUT 0#define OUTPUT 1int main(){ int fd(2); pid_t pid; char buf[256]; int returned_count; pipe(fd); pid = fock(); if(pid<0){ printf(\"Error in fork\\n\"); exit(1); }else if(pid==0){ printf(\"in child process ...\"); close(fd(INPUT)); write(fd(OUTPUT), \"hello world\", strlen(\"hello world\")); exit(0); }else{ printf(\"int parent process ...\"); close(fd(OUTPUT)); returned_count = read(fd(INPUT), buf, sizeof(buf)); printf(\"%d bytes of data received from child process: %s\\n\", returned_count, buf); } reutrn 0;} 在子进程中写数据,在父进程中读数据,两个进程实现了通信:父子进程分别有自己的读写通道,为实现父子进程的通信,只需把无关的文件描述符关闭即可。 具名管道还有一种管道叫做具名管道(FIFO)它不同之处是它提供一个路径名与之关联,以FIFO的形式存在于文件系统中。这样即使与FIFO创建不存在亲缘关系的进程,只要可以访问路径,就能够通过彼此的FIFO通信(能够访问路径和FIFO创建进程之间),因此通过FIFO不相关进程也能交换数据。 有名管道有以下特点 他可以使互不相关的两个进程实现通信 该管道可以通过路径名来指明,并且在文件系统中是可见的。在建立了管道之后,两个进程就可以把它当做普通文件一样读写,使用很方面 FIFO严格遵循先进先出的规则,对于管道与FIFO总是从开始处返回数据,而把数据添加到末尾。 消息队列消息队列用运行在同一机器上的进程通信,与管道类似,是一个系统内核中保存消息的队列,在内核中以消息链表的形式出现。 消息队列与有名管道有不少相同之处,消息队列进行通信可以使不相关的进程,同时他们都是以发送和接收的方式来传递数据的。而且他们都有一个最大长度的限制。 与命名管道相比,消息队列的优势在于:1、消息队列可以独立于发送和接收进程存在,从而消除了同步命名管道时的困哪 2、同时发送消息避免了管道的同步和阻塞,不需要进程提供同步方法 3、接收端可以有选择 的接收数据。 事实上它是一种正在被淘汰的通信方式,完全可以用流管道和套接口的方式取代。 共享内存共享内存允许两个不相关的程序访问同一个逻辑内存。共享内存是在两个正在运行的程序间共享和传递数据的一种非常有效的方式。不同进程间的共享内存通常安排在同一物理内存中。进程可以将同一段内存共享到自己的地址空间中,所有进程都可以访问共享 内存中的地址。 不过,共享内存未提供同步机制,需要进程自行进行同步操作。 共享内存的优缺点: 1:优点 使用共享内存通信非常方便,而且函数接口简单,数据共享还使进程间的数据不用传送,而是直接访问内存,加快了效率,并且没有亲缘关系的要求。 2:缺点 共享内存没有提供同步进制,这使得共享内存的通信往往要借助其他手段来完成。 信号量共享 内存是进程间通信的最快的方式,但是共享 内存的同步问题自身无法解决(即进程该何时去共享内存取得数据,而何时不能取),但用信号量即可轻易解决这个问题 。 进程调度调度层次作业调度高级调度,主要任务是按一定原则从外存中将处于后备状态的作业挑选1个或几个,分配内存、输入输出等资源,建立相应进程。使得他们拥有竞争处理机的权力(内存与辅存之间的调度) 中级调度进程的挂起与就绪 进程调度低级调度,某种方法和策略从就绪队列中选取一个进程,为其分配处理机。进程调度是最基本的调度,频率很高,一般几十毫秒一次 调度算法先来先服务(FCFS)算法FCFS是一种最简单的调度算法,从后备作业队列中选择最先进入该队列作业调度 FCFS是不可剥夺算法,长作业会使后到的短作业长期等待。 特点:算法简单,效率低,对长作业有利,有利于CPU繁忙性作业 短作业优先(SJF)算法从后背队列中选择一个或若干个估计运行时间最短的作业调入内存运行 特点:对长作业不利,如果短作业源源不断,会使得长作业一直处于饥饿状态。 优先级调度算法优先级调度算法每次从后背队列中选取优先级最高的一个或几个作业 特点:优先级调度可以剥夺式占有,也可以非剥夺式占有 高响应比优先高响应比有限主要用于作业调度,该算法是对FCFS和SJF算法的一种平衡,计算每个作业的响应比。 响应比的计算为(等待时间+要求服务时间)/ 要求服务时间 时间片轮转调度算法时间片轮转算法适用于分时系统,系统将所有就绪的进程按照到达时间排成一个序列,进程调度总是选择就绪队列中的第一个进程执行。但是仅能运行一个,如100ms。 特点:受系统响应时间影响、队列进程数目、进程长短影响较大 多级反馈队列调度算法多级反馈队列调度算法是时间片轮转调度算法和优先级调度算法的综合和发展 1) 设置多个就绪队列,为各个队列赋予优先级,1、2、3等等 2) 赋予各个队列中时间片大小不同,优先级高时间片越小 3) 一个进程进入内存后首先放入1级队列末尾,FCFS原则等待,如果其能够完成,则撤离系统,否则放入2级队列的末尾,依次向下执行 4) 仅当1级队列为空时,调度程序调度2级队列中的进程,依次类推。 临界区虽然多个进程可以共享系统中的资源,但许多资源一次只能被一个进程使用,把一次仅允许一个进程使用的资源称为临界资源。 // entry // critical section // exit section 同步与互斥同步:进程之间具有直接制约关系,进程之间需要按照一定的次序进行 互斥:进程之间的间接制约关系,不能同时访问临界区 信号量信号量是一个整形变量,可以被定义为两个标准的原语wait(S) signal(S) 即P、V操作 P操作 如果信号量大于0,执行 -1操作,如果等于0,执行等待信号量大于0 V操作 对信号量完成加1操作,唤醒睡眠的进程 123456789101112typedef int semaphoresemaphore mutex = 1 void P1(){ P(&mutex); //临界区 V(&mutex);}void P2(){ P(&mutex); //临界区 V(&mutex);} 使用信号量实现生产者-消费者问题问题描述:使用一个缓冲区来保存物品,只有缓冲区没满,生产者才可以放入物品;只有缓冲区不空,消费者可以拿走物品 由于缓冲区输入临界资源,需要一个互斥量mutex来完成缓冲区的互斥访问 为了同步生产者和消费者的行为,需要记录缓冲区物品数量,数量可以用信号量表示,empty记录空缓冲区,full记录满缓冲区 12345678910111213141516171819202122232425262728# define N 100typedef int semahporesemaphore mutex = 1;semaphore empty = N;semaphore full = 0;void producer(){ while(True){ int item = produceItem(); P(&empty); P(&mutex); Item.push(item); V(&mutex); V(&full); }}void consumer(){ while(True){ P(&full); P(&mutex); int item = Item.top(); Item.pop(); consume(item); V(mutex); V(&empty()) }} 管程使用信号量机制生产消费问题客户端代码需要很多控制,管程作用是把控制的代码独立出来。 管程有一个重要作用:一个时刻只能有一个进程使用。进程不能一直占用管程,不然其他程序都无法使用 管程的生产者消费者实现 读者-写者问题问题描述: 控制多个进程对数据进行读、写操作,但是不允许读-写和写-写操作同时进行 用一个count表示读进程数量,分别用read_mutex 和write_mutex 作为读锁和写锁 12345678910111213141516171819202122typedef int semaphoresemaphore count = 0;semaphore read_mutex = 1;semaphore write_mutex = 1;void read(){ P(&read_mutex); count++; if(count==1) P(&write_mutex); V(&read_mutex); read(); p(&read_mutex); count--; if(count==0) V(&write_mutex); V(&read_mutex);}void write(){ P(&write_mutex); write(); V(&write_mutex);} 哲学家进餐问题问题描述:五个哲学家围着一张圆桌,每个哲学家面前放着食物,哲学家有两种活动:吃饭与思考,吃饭时,他拿起左边及右边的筷子,并且一次只能拿一根 如果所有哲学家都拿左边的筷子,就会出现死锁,这样只需加一步,当哲学家拿起筷子时检查是否能同时拿起两根筷子,不然就等待 123456789101112131415typedef int semaphoresemaphore chop[5] = {1,1,1,1,1};semaphore mutex = 1;void process(){ while(true){ P(&mutex); P(chop[i]); P(chop[(i+1)%5]); V(&mutex); eat(); V(chop[i]); V(chop[(i+1)%5]); }} 死锁死锁的定义:多个进程因为竞争资源而造成的一种僵局(互相等待),若无外力作用,所有的进程都无法向前推进。 死锁四个必要条件: 互斥条件:进程要求对所分配的资源进行排他性控制,在一段时间内资源仅为一个进程所有。 不剥夺条件:进程所获得资源未使用完毕之前,不能被其他进程强行夺走,只能等获得资源的进程自己主动释放 请求和保持条件:进程已经至少保持了一个资源,但是又提出了新的资源请求,而该资源已被其他进程占有。此时进程被阻塞,但是对自己资源不释放。 循环等待条件:存在某一进程的循环等待链,链中每个进程已获得资源下个进程的请求。 死锁的处理策略死锁的处理便是破坏四个必要条件,使得死锁无法发生 鸵鸟策略把头埋在沙子里,假装问题没有发生 由于解决死锁问题的代价往往很高,鸵鸟策略在很多情况下可以取得更高的性能。 大多数操作系统,Unix、Linux、windows处理死锁都是采用鸵鸟策略 死锁预防 破坏互斥条件 对于可共享的资源竞争,不会发生死锁 破坏不剥夺状态 当一个进程无法获取其需要的资源时,将之前已获得的资源释放,待需要是再重新申请 破坏请求 和 保持条件 预先分配的静态方法,在进程运行前一次申请完它需要的所有资源。在资源不满足前不运行,一旦运行这些资源都归期所有。 破坏循环等待 资源顺序分配法,例如为资源编号,每个进程申请分配某个资源以后,再之后只能申请该编号以后的资源 死锁避免系统的安全状态:所谓安全状态,是系统能按照某种进程推进顺序(P1,P2,,)为每个进程分配资源,直至满足每个进程对资源的最大需求,使每个系统进程都能顺序完成,则(P1、P2,,)称为安全序列。如果无法找到安全序列,则系统处于不安全状态。 允许进程池动态的申请资源,但是每次分配资源前系统都会计算资源分配的安全性,如果分配资源不会导致系统进入不安全状态,将资源分配给进程;否则,进程等待 银行家算法 银行家算法是最著名的死锁避免算法。它的思想是,把操作系统看成银行家,操作系统管理的资源当成银行家管理的资金,向操作系统请求资源相当于向银行请求贷款。 进程申请资源时,系统评估该进程的最大需求资源,检查资源分配后系统是否还处于安全状态,由此来决定是否分配该资源 死锁检测和接触死锁检测死锁定理: 可以通过将资源分配图简化的方法来检测系统状态 S 是否为死锁状态。简化方法如下:(1)、在资源分配图中,找到既不阻塞又不是孤点的进程 Pi (即找出一条有向边与它相连,且该有向边对应资源的申请数量小于等于系统中已有空闲资源数量)。消去它所有的请求边和分配边,使之成为孤立的结点。在这里要注意一个问题,判断某种资源是否有空闲,应该用它的资源数量减去它在资源分配图中的出度。(2)、进程 Pi 所释放的资源,可以唤醒某些因等待这些资源而阻塞的进程,原来的阻塞进程可以变为非阻塞进程。根据(1)中的方法进行一系列简化后,若能消去图中所有的边,则称该图是可完全简化的。 S为死锁的条件是:当且仅当 S 状态的资源分配图是不可完全简化的,该条件为死锁定理。 死锁解除 资源剥夺法 挂起死锁进程,抢占其资源分配给其他进程 撤销进程法 强制撤销一些死锁进程 进程回退法 借助历史信息使一个或多个进程回退到系统不再死锁的地步 问题简析1 同步和异步的区别 同步和异步通常用来形容一次方法调用。 同步方法调用一旦开始,调用者必须等到方法调用返回后,才能继续后续的行为。 异步方法调用更像一个消息传递,一旦开始,方法调用就会立即返回,调用者就可以继续后续的操作。而,异步方法通常会在另外一个线程中,“真实”地执行着。整个过程,不会阻碍调用者的工作。 2 进程和线程的区别,谁调度的进程 进程是资源分配的最小单位,线程是程序执行的最小单位。 进程有自己的独立地址空间,每启动一个进程,系统就会为它分配地址空间,建立数据表来维护代码段、堆栈段和数据段,这种操作非常昂贵。而线程是共享进程中的数据的,使用相同的地址空间,因此CPU切换一个线程的花费远比进程要小很多,同时创建一个线程的开销也比进程要小很多。 线程之间的通信更方便,同一进程下的线程共享全局变量、静态变量等数据,而进程之间的通信需要以通信的方式(IPC)进行。不过如何处理好同步与互斥是编写多线程程序的难点。 但是多进程程序更健壮,多线程程序只要有一个线程死掉,整个进程也死掉了,而一个进程死掉并不会对另外一个进程造成影响,因为进程有自己独立的地址空间。 3 死锁的条件,如何检测死锁 见上文死锁条件 死锁定理: 可以通过将资源分配图简化的方法来检测系统状态 S 是否为死锁状态。简化方法如下:(1)、在资源分配图中,找到既不阻塞又不是孤点的进程 Pi (即找出一条有向边与它相连,且该有向边对应资源的申请数量小于等于系统中已有空闲资源数量)。消去它所有的请求边和分配边,使之成为孤立的结点。在这里要注意一个问题,判断某种资源是否有空闲,应该用它的资源数量减去它在资源分配图中的出度。(2)、进程 Pi 所释放的资源,可以唤醒某些因等待这些资源而阻塞的进程,原来的阻塞进程可以变为非阻塞进程。根据(1)中的方法进行一系列简化后,若能消去图中所有的边,则称该图是可完全简化的。 S为死锁的条件是:当且仅当 S 状态的资源分配图是不可完全简化的,该条件为死锁定理。4 死锁的必要条件,银行家算法 见上文死锁条件 见上文银行家算法 5 调度算法有哪些 见上文调度算法 6 通俗的语言,面对一个非程序员,解释进程与线程的区别 个人理解:把进程和线程比作一家公司和公司的员工,类比如下: 进程是分配资源的最小单位(资金、材料、工具属于公司),而线程是最小的调度单位(公司领导指派某个人去工作) 进程有独立的地址空间,独立的代码段、堆栈段和数据段,申请昂贵(公司有独立的地址、办公室、公章、注册单位)而且进程间切换代价大,一个进程内的线程切换十分方便(同一个公司的员工互相调动很方便) 线程间通信方便,由于线程共享堆栈、数据段(公司内部沟通方便)而进程间沟通需要通过IPC方式,还需要处理同步、互斥,这也是多线程编程的难点。 而多进程更加的健壮,进程间并不互相依赖。(公司A和公司B都可以独立完成一个项目) 7 死锁是什么,为什么会产生死锁,怎么解决死锁问题,预防死锁、避免死锁 死锁的定义:多个进程因为竞争资源而造成的一种僵局(互相等待),若无外力作用,所有的进程都无法向前推进。 死锁有四个必要条件:互斥、不剥夺、请求和保持、循环等待 破坏这些条件,包括:资源剥夺、撤销进程、进程回退 可以采用银行家算法预防和避免死锁 8 进程的同步进制有哪些? 进程的通信机制有哪些? 临界区、互斥量、信号量、事件 临界区: 通过多线程的串行化来保证某一时刻只有一个线程能访问资源或代码,适合控制数据访问,只能控制同一进程中的线程 互斥量: 为协调共享资源设计,互斥对象只有一个,只有拥有互斥对象的线程可以访问资源 信号量:允许多个线程访问同一资源,但是限制线程数目。适用于跨进程同步,功能强大。 事件:通知线程有事情发生, 启动后续任务。 进程通信: 管道:父子进程通过管道通信,管道是一种两个进程间单向通信的机制。因为管道传递数据的单向性,管道又被称为半双工管道,管道这一特点决定了其使用的局限性。管道是最原始的一种通信方式。 具名管道(FIFO):还有一种管道叫做具名管道(FIFO)它不同之处是它提供一个路径名与之关联,以FIFO的形式存在于文件系统中。这样即使与FIFO创建不存在亲缘关系的进程,只要可以访问路径,就能够通过彼此的FIFO通信(能够访问路径和FIFO创建进程之间),因此通过FIFO不相关进程也能交换数据。 消息队列:消息队列用运行在同一机器上的进程通信,与管道类似,是一个系统内核中保存消息的队列,在内核中以消息链表的形式出现。消息队列与有名管道有不少相同之处,消息队列进行通信可以使不相关的进程,同时他们都是以发送和接收的方式来传递数据的。而且他们都有一个最大长度的限制 共享内存:共享内存允许两个不相关的程序访问同一个逻辑内存。共享内存是在两个正在运行的程序间共享和传递数据的一种非常有效的方式。不同进程间的共享内存通常安排在同一物理内存中。进程可以将同一段内存共享到自己的地址空间中,所有进程都可以访问共享 内存中的地址。 信号量:信号量是一个计数器,可以用来控制多个进程对共享资源的访问。它常作为一种锁机制,防止某进程正在访问共享资源时,其他进程也访问该资源。因此,主要作为进程间以及同一进程内不同线程之间的同步手段。 9 进程的状态转换图及转换事件 见上文","categories":[],"tags":[{"name":"后台开发","slug":"后台开发","permalink":"http://yoursite.com/tags/后台开发/"}]},{"title":"[后台开发工程师总结系列] 1.C++","slug":"后台开发工程师总结系列-1-C","date":"2019-03-09T08:28:29.000Z","updated":"2019-03-09T08:38:20.049Z","comments":true,"path":"2019/03/09/后台开发工程师总结系列-1-C/","link":"","permalink":"http://yoursite.com/2019/03/09/后台开发工程师总结系列-1-C/","excerpt":"数组1. 一维数组的声明一维数组的声明应指出以下几点 储存在元素中的值类型 2. 数组名 3. 数组中的元素数 数组中定义的类型可以使内置类型或类类型,除引用外,数组元素还可以是符合类型,但是不能定义引用。 虽然没有引用数组,但是可以有数组引用 12int a[6] = {0,2,4,6,8};int (&p)[6] = a;","text":"数组1. 一维数组的声明一维数组的声明应指出以下几点 储存在元素中的值类型 2. 数组名 3. 数组中的元素数 数组中定义的类型可以使内置类型或类类型,除引用外,数组元素还可以是符合类型,但是不能定义引用。 虽然没有引用数组,但是可以有数组引用 12int a[6] = {0,2,4,6,8};int (&p)[6] = a; 2. 一维数组的初始化在定义数组时,可以为元素提供一组用逗号分隔的初值,称为初始化列表。数组元素若没有被显示初始化,就会被像普通变量一样初始化。 函数体外定义内置数据类型,元素初始化为0 函数体内定义内置数据类型,元素无初始化 如果不是内置类型,不管定义在哪里都会调用构造函数,没有构造函数报错 数组大小未知,可以用C++风格的一维数组动态声明 123int* a = new int[n];delete[] a; 3. C风格的字符串与字符数组C风格字符串包含两种 1 字符串常量 以双引号扣起来的字符序列是字符串常量 2 末尾添加了”\\0” 的字符数组 C++中有很多字符串处理函数(strcpy, strcat)传递给这些函数的参数必须有非零值,且指向以NULL结束的字符数组。 4. 二维数组二维数组是最常用的高维数组,包含了数据行和列 1234567int ia[3][4]={ {0,1,2,3}, {4,5,6,7}, {8,9,10,11}};// 或顺序初始化int ia[3][4] = {0,1,2,3,4,5,6,7,8,9,10,11}; 本质上讲,所有数组在内存中都是一维线性的,不同的语言储存方式不同。 C++中采取了行优先的存储方式 5. 数组指针、指针数组与数组名的指针操作C++常常把地址当成整数来处理,但这不意味着程序员可以进行算术操作。 C++的指针运算一般包含两种形式,第一种形式是 指针 + - 整数 ,在C++中这种操作表示走动几个元素的位置。 1234567void main(){ int i = 11; int const *p = &i; p++; printf(\"%d\", *p);}// 运行结果是一个 Garbage value 第二种类型是指针运算有以下形式:指针 - 指针 得到是字符间的鲁丽,而且是以数组长度为单位,(如果两指针不在一个数组,结果未定义) 6. 指针数组与数组指针所谓指针数组,是指一个数组里面装着指针。即指针数组是一个数组。一个有10个指针的数组如下定义 1int* a[10]; 所谓数组指针,知识一个指向数组的指针,一个指向10个元素的数组指针定义为 1int (*p)[10]; 7. 线性表的线性存储一维数组可用来实现线性表的顺序存储 线性表的储存顺序又称顺序表,其中,线性表是逻辑概念(一一对应)而顺序表和链表是储存结构,二者属于不同层面的概念 顺序表最主要的特点是随机存取,及通过首地址和元素号在O(1) 的时间找到指定的元素,但是插入和删除需要大量操作。其有n+1 个插入点,平均时间复杂度是 n/2, 删除的平均时间复杂度是O(n+1/2) 字符串1. 字符串标准函数 函数名 含义 strlen(s) 返回s的产犊,不包括最后的空字符串 null strcmp(s1, s2) 比较两个字符串是否相同,相等返回0; s1大于s2 返回整数,否则返回负数 strcat(s1, s2) s2 拼接拼接到s2 后 返回s1 strcpy(s1, s2) 将s2 赋值给s1, 并返回 以上函数的简要实现 123456789101112131415161718192021222324252627282930int strlen(const char *str){ assert(str!=NULL); int len = 0; while((*str++) != '\\0') len++; return len;}int strcmp(const char* str1, const char* str2){ assert(str1!=NULL && str2!=NULL); int ret = 0; while(!(ret=*(unsigned char *)str1 - *(unsigned char*)str2) && *str1){ str1++; str2++; } if(ret<0) ret = -1; else if(ret>0) res = 1; return ret;}char* strcat(char* strDest, const char* strSrc){ char* address = strDest; assert(strDest!=NULL&&strSrc!=NULL); while(*strDest) strDest++; while(*strDest++=*strSrc++); return address;}char* strcpy(char* strDestination, const char* strSource){ assert(strDestination!=NULL && strSource!=NULL); char* strD = strDestination; while((*strDestination++=*strSource++) != '\\0'); return strD;} 2. memcpy与 memsetmemcpy 功能:从源 src 所指向的内存地址开始拷贝n个字节到目标dest地址所存地址的起始位置。 memset功能:将s中前n个字节用ch替换并返回s,该方法是对较大结构体、数组较快的清零方法。 123void* memcpy(void* dest, const void* src, size_t n);void* memset(void* dest, int ch, size_t n); strcpy 与 memset 的区别 复制的内容不容,strcpy只能复制字符串,而memcpy可以复制任何内容,而且strcpy还会复制结束的’\\0’ 复制的方法不同,strcpy不需要指定长度,遇到 \\0 结束, 而memcpy有第三个参数限制长度 结构体、共用体、枚举1. 结构体2. 共用体结构体和共用体都由不同的数据结构组成,但是在同一时刻,共用体只存放了一个被选中的数据成员,对于共用体中成员的赋值,会导致其他成员的重写,原来的值就不存在。 共用体的这个特性常与大端、小端一起考察, 在操作系统中,x86和一般的OS(如windows,FreeBSD,Linux)使用的是小端模式。但比如Mac OS是大端模式。 大端储存格式是字节高在低地址中,而小端相反 123456789union Student{ int i; unsigned char ch[2];}int main(){ Student student; stuednt.i = 0x1420; printf(\"%d, %d\", student.ch[0], student.ch[1]);} 3. sizeof 运算符sizeof 是一个单目运算符,就像其他++ – 一样,它并不是函数,sizeof 以字节形式给出储存的大小,操作数可以使一个表达式或类型名,而且sizeof发生在编译时,忽略括号中的各种计算 12345678char ca1[] = {'C', '+', '+'}; // strlen(ca1) = 未定义 sizeof(cha1) = 3char ca2[] = {'C', '+', '+', '\\0'}; //strlen(ca2) = 3 sizeof(ca2) = 4int a[10] ; // sizeof(a) = 40char b[] = \"hello\"; // sizeof(b) = 6int *c = new int[50]; // sizeof(c) = 4int (*a)[10]; // sizeof(a) = 4; 4. struct的空间计算关于struct的笔试题比较多,struct计算较为复杂,总体遵循两个原则 整体占用空间是 最大成员所占字节数的整数倍 , 数据对齐原则, 排到成员变量时,前面拜访的大小必须是该类型大小的整数倍 123456789101112struct s1{ char a; double b; int c; char d;}; // 24struct s2{ char a; char b; int c; double d;}; //16 预处理器、作用域、static、const 以及内存管理1. C预处理器C语言预处理器在编译器之前运行 ,主要包括 宏定义与宏替换 2 文件包含 3 条件编译 宏是借用汇编语言的概念,为C语言程序中方便做一些定义和扩展,这些语句以define开头, 1#define max_v 1000 由于预处理在编译之前进行,而编译的任务之一是语法检查,所以预处理不做语法检查、不分配内存 12#include <standard_header>#include \"my_file.h\" 如果定义在尖括号 <> 里, 那么认为该头文件是标准头文件。编译器会在预定位置搜索这些文件,如果文件名在一对引号里,那么是非系统头文件,查找源于源文件坐在的路径。 2. 全局变量与局部变量全局变量也被称为外部变量,他在函数外部定义,不属于哪个函数,它属于一个源程序文件,作用域是整个个源程序。 引用一个全局变量有两种方式:引用头文件、extern 两种方式 123456// file1.cppint count = 1;// fiel2.cppextern int count;count++; 3. STATIC不考虑类,static 的作用主要有三条: 隐藏 当同事编译多个文件时,所有为加static前缀的变量和函数都具有全局可见性 static默认初始化为0, 包括未初始化的全局静态变量和局部静态变量。静态变量和全局变量都储存与BSS段中,BSS段中所有字节默认值都为0, 保持局部变量内容的持久 函数内部的局部变量,调用时存在,退出时消失,但是静态局部变量定义后就一直存在着。值的注意的是,他虽然存在,但是退出作用域后依然不能调用。 4. 类中static中的作用C++重用了static这个关键字,并赋予了与之前不同的含义:表示属于一个类而不属于这个类任何对象的变量或函数(和java一样) static独立于类的对象存在 1234567891011class Account{public: void applyint(){ amout += amount * interRate} static double rate() {return interRate;} static double rate(double);private: std::string owner; double amount; static double interRate; static double initRate();} 静态数据成员 在内类数据成员声明前加static, 该数据成员就是类的静态数据成员。通常,非static数据成员存在于每个对象中。static 数据成员独立于类的任何对象而存在;每个与static数据成员是与类关联的对象,并不与该类相关联,也就是当某个类的实例修改了静态成员变量,修改值被所有类可见 静态数据成员也存在全局区,静态数据成员定义时要分配空间,所以不能在类中声明,static数据成员必须在类的定义体外部定义正好一次 , 规则由例外, const static 基本整形可以在定义体中初始化。 类中数据成员的布局情况是: 非静态成员在类对象中排列顺序和生命顺序一致,其在任何声明的静态成员都不会被放进静态布局中 静态数据成员放在全局中,和类对象无关。 5. 静态成员函数静态数据成员与静态成员函数一样,都是类的内部实现,属于类定义的一部分,因为它为类服务而不是为某一个类服务。因为普通的成员函数属于某个类的对象,所以普通的成员函数一般隐含了一个this指针,这个this指针指向类对象本身。 但是与其他普通成员函数比,静态成员函数不与任何对象关联,因此它不具有this指针,因而它无法访问属于类对象的非静态数据成员,也无法访问非静态成员函数 他只能调用其他的静态成员函数与静态数据成员。 因为static成员不是任何对象的组成部分,它不能被声明为const, 也不嗯呢该被声明为虚函数、volatile 静态函数被总结为以下几点: 静态成员之间可以互相访问,包括静态成员函数访问静态成员、函数。 非静态成员函数 可以任意非访问静态、或非静态成员函数、成员 由于没有this指针的开销,静态函数相比非静态函数略快 6.const定义C++ 中 const限定符 把一个对象转换成一个常量 1const int buffSize = 512; 常量修改后就不能被修改,所以定义时必须初始化。 在全局作用域里定义非const变量时,它在整个程序中都可以访问。与其他变量不同,除非特别说明,const变量定义该对象文件的局部变量,不能被其他文件访问。而定义的extern关键字可以使其在外部访问。 const在 C和C++中的区别常量引进是在早起的C++中,当时标准正在制定。C中const意思是 “一个不能被改写的普通变量”因而它总是占用存储。 C中的const是内部连接,而C++中默认const是外部连接。这样C++中完成相同的事就需要改成外部连接。 const最初提出是取代#define,其有几个优点 const 常量有数据类型 常量可能会比define产生更小的目标代码 const 可以执行常量折叠 指针和 const 修饰符1234567// const 对象指针// 指针指向的对象不可变const double *cptr;// const 指针// 指针 指向不可变double* const cptr; 修饰参数和返回值const最具威力的用法是对函数声明的应用,在一个函数式声明内,const可以和函数返回值、参数、函数自身产生关联 const修饰返回值 若返回值是值类型,则对于内部数据类型来说,返回值是常量并没有关系 const修饰函数参数 如果函数值传递,可用const限制函数参数。这是明确告诉编译器这个值不会也无法改变。由于是传值,这种约定对于调用者意义不大,然而若是在函数参数使用引用,函数可能会接受临时对象。const保证了该引用的值在函数运行过程中不会被改变。 const在类中的使用 const成员函数 1234class base{ void func1(); void func2() const;} 上述代码中,func2 是base常量的成员函数,func2函数末尾声明const隐含了this形参的类型 const施加于成员函数的目的,是为了确保成员函数可以作用域const对象上,const对象、指针、指向const对象的指针或引用只能调用非const成员函数。 C++中说明 static、const、static、const成员变量的初始化 C++中,static静态成员变量不能在类内初始化,在类内部只是声明,定义必须在类定义体的外部,通常在类的实现文件中初始化,static关键字只能用于类定义体内部的声明中,定义时不能标注为static 在C++中, const成员变量也不能在类定义处初始化,只能通过构造函数初始化列表进行,并且必须有构造函数。const数据成员只在某个对象的生存期是常量,而整个类而言是可变的。因为类可以创建多个对象,不同对象其const值可以不同,所以不能在类的声明中初始化const成员。 内存的管理与释放一个c++程序,内存主要包含以下部分,栈区、堆区、全局(静态)储存区、文字常量区、代码区 Linux 程序内存空间布局下图是一个典型的内存空间布局 代码段 通常指存放程序执行代码的一块内存区域。这部分大小在程序运行前已经确定,并且内存区域只读,某些架构也允许可写 初始化数据段,存放程序中已初始化的全局变量 未初始化数据段, 未初始化全局变量的一块区域 堆 堆用于储存程序运行时动态分配的内存段,它的大小不确定,可以动态的扩张或缩减,程序调用malloc及free来动态分配内存,当进程调用malloc、free时,新配的内存被动态添加到堆上或删去 栈 存放程序的局部变量,并且用户函数调用的传参和返回 堆栈的区别1 申请方式不同 栈: 系统自动分配,声明在函数中一个局部变量;系统自动在栈中为其分配空间 堆: 需要程序员自己申请。并指明大小 2 申请后系统的相应不同 栈: 只要栈的剩余空间大于申请空间,系统便提供内存,否则报异常 堆: 操作系统有一个记录空闲内存的链表,系统受到申请会遍历链表去找一块内存,从空间链表中删去,并将剩下的空间再放回去。 3 申请大小的限制不同 栈: 栈向低地址扩展,是一块连续的区域,栈顶的地址和栈的最大容量是系统规定好的 10M 堆: 堆是低地址向高地址扩展,不连续的内存区域 理论32 位系统可以有 4-1 = 3G的空间 4 申请效率不同 栈由系统自动分配,速度较快,程序员无法控制 堆是由new分配、速度慢,容易产生碎片,但是方便 堆和栈的区别 栈区由编译器自动分配释放,存放函数的参数值,局部变量的值等。其操作方式类似于数据结构中的栈,速度较快。 堆区一般由程序员分配释放,若程序员不释放,程序结束后由操作系统回收。注意他和操作系统中的堆不一样,分配方式类似于链表,速度较慢且易产生碎片,不过用起来方便 1234// C语言char* p1 = (char *)malloc(10);// C++char *p2 = new char[10]; malloc,free和 new, delete 的区别 相同点:都可以动态的申请、释放内存 不同点: 操作对象不同。 malloc和free是库函数,不是运算符,不在编译器控制权限之内,无法构造和析构。 new的执行过程是,首先调用 opreator new 的标准库函数,分配足够大的原始未知类型的内存,以保存指定类型的一个对象,接下来运行该类型的一个构造函数,用指定的初始化方式构造对象,最后返回新构造对象的指针。 delete执行过程是 首先指向对象的析构函数,然后调用opreator delete标准函数释放内存。 用法上不同 malloc的返回值是void*, 所以调用malloc需要进行显示转换,转换成需要的指针类型 malloc函数本身不识别内存是什么类型,他只关心字节大小 free(p)释放内存,如果p是NULL指针,那么free多少次都不会出问题,但是如果p不是null指针,对p的两次free就会出问题。 总结如下: malloc free 是C++库函数,new delete是运算符 new自动计算分配空间,而malloc需要手工计算 new类型安全的,而malloc不是 new调用opreator new分配空间、调用构造函数、而malloc不调用构造函数。delete调用析构函数,然后operator delete, 释放实例的空间 malloc,free 需要库函数支持, new/delete 不需要 什么是声明周期、作用域、全局变量、静态变量、局部变量、const变量生命周期 | 类型 | 作用域 | 生命周期 | 内存布局 | 定义方法 || ———— | ———————————- | —————— | —————— | ————————– || 全局变量 | 全局作用域(只在一个源文件中定义) | 程序运行中一直存在 | 全局(静态)存储区 | || 全局静态变量 | 文件作用域 | 程序运行一直存在 | 全局(静态)存储区 | static 关键字 const 关键字 || 静态局部变量 | 局部作用域 | 程序运行期一直存在 | 全局(静态)存储区 | 局部static定义 || 局部变量 | 局部作用域 | 程序出局部即被销毁 | 栈区 | auto 或省略 | 函数 函数是有名字的计算单元,对程序的结构化至关重要。 C++中,函数原型就是函数的声明。所以函数除了向用户说明如何使用以外,还告诉编译器存在这样一个可以使用的函数。函数的声明同变量一样,是一个语句,函数的定义为返回类型、函数名、形参表、函数体组成。 1. 参数传递 函数的参数分为形参和实参两种。 形参出现在函数的定义中,整个函数体都可以使用,离开函数不能使用。实参出现在主调函数中,被调入函数后实参也不能使用。C++有三种传值方式:值传递、指针传递、引用传递 给函数传递实参遵循变量的初始化规则,非引用类型以相应的实参副本初始化,对形参的修改只作用域副本,为了避免副本的开销,可以将参数指定为引用类型。任何对引用类型的修改都会影响实参值本身 引用传递有以下特点: 传递引用给函数,这时被调函数的形参就作为原来主调函数中实参变量或对象的一个别名来使用,所以在被调函数中对形参变量的操作就是对其相应目标对象的操作。 使用应用传递函数的参数,在内存中没有实参的副本,它是直接对实参尽心该操作。函数调用时,需要给形参分配存储单元,形参是副本;如果传递的是对象,还将调用拷贝构造函数。 指针作为函数参数虽然也能达到引用的效果,但是在被调函数中同样要给形参分配存储单元 2. 内联函数内联函数一般用inline修饰,一般有两种 成员函数为内联函数 在类中定义的成员函数全部默认为内联函数,可以显式加上inline标识符。 普通函数成为内联函数 普通函数添加inline关键字称为内联函数。 通常编译时,内联函数不进行调用,而是函数体替换成函数名。内联扩展可以消除调用时的时间开销 3. 函数重载函数重载是指在同一块作用域内,可以有一组相同的函数名,不同的参数列表,这组函数被称为重载函数。重载函数通常被命名为一组功能相似的函数,减少了函数名的数量。 4. 函数的模板与泛型首先介绍泛型编程的概念。所谓泛型编程就是独立于任何特定的方式编写代码。使用泛型时,需要提供程序实例所操作的值或类型。泛型编程和面向对象编程一样,都依赖于某种形式的多态性。 面向对象编程的多态性应用于存在于继承关系的类,我们能够使用这些类的代码,忽略基类与派生类之间的类型差异。只要使用基类的指针或引用,基类、派生类对象就可以使用相同的代码。 5. 函数模板函数模板定义以template关键字开始,后接模板形参表,模板形参表用尖括号括住一个或多个模板形参的列表,形参之间以逗号分隔。 1234template <typename T>void callWithMax(const T &a, const T &b){ f(a>b?a:b);} 形参关键字跟在关键字class或 typename 之后,这里几乎没有区别。 6. 引用引用就是对象的另一个名字,所谓引用其实是一个特殊的变量,这个变量的内容是绑定在这个引用上面对象的地址,而使用这个变量时,系统就会自动根据这个地址去找它绑定的变量,然后再对变量进行操作。所以本质上说引用还是指针,只不过这个指针是不能被修改的,任何时候他的操作都会发生到他指向的指针身上。所以说C++中引用一旦定义,就必须将他与一个变量绑定起来,且不能修改这个绑定 引用不能为空,引用被创建时它必须被初始化。而指针可以为空值 一旦引用被定义就不能被修改,而指针可以随时修改 不可能有空引用 sizeof(引用) 得到的是指向变量的大小, 指针得到的指针本身的大小 引用复制修改是直接修改这个引用关联的对象值 引用使用时不需要解引用。而指针需要解引用 如果返回动态对象或内存,必须使用指针,否则可能引起内存泄露 类1. 访问标号访问标号public、private、protected 多次出现在类定义中,给定的访问标号应用到下一次出现为止 2. 类成员简介* 成员函数声明成员函数是必须的,但是定义成员函数是可选的,类内定义默认为inline 调用成员函数时,实际上使用对象调用,每个额成员函数都有一个隐含的形参的this 。 在调用成员函数时,this初始化为函数的地址。 * 构造函数构造函数时特殊的成员函数,与其他成员函数不同,构造函数和类同名,而且没有返回类型。一个类可以有多个构造函数,每个构造函数必须有与其他构造函数不同的类型或形参。如果一个类没有显示的定义任何构造函数,编译器将自动为这个类生成默认构造函数。 若使用编译器自动生成的默认构造函数,则类中变量按照初始化变量的规则初始化。 C++中,成员变量的初始化顺序与变量在类型中的声明顺序相同,而与他们在构造函数中初始化列表中的顺序无关。 * 拷贝构造函数拷贝构造函数、赋值操作符、析构函数 总称为赋值控制,编译器自动实现这些操作。 如果累需要析构函数、则它也需要赋值操作符、拷贝构造函数。这是一个有用的经验法则, 被称为三法则。它的含义是,如果有析构函数,就需要所有的三成员。 通常编译器合成拷贝构造函数十分精炼–只做必要的工作。但是某些类依赖默认定义会导致灾难。 只有单个形参,而且形参是对该类型的引用(常常加const),这样的构造函数被称为 拷贝构造函数。该函数有以下作用 根据另一同类型对象初始化一个对象 复制一个对象,将他作为实参传递给一个函数。 初始化顺序容器中的元素 根据元素初始化列表初始化元素数组 *浅拷贝与深拷贝浅拷贝:被复制对象的所有变量都与原来有相同的值,而所有其他对象的引用还指向元对象,仅仅复制对象,不复制其引用对象 深拷贝:被复制的对象都与原对象有相同的值,除去其他对象的变量。引用其他对象变量指向被复制过的新对象,而不是原有对象。(换言之把要复制对象的应用对象全部复制了一份) 12345678910struct Test{ char *ptr;}void shallow_copy(Test &src, Test &dest){ dest.ptr = src.ptr;}void deep_copy(Test &src, Test &dest){ dest.ptr = malloc(strlen(str.ptr)+1); memcpy(dest.ptr, src.ptr);} *析构函数构造函数的用途是自动分配资源。构造函数可以打开缓冲区或文件,在构造函数分配了资源以后,需要一个对应的操作回收、自动释放资源。析构函数就是这样一个特殊的函数。作为构造函数的补充,对象超出动态分配的作用域或被删除时,自动应用析构函数。 构造函数不能被定义为虚函数,但是析构函数可以被定义为虚函数。 * 方法覆盖、重写覆盖是指:派生类覆盖类中的同名函数,要求基类函数必须是虚函数 与基类虚函数有相同的参数个数 与基类虚函数有相同的参数类型 与基类虚函数有相同的返回类型 覆盖和重写是子类和父类之间的关系,是垂直关系。而重载是同一个类中不同方法之间的关系。 * 方法隐藏隐藏是指在某些情况下,派生类函数屏蔽了同名函数。 如果两个函数参数相同,基类不是虚函数(与重写的区别是是否是虚函数) 两个函数参数不同,不论是否虚函数都会被屏蔽 面向对象1. 继承权限通过继承机制,可以利用已有数据类型定义新的数据类型。所以定的新数据类型不仅有新定义成员,还有旧成员。已存在的类称为父类、或基类。而派生出的类称为派生类或子类。 继承可以有多继承,而这些基类有一个共同的基类,则在最低层的派生类中会保留这个间接共同基类成员的多份同名成员。为了解决这个问题,提出了虚继承。虚继承时,公共基类在对象中只有一份拷贝。 继承的访问权限 值的注意的是,派生类对象和派生类中成员函数对基类的访问权限是不同的。 2. 继承二义性类指针的转换规则是: 共有继承时,派生类对象、对象指针、对象引用 可以赋值给基类的对象、指针、引用 (隐式转换) C++允许把基类指针显示转换成派生类的指针或引用 一个指向基类指针可以用来指向该基类公有派生类的任何对象,这是C++动态性实现的关键。 3.多重继承和菱形继承当继承基类时,派生类获得了基类所有数据成员的副本。如果这时进行多继承,类会包含两个类的子对象。 一般来说,派生类对基类的访问应当具有唯一性,但是多继承时,编译器无法判断数据成员,这就是二义性问题。二义性问题可以通过定义一个同名函数,对父类同名函数进行隐藏来解决。 菱形继承 可以通过虚基类解决。 4. 虚函数多态多态性是面向对象语言的基本特征,仅仅是将数据和函数绑在一起,封装、继承都不能真正的了解面向对象的设计思想。多态是面向对象语言的精髓。多态性可以被概括为:“一种接口, 多种方法”,前面讲过函数重载是一种简单的多态,一个函数名对应着几个不同的函数原型。 更通俗的说,多态是统一个操作作用于不同的对象会有不同的响应;多态分为静态多态和动态多态。函数重载和运算符重载是静态的多态,虚函数属于动态的多态。 静态和多态联编程序调用函数时, 具体使用哪个模块是编译器决定的。以函数重载为例,C++编译器根据传递给函数的参数和函数名来决定具体使用哪个函数,称为联编或绑定。编译器可以在编译过程中实现这个联编,在编译过程中进行的联编叫做静态联编、或早期联编。 在一些场合下,编译器无法在编译过程中完成联编,必须在程序运行时选择,因此编译器必须提供一套“动态联编”的机制,也叫晚期联编。C++通过虚函数实现动态联编。 虚函数的定义虚函数定义很简单,加virtual即可 如果一个基类的成员函数被定义为虚函数,那么他在所有派生类中保持为虚函数;即在派生类中省略了virtual关键字,也仍然是虚函数。 派生了虚函数有要求: 与基类虚函数有相同的参数个数 与基类虚函数有相同的参数类型 与基类的虚函数有相同法返回类型 即除函数体完全相同,由之前的定义,其中有不相同的定义,即被认为是函数隐藏。 虚函数的访问和普通函数一样,虚函数一样可以通过对象名来访问,此时编译器采用静态联编。通过对象名访问虚函数时,调用哪个类取决于定义对象名的类型。对象类型是基类时,调用基类函数;对象类型是子类时,调用子类函数。 使用指针访问非虚函数时,编译器根据指针类型来决定调用的函数,而不是根据指针指向的对象类型 使用指针访问虚函数时,编译器根据指针指向的类型来决定调用的函数(动态联编),而与指针本身的类型无关 引用访问与指针访问类似,不同的是引用一经声明其调用函数就不会被改变。引用可以作为限制的指针。 总结如下, C++默认不触发动态绑定,触发条件有2: 只有指定为虚函数的成员函数才能动态绑定,成员默认为非虚函数 必须通过基类类型的指针或引用进行访问。 构造函数为什么不能是虚函数假设A是父类,B是子类,则构造函数的顺序是 A -> B 而根据虚函数的性质,如果构造函数是虚函数, 一个声明A类的指针去指向B类,该类初始化时需要先找B类的构造函数 B -> A 这样产生了循环调用 虚函数表指针 (vptr)及 虚基类表指针(bptr)见下文 C++对象模型 纯虚函数许多情况下,基类不能对虚函数给出有意义的实现,而把它声明为纯虚函数,它的实现留给基类的派生类去做,这就是纯虚函数的作用。 纯虚函数可以让类有一个操作名称而没有操作内容,让派生类继承时再具体的给出定义。凡是含有纯虚函数的类称为抽象类,这种类不能声明对象,只是作为基类为派生类服务。除非在派生类中完全实现所有的纯虚函数。否则派生类也是抽象类,不能实例化对象。 简单对象模型对比简单对象模型第一个模型十分简单,它可能为了降低 C++ 编译器设计复杂度而开发出来,而空间、执行效率较低。在这个简单的模型中,一个objects是一系列的slots, 每一个slots指向一个menbers。 在这模型中,members 本身不在objects中,只有指向members的指针才放在objects内,这样可以避免 members不同类型而需要不同空间所招致的问题。这个模型并没有被引用,不过索引、slot 数目的概念被应用于指向成员的指针概念。 表格驱动对象模型为了所有classes所有objects有一致的表达方式,一种对象模型把members信息抽出来,放在一个成员变量和成员函数表格中。 C++对象模型C++对象模型是从 简单对象模型派生出来的,并且对内存空间和存取时间做了优化。在这个模型中,非静态数据被配置与一个 class object 之内。静态数据成员、静态和非静态函数成员被被放在class 之外。而虚函数有两个步骤处理 每一个class产生一堆指向虚函数的指针,放在表格之中,这个表格称为 虚表 每一个class object 被添加了一个指针,指向相关的 虚表,通常这个指针被称为虚指针。虚指针的设定和重置都由class 的构造、析构和拷贝运算符自动完成。此外 class的 type_info 也被放在 虚表中 单例模式完全实现123456789101112131415161718192021222324252627282930313233343536373839404142434445464748// 线程不安全的单例class Singleton{private: Singleton(){}; static Singleton* m_instance;public: static Singleton* getInstance(){ if(m_instance==NULL){ m_instance = new Singleton(); } reutrn m_instance; }} Singleton* Singleton::m_instance = NULL;// 线程安全单例// 懒汉模式class Singleton{private: Singleton(){}; static Singleton* m_instance;public: static Singleton* getInstance(){ if(m_instance==NULL){ lock(); if(m_instance==NULL){ m_instance = new Singleton; } unlock(); } return m_instance; }}Singleton* Singleton::m_instance = NULL;//饿汉模型class Singleton{private: Singleton(){}; Static Singleton* m_instance;public: static Singleton* getInstance();}Singleton* Singleton::m_instance = new Singleton();Singleton* Singleton::getInstance(){ reutrn m_instance;} C++11 智能指针的设计实现1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980 1 #include <iostream> 2 #include <memory> 3 4 template<typename T> 5 class SmartPointer { 6 private: 7 T* _ptr; 8 size_t* _count; 9 public:10 SmartPointer(T* ptr = nullptr) :11 _ptr(ptr) {12 if (_ptr) {13 _count = new size_t(1);14 } else {15 _count = new size_t(0);16 }17 }18 19 SmartPointer(const SmartPointer& ptr) {20 if (this != &ptr) {21 this->_ptr = ptr._ptr;22 this->_count = ptr._count;23 (*this->_count)++;24 }25 }26 27 SmartPointer& operator=(const SmartPointer& ptr) {28 if (this->_ptr == ptr._ptr) {29 return *this;30 }31 32 if (this->_ptr) {33 (*this->_count)--;34 if (this->_count == 0) {35 delete this->_ptr;36 delete this->_count;37 }38 }39 40 this->_ptr = ptr._ptr;41 this->_count = ptr._count;42 (*this->_count)++;43 return *this;44 }45 46 T& operator*() {47 assert(this->_ptr == nullptr);48 return *(this->_ptr);49 50 }51 52 T* operator->() {53 assert(this->_ptr == nullptr);54 return this->_ptr;55 }56 57 ~SmartPointer() {58 (*this->_count)--;59 if (*this->_count == 0) {60 delete this->_ptr;61 delete this->_count;62 }63 }64 65 size_t use_count(){66 return *this->_count;67 }68 };69 70 int main() {71 {72 SmartPointer<int> sp(new int(10));73 SmartPointer<int> sp2(sp);74 SmartPointer<int> sp3(new int(20));75 sp2 = sp3;76 std::cout << sp.use_count() << std::endl;77 std::cout << sp3.use_count() << std::endl;78 }79 //delete operator80 }","categories":[],"tags":[{"name":"后台开发","slug":"后台开发","permalink":"http://yoursite.com/tags/后台开发/"}]},{"title":"HTTP复习","slug":"HTTP复习","date":"2019-02-17T11:59:06.000Z","updated":"2019-02-17T12:02:51.746Z","comments":true,"path":"2019/02/17/HTTP复习/","link":"","permalink":"http://yoursite.com/2019/02/17/HTTP复习/","excerpt":"HTTP 复习协议是计算机通信网络中两台计算机之间进行通信所必须共同遵守的规定或规则。HTTP协议是一种详细规定了浏览器和万维网服务器之间相互通信的规则,通过因特网传送万维网文档和数据的协议。HTTP协议可以使浏览器更加高效的运行,使网络的传输效率更高。它不仅保证计算机快速正确的传输超文本文档,还确定哪一部分先显示。 HTTP由于其灵活简单快速的特点,应用非常广泛。浏览器网页是HTTP主要的应用,但其不只是用于网页浏览。只要通信的双方都使用他就可以通信。比如QQ软件就用到了HTTP协议。","text":"HTTP 复习协议是计算机通信网络中两台计算机之间进行通信所必须共同遵守的规定或规则。HTTP协议是一种详细规定了浏览器和万维网服务器之间相互通信的规则,通过因特网传送万维网文档和数据的协议。HTTP协议可以使浏览器更加高效的运行,使网络的传输效率更高。它不仅保证计算机快速正确的传输超文本文档,还确定哪一部分先显示。 HTTP由于其灵活简单快速的特点,应用非常广泛。浏览器网页是HTTP主要的应用,但其不只是用于网页浏览。只要通信的双方都使用他就可以通信。比如QQ软件就用到了HTTP协议。 HTTP工作流程在网路的七层模型中,HTTP在应用层也就是传输层以上,其基于TCP协议。 HTTP的默认端口号为80, 而HTTPS的默认端口号是443 HTTP是基于传输层的TCP协议,而TCP是一个端到端面向连接的协议。所谓的端到端可以理解为进程到进程的通信,所以HTTP在开始传输之前,首先要建立TCP连接,而TCP连接的过程需要三次握手。在TCP的三次握手后,建立了TCP连接。此时HTTP就可以进行传输了。一个HTTP操作称为一个事务,可以分为以下几步: 首先客户机与服务器需要建立连接。只要单机某个超级链接,HTTP工作即开始 建立连接后,客户机发送一个请求给服务器,请求的格式为:同一资源标识符(URL),协议版本号,后面是MIME信息,包括(请求修饰符、客户机信息和可能的内容) 服务器接到请求后,给与一个相应的响应信息,其格式为一个状态行、包括信息的协议版本号、一个成功或错误的状态码、后面是MIME信息(服务器信息、实体信息、可能的内容) 客户端接受服务器返回的信息通过浏览器返回在用户显示屏上,然后客户机与服务器断开连接。 如果上述几步中某一步出现错误,那么产生的错误信息返回客户端,由显示屏输出。HTTP协议永远都是客户端发起请求,服务器响应。 HTTP 协议结构HTTP 协议无论是请求报文还是回应报文,都包含以下四个部分。 报文头: GET http://www.baidu.com HTTP/1.1 0个或多个请求头 Accept-Language: en 空行 可选的消息体 HTTP协议是基于行的协议,每一行以\\r\\n 为分隔符。报文头通常表明报文的类型,且报文头只占一行;请求头附带一些特殊信息,每一个请求头占一行 name:value HTTP请求方法HTTP/1.1 协议中定了 9 种方法来表明Request-URL 指定资源的不同的操作方式 OPTIONS 返回服务器针对特定资源的HTTP请求方法 HEAD 向服务器索要与GET请求一致的相应,而响应体不会被返回 GET 向特定资源发出请求(GET可能会被爬虫随意访问) POST 向指定资源提交数据处理的请求(提交表单、上传文件)数据被包含在请求体中。POST可能会导致新建资源或已有资源的修改 PUT 向指定资源上传最新内容 DELETE 请求服务器删除 REQUEST_URL 所标识的资源 TRACE 回显服务器收到的请求 CONNECT 预留给连接方式改为管道的代理服务器 PATCH 局部修改某一资源 当请求的资源不支持请求方法时, 服务器返回405(Method Not Allowed)服务器不认识或不支持方法时, 返回501(Not Implemented) 常见的请求头Host:(发送请求,请求头是必须的)主要用于执行被请求资源的Internet主机和端口号,它从HTTP的URL中提取出来, Connection: 它的值通常有两个,keep-alive 和close。 HTTP 是一个请求响应模式的典型范例,即客户端向服务器发送一个请求信息,服务器来响应这个信息。在之前的HTTP版本中,每个请求被创建一个新的到服务器端的连接,在这个连接上发送请求,然后接受请求。keep-alive被用来解决效率了低的问题。keep-alive 使客户端到服务端的连接持续有效,当出现了后续请求时, keep-alive避免了建立或者重新建立。对于市面上大部分服务器keep-alive都被支持,但是对于负担较重的网站,保留的客户端会影响性能。 Accept:浏览器可以接受的MIME类型。 Accept:text/html, 如果不能接受将返回406错误 Cache-control: 指定请求和响应遵循缓存机制。缓存指令是单向的,且独立。 Accept-Encoding:浏览器生命可接受的编码方法,通常说明是否支持压缩。 Accept-Language:浏览器声明接受语言 Accept-Charset:浏览器可以接受的字符集,默认任何字符集都可以接收 User-agent:用于告诉HTTP服务器,客户端使用操作系统浏览器的名称和版本。 HTTP回应报文第一行是报头,第一个字段表明HTTP协议版本们可以直接以请求报文为准;第二个字段是status code, 也就是返回码,相当于请求结果 HTTPS在网络上,两个实体间的通信容易被窃听。 当你学习网络安全后、你发现网络再也不安全了 初级的防御手段,是双方都约定一个加密的算法,把加密后的数据进行传输,收到数据方再进行解密。这里涉及一个概念,对称加密和非对称加密 对称加密:对称加密是指加密秘钥和解密秘钥是一样的,通常有AES和TEA加密算法,它的特点是计算量小,有又一定的破解门槛。 非对称加密:加密的秘钥和解密的秘钥不一样,秘钥成对出现。加密解密使用不同的秘钥(公钥加密需要私钥解密,私钥加密需要公钥加密)它的特点是计算量大,常用的有RSA、ECC等算法。基于性能考虑,一般使用非对称加密得到秘钥, 再用对称秘钥进行加密。 HTTP协议可以轻松抓包并获取协议,是不安全的协议。而HTTPS是安全为目标的HTTP通道,简单来说是HTTP的安全版。 TLSTLS协议可用于保护运行于TCP之上的任何协议通信,如HTTP、FTP 等更高级的协议。最常见的是通过TLS来保护HTTP通信。 主要过程如下所述 客户端浏览器向服务器传送TLS协议的版本号、加密算法种类、产生的随机数以及其他信息 服务器向客户端传送TLS版本号、加密算法的种类、随机数及其他信息 客户端利用服务器传送的信息验证服务器的合法性,服务器的合法性包括:整数过期、CA是否可靠、发型整数是否能解开数字签名,如果验证不能通过,服务将断开;否则继续下一步 用户随机产生一个对称密码,然后用公钥进行加密,并传给服务器 【A】如果服务器要求客户端验证身份,用户生成一个随机数并签名一起传送 【A】如果服务器要求验证客户端身份,服务器检验客户整数和签名随机数的合法性。 服务器和客户端使用相同的主密码,即“通话密码”。 客户端向服务器发送信息,指明后续的通话使用7中的密码为对称秘钥 服务器相客户端发送信息,指明7中的密码为对称秘钥,握手结束 这样TLS的握手就结束了,TLS的安全通道数据通信开始,客户和服务器开始使用相同的秘钥进行通信。 这样基于TLS的HTTPS也有了这样的特点1。 客户端产生的秘钥只有客户端和服务器能得到 2 加密的数据只有客户端和服务器能得到明文 3 客户端到服务器的通信是安全的。 HTTP和HTTPS有以下区别 HTTPS协议需要CA申请证书,需要交费 HTTP是超文本传输协议,明文传输, HTTPS是ssl加密传输 80 和443 HTTP连接很简单,无状态, HTTPS经过SSL+HTTP协议构建的,加密传输、身份认证。 HTTPS耗性能,安全性要求低用HTTP CGICGI 是HTTP 中重要的技术之一,有着不可替代的作用。CGI是一个web服务器的标注接口。通过CGI接口Web服务器就能获取客户端提交的信息转交给服务器端的CGI程序处理,最后结果返回给客户端。 服务器和客户端之间的通信,是浏览器和服务端web服务器的HTTP通信,所以只需知道浏览器执行哪个CGI程序即可。","categories":[],"tags":[{"name":"计算机网络","slug":"计算机网络","permalink":"http://yoursite.com/tags/计算机网络/"}]},{"title":"进程","slug":"进程","date":"2019-02-15T13:48:56.000Z","updated":"2019-02-15T13:50:41.383Z","comments":true,"path":"2019/02/15/进程/","link":"","permalink":"http://yoursite.com/2019/02/15/进程/","excerpt":"进程程序与进程进程结构一般由三部分组成:代码段、数据段和堆栈段。代码段用于存放程序代码数据,数个进程可以共享一个代码段。而数据段存放程序的全局变量、常量和静态变量。堆栈段中栈用于函数调用,它存放着函数参数、函数内部定义的局部变量。对斩断还包含了进程控制块(PCB)。PCB处于进程核心堆栈底部,不需要额外分配空间。PCB是进程存在的唯一标识。系统通过PCB的存在而感知进程的存在。 程序如何转换为进程? 一般情况下linux下C++的生成分为四个阶段:预编译、编译、汇编、连接。编译器g++经过预编译、编译、汇编3个步骤将源程序将文件转换为目标文件。最后形成可执行程序。当程序执行时,操作系统将可执行程序复制到内存中 1 内核将程序将程序读入内存、为程序分配内存空间 2 内核为进程分配进程标识符(PID)及其他资源 3 内很为进程保存 PID 及其他信息,把进程放入运行队列中等待执行,程序转化为进城后就可以被调度执行。","text":"进程程序与进程进程结构一般由三部分组成:代码段、数据段和堆栈段。代码段用于存放程序代码数据,数个进程可以共享一个代码段。而数据段存放程序的全局变量、常量和静态变量。堆栈段中栈用于函数调用,它存放着函数参数、函数内部定义的局部变量。对斩断还包含了进程控制块(PCB)。PCB处于进程核心堆栈底部,不需要额外分配空间。PCB是进程存在的唯一标识。系统通过PCB的存在而感知进程的存在。 程序如何转换为进程? 一般情况下linux下C++的生成分为四个阶段:预编译、编译、汇编、连接。编译器g++经过预编译、编译、汇编3个步骤将源程序将文件转换为目标文件。最后形成可执行程序。当程序执行时,操作系统将可执行程序复制到内存中 1 内核将程序将程序读入内存、为程序分配内存空间 2 内核为进程分配进程标识符(PID)及其他资源 3 内很为进程保存 PID 及其他信息,把进程放入运行队列中等待执行,程序转化为进城后就可以被调度执行。 进程的创建与结束进程创建有两种方式:一种由操作系统创建,一种由父进程创建 系统启动时, 操作系统会创建一些进程,他们承担管理和分配系统资源的任务,这些进程被称为系统进程。系统允许一个进程创建新进程(子进程),子进程还可以创建新的进程,形成树型结构。整个linux的所有进程也是一个树形结构。树根由系统自动构造,即内核态下的0号进程,它是所有进程的祖先。0号进程创建1号进程,负责执行内核部分的初始化及系统配置,并创建告诉缓存和虚拟储存管理的内核线程。随后1号进程调用 execve() 运行可执行程序 init , 并演化成用户态的1号进程 它按照init/initab 的要求完成启动工作,创建若干终端注册进程getty() 用于用户的登录。 进程的创建 – forklinux系统允许任何一个用户创建子进程,创建成功后,子进程在系统中并独立于父进程。该子进程接受系统调度,与父进程有相同的权利。 12@include <unistd.h>pid_t fork(void); 父进程和子进程:除了0号进程,linux系统中其他任何一个进程都是其他进程创建的。而相对的 ,fork 函数的调用方是父进程, 而创建的新进程是子进程。 fork 函数不需要参数,返回值是一个进程标识符 1 对于父进程,fork函数返回创建子进程ID 2 子进程 fork 函数返回0 3 创建出错的话 fork 函数返回 -1 fork函数创建一个新的进程,并从内核中为其分配一个可用的进程标识符PID,之后为其分配进程空间,并将父进程空间的内容中复制到子进程空间, 包括数据段和堆栈段,和父进程共享代码段。这时候系统中多了一个进程父进程和子进程都接受系统的调度,fork函数返回两次(分别在父进程和子进程中返回)。 12345678910111213141516#include <stdio.h>#include <stdlib.h>#include <unistd.h>int main(void){ pid_t pid; pid = fork(); if(pid<0){ perror(\"fail to fork\"); exit(-1); }else if(pid ==0){ printf(\"Subprocess, PID: %u\", getpid()); }else{ printf(\"Parentprocess, PID: %u\", getpid()); } return 0;} 子进程和父进程共享数据段和堆栈段中的内容。例子略 事实上,子进程完全复制了父进程的地址空间,包括堆栈段和数据段。但是,子进程并未复制代码段,而是公用代码段。 进程的结束 – exit僵尸进程在linux中 正常情况下子进程是通过父进程创建的, 子进程和父进程的运行是一个异步的过程。父进程无法预料子进程在何时结束,于是就产生了孤儿进程和僵尸进程。 孤儿进程,是指一个父进程退出后,它的一个或多个子进程还在运行,那么这些子进程将成为孤儿进程。孤儿进程将被init进程(进程号为1) 到进程所收养,并由init进程完成状态收集工作。 僵尸进程,是指一个进程使用fork创建子进程,如果子进程退出,而父进程没有用wait或waitpid调用子进程的状态信息,子进程的进程描述符仍在系统中,这种进程被称为僵尸进程。 简单理解为,孤儿是父进程已退出而子进程未退出;而僵尸进程是父进程未退出而子进程先退出。 为了避免僵尸进程,需要父进程通过wait函数来回收子进程 守护进程linux系统中在系统引导时会开启很多服务,这些服务就叫做守护进程。为了增加灵活性,root可以选择开启的模式,这些模式叫做运行级别。守护进程是脱离于终端在后台运行的进程,守护进程脱离终端是为了避免进程在执行过程中在终端上显示并且不会被终端的信息打断。 守护进程是一个生存期较长的进程,通常独立于控制终端并且周期性的执行某种任务或等待处理某些发生的事件。说话进程常常在系统引导装入时启动,linux系统有很多的守护进程,大多数服务都是通过守护进程实现的。如作业规划进程、打印进程。 在 linux 中每一个与用户交流的界面称为终端,每一个终端开始的进程都会依附于该终端,这个终端就被称为 进程的控制终端,当控制终端被关闭时,相应的进程都会被自动关闭。但是守护进程可以突破这种限制,它从被执行时开始运转,整个系统关闭时才推出。如果想让某个进程不因为用户或终端等变化受到影响,那么就需把一个进程变成一个守护进程。 创建一个守护进程的步骤如下所示: 1 创建子进程,父进程退出。 这是编写守护进程的第一步。由于守护进程脱离终端控制,因此在第一步完成后就会在终端里造成程序已经运行完毕的假象。之后所有的工作都在子进程完成,而与用户终端脱钩。 2 子进程中创建会话 这个步骤是创建守护进程最重要的一步,虽然他的实现十分简单,但是他的意义重大。这里使用的系统函数setid, 这里有两个概念:进程组和会话期 1) 进程组: 一个或多个进程的集合。进程组由进程组ID来唯一标识,除了进程号以外,进程组ID也是一个进程的必备属性,每个进程组都有一个组长进程,组长进程号等于进程组ID,而且进程组ID不会因为组成进程的退出而受到影响。 2) 会话周期: 会话期是一个或多个进程组的集合。通常一个会话开始于用户登录,终止于用户退出,再次期间运行的所有进程都输入这个会话期。 setid 函数用于创建一个新的会话,并担任该会话组的组长,调用 setid 有三个作用: 1 让进程摆脱原会话的控制 2 让进程摆脱原进程组的控制 3 让进程摆脱原控制终端的控制。 那么 创建守护进程为什么需要setid函数? 这是由于创建守护进程的第一步掉用了fork函数来创建子进程,再将父进程退出。由于调用fork函数时子进程全盘拷贝了父进程的会话期、进程组、控制终端等,虽然父进程退出了,但是会话期、进程组、控制终端等还没有改变,所以还不是真正意义上的独立。 3) 改变当前目录为根目录 这一步骤也是必要的步骤,使用 fork 创建的子进程集成了父进程的工作目录,由于进程的运行过程中当前的目录是不能卸载的,这对于以后的使用造成诸多麻烦。通常的做法是将 “/” 变为守护进程的当前工作目录,这样就可以避免上述问题。 4) 重设文件权限掩码 文件权限掩码是指屏蔽掉文件权限中的对应位。 5) 关闭文件描述符 同文件权限码一样, 用fork函数创建的子进程会从父进程哪里继承一些已经打开的文件,这些文件节能永远不会被守护进程读写,但是他们一样消耗系统资源。所以需要关闭来自继承的文件描述符。 1234567891011121314151617181920212223242526272829303132333435363738#include <stdio.h>#include <stdlib.h>#include <string.h>#include <fcntl.h>#include <unistd.h>#include <sys/wait.h>#include <sys/types.h>#include <sys/stat.h>#define MAXFILE 65535int main(){ pid_t pc; int i, fd, len; char* buf = \"this is a Dameon\\n\"; len = strlen(buf); pc = fork(); if(pc<0){ printf(\"error fock\\n\"); exit(1); }else if(pc>0){ exit(0); } setsid(); chdir(\"/\"); umask(0); for(int i=0; i<MAXFILE; i++){ close(i); } while(1){ if(fd = open('/temp/demeon.log',O_CREAT|O_WRONLY|O_APPEND,0600))<0){ perror(\"open\"); exit(1); } write(fd, buf, len+1); close(fd); sleep(10); } return 0;}","categories":[],"tags":[{"name":"操作系统","slug":"操作系统","permalink":"http://yoursite.com/tags/操作系统/"}]},{"title":"多线程编程(C++)","slug":"多线程编程-C","date":"2019-01-31T03:54:56.000Z","updated":"2019-01-31T03:56:40.868Z","comments":true,"path":"2019/01/31/多线程编程-C/","link":"","permalink":"http://yoursite.com/2019/01/31/多线程编程-C/","excerpt":"多线程早期计算机只允许一个程序独占资源,一次只能执行一个程序。计算力是一种宝贵的资源。 这种背景下,多程序并发执行的需求十分迫切,由此产生了进程的概念。进程在多数早期操作系统中是执行工作的基本单元。进程是包含程序和资源的集合,每个程序与其他程序一起参与调度,竞争CPU、内存等系统资源。每次进程切换都存在资源的保存和恢复,这被称为上下文切换。进程的引入解决了多用户支持的问题,但是产生了新的问题:进程频繁切换引起的额外开销严重影响系统性能。进程通信要求复杂的系统级实现。 例如一个简单的GUI任务,通常一个任务支持界面交互、一个任务支持后台运算。如果每个任务都由一个进程来实现会相当的低效。对每一个进程来说,系统资源看上去都是独占的,比如内存空间。这样演化利用分配给统一个进程实现多个任务的方法。同一个进程内部的线程共享进程的所有资源。共享这些内存空间,比如定义个全局变量,A将其赋值为1,B看到这个变量也是1。线程很方便的支持了进程内部的并发,避免了频繁切换的开销。","text":"多线程早期计算机只允许一个程序独占资源,一次只能执行一个程序。计算力是一种宝贵的资源。 这种背景下,多程序并发执行的需求十分迫切,由此产生了进程的概念。进程在多数早期操作系统中是执行工作的基本单元。进程是包含程序和资源的集合,每个程序与其他程序一起参与调度,竞争CPU、内存等系统资源。每次进程切换都存在资源的保存和恢复,这被称为上下文切换。进程的引入解决了多用户支持的问题,但是产生了新的问题:进程频繁切换引起的额外开销严重影响系统性能。进程通信要求复杂的系统级实现。 例如一个简单的GUI任务,通常一个任务支持界面交互、一个任务支持后台运算。如果每个任务都由一个进程来实现会相当的低效。对每一个进程来说,系统资源看上去都是独占的,比如内存空间。这样演化利用分配给统一个进程实现多个任务的方法。同一个进程内部的线程共享进程的所有资源。共享这些内存空间,比如定义个全局变量,A将其赋值为1,B看到这个变量也是1。线程很方便的支持了进程内部的并发,避免了频繁切换的开销。 多线程一个程序的运行中,只有一个控制权存在。但函数被调用时,该函数获得控制权成为激活函数,各个函数像是连在一条线上,计算机流水线执行操作,这样叫做单线程序。 多线程就是允许一个进程存在多个控制权,同时有多一个函数处于激活状态。单线程中函数会被压栈,只有栈顶函数被调用,而多线程则会在内存中存在多个栈。 多线程的创建与结束线程的创建与结束 123#include <pthread>int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine) (void *), void *arg); pthread_t 实际上就是 unsigned long int . 第一个参数指向线程标识符,第二个参数设置线程属性,第三个参数是线程运行函数的起始地址, 最后一个参数是运行函数的参数。 若创建成功则返回0, 否则返回错误号。 123456789101112131415161718192021222324252627282930313233343536#include <stdio.h>#include <pthread.h>#include <string.h>struct arg_type{ int a; char b[100];};void* say_hello(void* args){ arg_type arg_temp = *(arg_type*)args; printf(\"hello from thread, info: %d, %s\\n pthread=%lu\\n\",arg_temp.a, arg_temp.b, pthread_self()); pthread_exit((void*)1);}int main(){ pthread_t tid; arg_type arg_temp; arg_temp.a = 10; char temp[100] = \"To be number one\"; strncpy(arg_temp.b, temp, sizeof(temp)); int iRet = pthread_create(&tid, NULL, say_hello, &arg_temp); if(iRet){ printf(\"pthread_create error\"); return iRet; } printf(\"Thread id in process: %lu\\n\", tid); void *retval; iRet = pthread_join(tid, &retval); if(iRet){ printf(\"pthread_join error\"); return iRet; } printf(\"retavl\\n\"); return 0;} 线程的属性线程有一组属性可以在线程被创建时指定,该属性封装在一个对象中,对象的类型为pthread_attr_t 属性值不能直接设置,必须使用相关的函数操作,这个函数必须在 pthread_create 之前调用并通过pthread_attr_destory释放,主要属性包括:作用域、栈尺寸、栈地址、优先级、分离状态、调度策略和参数。 POSIX.1 之定义一系列属性,主要如下表 分离状态:若线程终止,线程处于分离状态,系统不保留线程的终止状态;当不需要线程的终止状态时,可以分类线程。 栈地址:设置和获取线程的栈地址 栈大小:系统中有很多线程时,可能需要减少每个线程栈的默认大小,防止进程的地址空间不够用。 栈保护区大小:在线程顶留出一段空间,防止栈溢出。 线程优先级,新线程的默认优先级是0 继承父进程的优先级:新线程不继承父进程的优先级 调度策略 争用范围 线程并行级别,POSIX 标准定义了3中调度策略:先入先出策略(FIFO)、循环策略(RR)和自定义策略(OTHER)。FIFO是基于队列的调度程序,对于每个优先级使用不同的队列。 FIFO :如果继承具有有效的用户ID为0,则争用范围为系统的先入先出属于实时调度类,如果这些线程未被更高级的线程抢占,则会继续处理该线程,直到线程放弃或阻塞为止。 多线程同步多线程相当于一个并发系统,一般通知执行多个任务,如果多个任务可以共享资源,就需要解决同步问题。 线程火车票 1234567891011121314151617181920212223242526272829303132333435363738#include <stdio.h>#include <pthread.h>#include <unistd.h>int total_ticket_num = 20;void *sell_ticket(void *arg){ int id = *(int*)arg; for(int i=0; i<20; i++){ if(total_ticket_num > 0){ sleep(1); printf(\"from id: %d, sell the tikect %dth ticket\\n\", id, 20-total_ticket_num+1); total_ticket_num--; } } return 0;}int main(){ int iRet; pthread_t tids[4]; int i = 0; for(int i=0; i<4; i++){ sleep(1); int iRet = pthread_create(&tids[i], NULL, &sell_ticket, &i); if(iRet){ printf(\"pthread_create error, iRet=%d\\n\",iRet); return iRet; } } sleep(1); void *retval; for(int i=0;i<4;i++){ iRet = pthread_join(tids[i], &retval); if(iRet){ printf(\"pthread_join error, iRet=%d\\n\",iRet); return iRet; } printf(\"retval=%ld\\n\", (long)retval); } return 0;} 事实上,如果只有一个线程执行上面的程序,没有问题。但是如果多个线程都执行就会出现问题。其根本原因在于各个线程都可以对 total_ticket_num 进行写入。 这里if会判断是否有剩余票,如果有则卖。但是不同的线程之间会存在时间窗口,其他线程可能在这个窗口进行卖票操作,导致卖票的条件不成立,但是线程已经进行了判断,所以无法知道 total_ticket_num 发生了变化。 在并发的情况下,指令的先后顺序由内核决定。同一个线程内部指令按照先后顺序执行,但不同的线程很难说哪一个先执行。如果运行的结果依赖于不同线程执行的先后顺序,就会造成竞争条件。这样条件下计算机的计算结果未知。所以应该避免竞争条件的形成。 对于多线程程序来说,同步是指在某一定时间内只允许一个线程访问资源。可以通过互斥锁、条件变量、读写锁和信号量来同步资源。 同步锁互斥锁 是一个特殊的变量,他有lock和unlock两个状态。互斥锁一般被设置为全局变量。打开的互斥锁可以由某个线程获得,一旦获得互斥锁会被锁上,只有该线程有权打开。 火车票系统的互斥系统可以被这样表示 12345678910111213141516171819202122232425262728293031323334353637383940414243#include <stdio.h>#include <stdlib.h>#include <string.h>#include <pthread.h>#include <unistd.h>pthread_mutex_t mutex_x = PTHREAD_MUTEX_INITIALZER;int total_ticket_num = 20;void *sell_ticket(void *arg){ int id = *(int*)arg; for(int i=0; i<20; i++){ pthread_mutex_lock(&mutex_x); if(total_ticket_num > 0){ sleep(1); printf(\"from id: %d, sell the tikect %dth ticket\\n\", id, 20-total_ticket_num+1); total_ticket_num--; } pthread_mutex_unlock(&mutex_x); } return 0;}int main(){ int iRet; pthread_t tids[4]; int i = 0; for(int i=0; i<4; i++){ sleep(1); int iRet = pthread_create(&tids[i], NULL, &sell_ticket, &i); if(iRet){ printf(\"pthread_create error, iRet=%d\\n\",iRet); return iRet; } } sleep(1); void *retval; for(int i=0;i<4;i++){ iRet = pthread_join(tids[i], &retval); if(iRet){ printf(\"pthread_join error, iRet=%d\\n\",iRet); return iRet; } printf(\"retval=%ld\\n\", (long)retval); } return 0;} 第一个执行pthread_mutex_lock()的线程会首先获得mutex_x ,其他线程必须等待,直到第一个线程释放后,其他线程才可以获得锁并继续执行。 条件变量互斥是线程程序必备的工具,但是并非万能。例如,如果线程等待共享数据某个条件出现,它可能重复对互斥对象锁定和解锁,每次都会检查共享数据结构,这样查询效率很低。 每次检查之间,可以使线程短暂的进入睡眠,但是这样无法立即响应。真正需要的一种方法是:当线程满足某些条件时使得线程进入睡眠状态,一旦条件满足就唤醒睡眠的线程。这正是条件变量的任务。 条件变量通过允许线程阻塞和等待另一个线程信号方法弥补互斥锁的不足,他常常和互斥锁一起使用。使用时条件变量被用于阻塞一个线程,一旦某个线程改变了条件变量,它将通知相应的条件变量唤醒一个或多个阻塞的线程,这些线程会重新测试是否满足条件。 条件变量例子 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849#include <iostream>#include <pthread.h>#include <stdlib.h>#include <unistd.h>using namespace std;pthread_cond_t qready = PTHREAD_COND_INITIALIZER;pthread_mutex_t qlock = PTHREAD_MUTEX_INITIALIZER;int x = 10;int y = 20;void *func1(void* args){ cout<<\"func1 start\"<<endl; pthread_mutex_lock(&qlock); while(x<y){ pthread_cond_wait(&qready, &qlock); } pthread_mutex_unlock(&qlock); sleep(3); cout<<\"func1 end\"<<endl;}void *func2(void* args){ cout<<\"func2 start\"<<endl; pthread_mutex_lock(&qlock); x = 20; y = 10; cout<<\"has change x and y\"<<endl; pthread_mutex_unlock(&qlock); if(x>y){ pthread_cond_signal(&qready); } cout<<\"func2 end\"<<endl;}int main(){ pthread_t tid1, tid2; int iRet; iRet = pthread_create(&tid1, NULL, func1, NULL); if(iRet){ cout<<\"pthread 1 create error\"<<endl; return iRet; } sleep(2); iRet = pthread_create(&tid2, NULL, func2, NULL); if(iRet){ cout<<\"pthread 1 create error\"<<endl; return iRet; } sleep(10); return 0;} 读写锁某些程序存在读者写者问题,对某些资源的访问可能有两种情况,一种是排他性的,必须独占,这被称为写操作;一种是可以共享的,被称为读操作。 读写锁比互斥锁具有更高的适应性与并行性,可以有多个线程同时占用读写锁,但是只有一个线程占用写模式的读写锁。 当读写锁是 写加锁 状态时,其他试图加锁的程序都会被阻塞 当读写锁在 读加锁状态是,所有读模式加速线程可以被授权,但是写模式加锁会被阻塞 读写锁在 读加锁 模式时,如果有试图写加锁的请求,后续的读模式会被阻塞, 以避免长期的读模式占用,而 等待写模式的请求则长期阻塞。 读写锁最适用于对数据 读操作多于写操作的场合,应为读模式可以共享,而写模式只能由某个线程独占,因而读写锁也叫 共享-独占锁。 处理读者、写者问题两种常见的步骤是强读者同步和强写者同步。强读者同步给于读更高的优先权,强写着同步给与写更高的优先权。航班订票–强写者同步 图书馆查询–强读者同步 读写者例子 123456789101112131415161718192021222324252627282930313233343536373839404142#include <stdio.h>#include <pthread.h>#include <stdlib.h>#include <unistd.h>#define THREADNUM 5pthread_rwlock_t rwlock;void *readers(void *args){ pthread_rwlock_rdlock(&rwlock); printf(\"reader %ld got the lock\\n\", (long)args); pthread_rwlock_unlock(&rwlock); pthread_exit((void*)0);}void *writers(void *args){ pthread_rwlock_wrlock(&rwlock); printf(\"reader %ld got the lock\\n\", (long)args); pthread_rwlock_unlock(&rwlock); pthread_exit((void*)0);}int main(){ int iRet, i; pthread_t writer_id, reader_id; pthread_attr_t attr; int nreadercount = 1, nwritercount = 1; iRet = pthread_rwlock_init(&rwlock, NULL); if(iRet){ printf(\"ERROR\"); return iRet; } pthread_attr_init(&attr); pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED); for(int i=0; i<THREADNUM; i++){ if(i%3){ pthread_create(&reader_id, &attr, readers, (void*)nreadercount); printf(\"create reader %d\\n\", nreadercount++); }else{ pthread_create(&reader_id, &attr, writers, (void*)nwritercount); printf(\"create writer %d\\n\", nwritercount++); } } sleep(5); return 0;} 信号量线程还可以通过信号量通信,信号量和互斥锁的区别: 互斥锁只允许 一个线程进入临界区,而信号量允许多个线程进入临界区,原理同互斥锁 多线程重入前面介绍的各种同步方式,其实就是为了解决“函数不可重入”问题。所谓“可重入”函数是指多于一个任务并发使用而不必担心错误的函数。而不可重入函数只能由一个函数独占,除非确保函数互斥。 可重入函数有以下特点 不为连续调用持有静态数据 不返回指向静态的指针 所有的户数都由函数的调用者提供 使用本地数据,或者通过制作全局数据的本地副本来保护全局数据 如果必须访问全局变量,利用互斥锁、信号量来保护全局变量 绝不调用任何不可重用函数 不可重用函数有以下特点 函数中存在静态变量,无论是全局还是局部静态变量 函数返回静态变量、 函数中掉用了不可重用函数 函数体使用了静态数据结构 函数体掉用了malloc或free函数 函数体内掉用了其他标准的IO函数","categories":[],"tags":[{"name":"操作系统","slug":"操作系统","permalink":"http://yoursite.com/tags/操作系统/"}]},{"title":"网络IO模型","slug":"网络IO模型","date":"2019-01-17T13:25:57.000Z","updated":"2019-01-17T13:28:02.439Z","comments":true,"path":"2019/01/17/网络IO模型/","link":"","permalink":"http://yoursite.com/2019/01/17/网络IO模型/","excerpt":"网络IO模型IO是计算机体系中重要的一部分。IO有两种操作,同步IO和异步IO。同步IO是指必须等待IO操作完成后控制权才能返回给用户进程。异步IO指的是,无需等待IO操作完成,就将控制权返回给用户进程。 网络中的IO常见如下情况: 输入操作:等待数据到达套接字接受缓冲区 输出操作:等待套接字发送缓存区有足够的空间容纳将要发送的数据 服务器接受连接请求:等待新的用户请求到来 客户端发送连接请求:等待服务器送回客户SYN对应的ACK","text":"网络IO模型IO是计算机体系中重要的一部分。IO有两种操作,同步IO和异步IO。同步IO是指必须等待IO操作完成后控制权才能返回给用户进程。异步IO指的是,无需等待IO操作完成,就将控制权返回给用户进程。 网络中的IO常见如下情况: 输入操作:等待数据到达套接字接受缓冲区 输出操作:等待套接字发送缓存区有足够的空间容纳将要发送的数据 服务器接受连接请求:等待新的用户请求到来 客户端发送连接请求:等待服务器送回客户SYN对应的ACK 4种IO模型1. 阻塞IO模型在Linux中,默认所有的socket都是阻塞的。 阻塞和非阻塞的概念描述的是用户线程调用内核IO的操作方式:阻塞是指IO操作彻底完成后才返回到用户空间,非阻塞是指IO调用后立即返回给用户一个状态值。 当进程调用了recvform这个系统调用后,系统内核开始了IO的第一阶段,准备数据。对于网络IO来说,很多数据在一开始没到达时(还没收到一个完整的TCP包) 系统内核等待数据的到来。而用户进程整个会被阻塞。阻塞IO模型的特点就是在 IO执行的两个阶段(等待数据、拷贝数据)被阻塞。 大部分socket接口都是阻塞型的。所谓阻塞型是指系统调用不返回结果,让当前线程一直阻塞。这给网络编程带来了一个重要的问题,如果调用send()时,线程处于阻塞状态,无法响应任何网络请求。 一个简单的改进方案是在服务器端使用多线程。多线程目的是让每个链接都拥有独立的线程。这样一个阻塞的连接不会影响其他连接。传统意义上进程开销远大于线程,所以客户端较多时多线程,单个服务占用资源较多选择安全的多进程。pthread_create()创建新线程,fork() 创建新进程 在socket设计之初,一个句柄就可以被accept()多次。 1int accept(int fd, struct sockaddr *addr, socklen_t *addrlen) 调用accept正是从 请求队里中抽出第一个连接信息,创建一个与fd同类的新socket返回句柄。如果当前没有请求,accept便会阻塞至有新的请求进入。 上述多线程的服务器模型似乎完美解决了多个客户机应答的需求,但是并不是这样。这主要是因为相应成千上百路的需求对于多进程、多线程都会严重占用系统资源。即使是考虑到线程池和连接池。 线程池旨在降低创建和销毁线程的频率,维护一定数量的线程,并让空闲的线程重新承担新的执行任务。 连接池位置连接的缓存池,尽量重用已有的连接,降低创建和关闭连接 的频率。 这样两种方法广泛应用于大型系统,然而池终有上限,当请求大大超过上限时,池效果并不好。现实中面临上千、上万次的用户请求,多次按成模型会遇到瓶颈。 2. 非阻塞IO模型Linux下可以设置socket使其为非阻塞状态。 如图所示,当用户发出read操作时,如果内核数据还没准备好,它不会block用户进程而是返回一个错误。从用户进程角度讲,read后不需要等待而是得到一个结果。当用户进程判断其为错误时,就知道它还没准备好,这样便可以再次read操作。 所以非阻塞IO中,用户需要不断询问kernel是否准备好数据。 1fcnt1(fd, F_SETFL, O_NONBLOCK); 在非阻塞状态下recv()被调用后立即返回。 recv()大于0表示数据接收完毕,返回接收字节数。0表示断开;-1表示没完成或系统错误。 实际操作系统提供了更高效的接口,例如 select() 多路复用 3. 多路IO复用模型多路IO复用有时也被称为事件驱动IO,它的基本原理是有一个函数(select)不断的轮询所有的(socket),当某个socket数据到达了,就通知用户进程。 当用户调用了select,那么整个进程会被阻塞,而同时内核会监视所有的socket,当某一个socket数据准备好了,select就会返回。这个时候用户进程再进行read操作将数据从内核拷贝到用户进程。 这个模型其实和阻塞IO没有太大的区别,事实上还更冗余,因为需要调用两个系统调用(select、recvfrom)而阻塞IO只需要调用一个 recvfrom 即可。但是select优势在于它可以处理多个连接。多以如果连接数并不高的情况下,select、epoll 不一定比阻塞IO的性能更好。 多路IO复用中,每一个socket一般都设置为非阻塞,而整个用户进程其实是被阻塞的,只不过进程是被select这个进程阻塞,而不是被socket IO阻塞。 这种模型的特征在于每个周期探测一次或一组事件,一个特定的时间会出发某个特定的响应,也被称为“事件驱动模型”。相比其他模型,select() 事件只用单线程执行、占用资源少,不消耗太多CPU资源,同时能为多客户端提供服务。 但是这个模型有很多问题,首先select() 接口本身需要消耗时间去轮询句柄,很多操作系统提供了更方便的接口 Linux 是 epoll BSD提供了 kqueue 4. . 异步IO模型 当用户发起read操作后,立刻就去其他工作;另一个方面,内核收到一个请求后会立刻返回。然后内核等待数据准备完成,然后拷贝到用户内存中,再向用户进程发送一个信号。 调用阻塞IO会一直阻塞IO直到操作完成,而非阻塞IO在内核准备数据的情况下就会立刻返回。二者的区别在于同步IO进行IO时会阻塞进程。按照这个定义,阻塞IO、非阻塞IO、多路IO复用都属于同步IO。 selectselect函数是socket编程中一个重要的函数,可以完成非阻塞工作程序,可以监视文件描述符的变化情况。 1int select(int maxfdp, fdp_set *readfds, fd_set *writefds, fd_set *errorfds, struct timeval* timeout); 这里涉及到两个结构体 fd_set 和 timeval 这个里fd_set理解为一个集合,存放文件描述符,及文件句柄。当然UNINX下任何设备、管道、FIFO都是文件形式,所以socket 就是一个文件。","categories":[],"tags":[{"name":"计算机网络","slug":"计算机网络","permalink":"http://yoursite.com/tags/计算机网络/"}]},{"title":"TCP API","slug":"TCP-API","date":"2019-01-17T13:22:03.000Z","updated":"2019-01-17T13:25:26.173Z","comments":true,"path":"2019/01/17/TCP-API/","link":"","permalink":"http://yoursite.com/2019/01/17/TCP-API/","excerpt":"TCP 网络编程API网络通信概要网络进程如何通信?首要解决如何标识一个进程,本地可以用PID来解决,而网络中用IP+端口号来唯一标识一个进程。这样利用(IP、端口号、PID)可以唯一标识一个网络中的进程。 socket起源于UNINX,UNINX哲学之一就是一切皆文件,有一个打开、读写、关闭的模式来操作。socket就是该模式的一个实现,socket就是一种特殊的文件。 使用TCP/IP的协议应用程序采用应用编程接口 UNINX BSD关键字来实现网络进程通信,目前几乎所有的应用程序都是使用socket,网络通信无处不在。其基本模式包括:","text":"TCP 网络编程API网络通信概要网络进程如何通信?首要解决如何标识一个进程,本地可以用PID来解决,而网络中用IP+端口号来唯一标识一个进程。这样利用(IP、端口号、PID)可以唯一标识一个网络中的进程。 socket起源于UNINX,UNINX哲学之一就是一切皆文件,有一个打开、读写、关闭的模式来操作。socket就是该模式的一个实现,socket就是一种特殊的文件。 使用TCP/IP的协议应用程序采用应用编程接口 UNINX BSD关键字来实现网络进程通信,目前几乎所有的应用程序都是使用socket,网络通信无处不在。其基本模式包括: 服务器根据地址类型(IPV4、IPV6)创建socket 服务器为socket绑定IP地址和端口号 服务器socket监听端口号请求,随时准备接受客户端的连接,这时候服务器socket并未打开 客户端创建socket 客户端打开socket,根据服务器IP和端口号试图连接服务器socket 服务器socket接收到客户端socket请求,被动打开开始接受客户端请求,直到客户端返回连接信息。这时候socket进入阻塞状态 客户端连接成功,向服务器发送连接状态信息 服务器accept方法返回,连接成功。 服务器向socket写入信息 服务器读取信息 客户端关闭 服务器关闭 仔细研究会发现,服务器和客户端连接的部分就是三次握手。 网络编程API1. socket函数1int socket(int domain, int type, int protocol) socket 函数对应于普通文件的打开操作,普通文件返回一个描述字,而socket创建一个socket描述符,它唯一标识一个socket。 当调用socket时, 返回的描述他存在与协议族空间中,但是没有具体地址,如果想要赋予地址需要使用bind()函数。如果不绑定,系统会在使用时随机生成一个。 2. bind函数bind() 函数把一个地址族中特定的地址赋给socket 1int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen) 3. listen 和 connect 函数这两个函数相对应,服务器调用listen函数来监听端口,而客户端调用connect函数去发出连接请求 12int listen(int sockfd, int backlog)int connect(int sockfd, const struct sockaddr *addr, docklen_t addrlen) 4. accept 函数TCP 服务器依次调用socket()、bind()、listen()之后,就会监指定的socket地址了。而TCP客户端调用socket()、connect()之后就会向TCP服务器发送一个连接请求。TCP连接接收到这个请求之后,就会调用accept()函数接受请求,这样连接就建立了。 1int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen) 注意:accept第一个参数为socket描述字,服务器调用socket生成的,称为监听socket关键字;而accept返回的是已连接socket关键字。一个服务器通常只创建一个监听socket描述字,这个描述子在服务器生命周期一直存在。内核为每个服务器接受的客户创建一个已连接socket关键字,当完成服务后就会关闭。 5. read 和 write 函数read()函数从fd中读取内容,write写入内容。成功时返回字节数 6. close函数1int close(int fd); TCP 协议选项1. SO_REUSEADDR一般来说一个端口释放后,大约两分钟才能再次使用(处于TIME_WAIT状态),而使用该选项可以使端口被释放后立即被使用(处于TIME_WAIT状态的端口)。 2. TCP_NODELAY/TCP_CHORK网络拥塞领域,有一个非常著名的算法叫做Nagle算法, 这是以其发明人的名字命名的。John Nagle 首次用该算法解决福特公司的网络拥塞问题。该问题具体描述是:如果应用程序每次产生1Byte的数据,而这个1Byte的数据又以数据报的形式发送给网络服务器,那么很容易使得网络过载。所以传送1Byte的包却要花费40Byte的包头(IP20字节、TCP20字节)这种有效载荷利用低下的情况被称为愚蠢窗后症候群。 针对这问题,Nagle算法改进:如果发送少量字符包(小于MSS的包被称为小包,大于MSS的包被称为大包)发送端只会发送第一个小包,将后面的小包缓存起来。直到接收到前一个数据报的ACK为止,或当前字符较紧急,积攒了较多的数据。 TCP中Nagle算法默认启用,但是不使用任何情况。而TCP_NODELAY/TCP_CHORK字段控制了包的Nagle化。例如TCP_NODELAY便是直接把包发出去,这样用户体验会更好。 3. SO_LINGERlinger是延缓的意思,这里的延缓指close操作。默认close立即返回,但是当缓冲区还有部分数据时,系统会尝试将数据发送给对方。 4. TCP_DEAFER_ACCPET实际上是接收到第一个数据包后,才会创建连接 5.SO_KEEPALIVESO_KEEPALIVE用来检测对方主机是否崩溃,避免服务器永远阻塞于TCP连接的输入 设置该选项后,如果2h内任何一方没有数据交换,TCP就会自动向对方发送一个保持存活探测, 对方接受正常,以ACK回应 对方已崩溃且重新启动,以RST响应,套接口错误置为restart,套接口本身被关闭 对方无响应,再次发送8个探测分节,11min15s 后无响应就放弃。 网络字节序与主机序关于字节序再次讨论。不同的CPU有不同的字节序类型,这些字节序是整数在内存中的保存顺序,称为主机序。最常见的两种 1 小端, 低序的字节存在起始位置 2 大端 , 高字节的存在起始位置 小端法 地址低位存在值的低位,高位存值的高位。这种方式符合人的思维方式。 大端法 地址低位存高位的值。它很直观不需要考虑对应关系,只需要内存地址写出去即可 为什么要注意字节问题呢?C++编译平台的储存是由编译平台的CPU确定的,而JAVA编写的程序唯一采用大端法。所有网络协议都是大端法。其也被称为网络字节序。 封包和解包TCP是一个流协议,所谓流就行没有界限的一串数据。但是通信程序是需要独立的数据包发送。 设想这样几种情况 先接收到data1 后接到 data2 先接收到data1 的部分数据,后接到data1的余下部分与 data2全部 先接收到了data1全部数据和部分data2数据,后接受到data余下数据 一次接受data1和data2全部数据 1是理想情况 而2、3、4即使粘包的情况,这时就需要拆包将受到的数据拆成独立的数据包。这种情况可能有这么几种原因。 1 由Nagle算法造成的粘包 2 接收端不及时接受造成的粘包 具体的解决办法便是封包和拆包。封包是给一段数据加上包头,这样数据就分为包头、包体两部分。包头包含了一个结构体成员变量表示了包体的长度,这样另一方便可以通过这个长度进行拆包;","categories":[],"tags":[{"name":"计算机网络","slug":"计算机网络","permalink":"http://yoursite.com/tags/计算机网络/"}]},{"title":"编译与调试(C++)","slug":"编译与调试(C-)","date":"2019-01-03T14:07:57.000Z","updated":"2019-01-03T14:15:34.969Z","comments":true,"path":"2019/01/03/编译与调试(C-)/","link":"","permalink":"http://yoursite.com/2019/01/03/编译与调试(C-)/","excerpt":"[TOC] 编译g++ helloworld.cpp 命令被分为四个步骤,预处理、编译、汇编、链接 编译与链接 预处理首先是源码相关和头文件,如iostream被预处理其处理成一个.i 文件,第一步相当于该命令 1g++ -E helloworld.cpp -o hellword.i 其中-E表示只进行预编译,直接输出预编译结果 预处理命令主要处理以#开头的预编译命令,如#include #define 将所有的#define删除, 展开所有的宏定义 处理所有的条件编译指令 #if、#elif、#else、endif 处理#include预编译指令,将文件插入指定位置 过滤注释内容 添加行号标识,一遍编译器调试时产生行号信息 保留所有的#progama指令","text":"[TOC] 编译g++ helloworld.cpp 命令被分为四个步骤,预处理、编译、汇编、链接 编译与链接 预处理首先是源码相关和头文件,如iostream被预处理其处理成一个.i 文件,第一步相当于该命令 1g++ -E helloworld.cpp -o hellword.i 其中-E表示只进行预编译,直接输出预编译结果 预处理命令主要处理以#开头的预编译命令,如#include #define 将所有的#define删除, 展开所有的宏定义 处理所有的条件编译指令 #if、#elif、#else、endif 处理#include预编译指令,将文件插入指定位置 过滤注释内容 添加行号标识,一遍编译器调试时产生行号信息 保留所有的#progama指令 编译编译过程就是将文件进行一列的词法分析、语法分析、语义分析及优化产生汇编代码文件,这往往是程序最复杂的部分 1g++ -S helloworld.i -o helloworld.s 编译器将高级语言翻译成机器语言的一个工具,编译过程大致有以下几部分 链接每个源代码模块独立的编译,然后按要求组装起来,这个过程就是链接。链接的主要部分就是把各个模块之间的部分处理好,使得各个模块可以正确的衔接。 最基本的链接过程如图所示,每个模块的源代码文件经过编译器编译成目标文件,目标文件和库一起链接成可执行文件。 库其实就是一组文件的包,一些代码编译成目标文件后打包存放。 每个目标文件除了自己的数据和二进制代码外,还提供三个表,未解决符号表、导出符号表、地址重定向表 未解决符号表 编译单元里引用但是定义不在本编译单元定义 导出符号表 对本单元定义,愿意给其他单元使用的符号和地址 地址重定向表 本编译单元对自身地址的引用 extern 声明置入未解决符号表,不置入导出符号表 static 属于内部链接 静态链接链接分为静态链接和动态链接。对函数库的链接放在编译使其完成的 是静态链接。所有相关的目标文件与涉及到的函数被链接合成一个可执行文件。程序运行时与函数库毫无关系。因为所有需要的函数已经被复制到相应位置,这些函数被称为静态库,通常命名为libxxx.a 动态链接除了静态链接、也可以把一些库函数链接载入推迟到运行使其,这就是动态链接库。动态库文件名规范和静态库类似,动态库的扩展名为so, 静态链接和动态链接的区别 动态链接库利于进程资源共享 当某个程序在运行中调用某个动态链接库函数时,操作系统会在内存中寻找库函数的拷贝。这样虽然带来一些动态链接的开销,却大大节省了内存资源。C语言标准库就是动态链接库,所有程序共享一个C语言标准库的代码段,而静态链接库则不同,系统中每个静态链接库都需要拷贝到自己的代码段,这样更耗费内存。 程序的升级更简单 如果库发生变化,使用库需要重新编译。使用动态库,只要动态库的接口没变,只需要更换动态库 链接载入程序员控制 由于静态库是编译时,库函数装到程序中,静态库会快一些 调试strace系统调用 为了创建文件、进程、复制文件、应用程序必须和操作系统交互。但是应用程序不能直接访问linux内核。它不能访问内核的内存、也不能调用内核函数。这样就需要操作系统上的内建函数,被称为系统调用 strace通过跟踪系统调用让开发知道程序在后台做的事 strace查看系统调用 gdbgdb 是 gcc 的调试工具 1自定义随心所欲的运行程序 2 可以让被调试的程序在断点停住 3 程序停住时可以检查程序的运行状态 4 动态改变程序环境 TOP PSLinux中 ps列出当前的进程快照, kill 用于杀死进程 linux上进程有5种状态 运行 R(正在运行或队列中等待) 中断 S(休眠、受阻、在等待某个条件的形成或接受信号) 不可中断 D(收到信号不唤醒、不可运行) 僵死 Z(进程已终止,但是进程描述符仍存在,知道父进程调用wait4()释放) 停止 T(进程收到信号后停止运行) valgrindvalgrind 是一套linux下开放源代码的仿真调试工具,它模拟了一个CPU环境,并给其他服务提供插件 Memcheck 这是运用最广泛的工具,一个重量级的内存检查器,发现大多数的内存错误使用情况。 callgrind 收集程序的运行数据,建立函数调用关系图 cachegrind 检查程序中缓存出现的问题,模拟CPU的一级、二级缓存 Helgind 检查多线程的竞争问题 寻找多个线程访问而没有一贯加锁的区域。 Massif堆栈分析器,测量堆、栈的大小 Extension 。。。 Linux 程序内存空间布局下图是一个典型的内存空间布局 代码段 通常指存放程序执行代码的一块内存区域。这部分大小在程序运行前已经确定,并且内存区域只读,某些架构也允许可写 初始化数据段,存放程序中已初始化的全局变量 未初始化数据段, 未初始化全局变量的一块区域 堆 堆用于储存程序运行时动态分配的内存段,它的大小不确定,可以动态的扩张或缩减,程序调用malloc及free来动态分配内存,当进程调用malloc、free时,新配的内存被动态添加到堆上或删去 栈 存放程序的局部变量,并且用户函数调用的传参和返回 堆栈的区别1 申请方式不同 栈: 系统自动分配,声明在函数中一个局部变量;系统自动在栈中为其分配空间 堆: 需要程序员自己申请。并指明大小 2 申请后系统的相应不同 栈: 只要栈的剩余空间大于申请空间,系统便提供内存,否则报异常 堆: 操作系统有一个记录空闲内存的链表,系统受到申请会遍历链表去找一块内存,从空间链表中删去,并将剩下的空间再放回去。 3 申请大小的限制不同 栈: 栈向低地址扩展,是一块连续的区域,栈顶的地址和栈的最大容量是系统规定好的 堆: 堆是低地址向高地址扩展,不连续的内存区域 4 申请效率不同 栈由系统自动分配,速度较快,程序员无法控制 堆是由new分配、速度慢,容易产生碎片,但是方便 5 堆栈存储内容不同 栈: 函数调用时,第一个进栈的是主函数中的下一条指令,然后是各个参数。 堆: 一般在堆的头部用一个字节存放大小,堆中的内容程序员安排","categories":[],"tags":[{"name":"C++","slug":"C","permalink":"http://yoursite.com/tags/C/"}]},{"title":"TCP(二轮复习)","slug":"TCP-二轮复习","date":"2019-01-03T13:48:07.000Z","updated":"2019-01-03T14:07:18.514Z","comments":true,"path":"2019/01/03/TCP-二轮复习/","link":"","permalink":"http://yoursite.com/2019/01/03/TCP-二轮复习/","excerpt":"网络模型1 七层网络模型国际标注化组织ISO于1981年 正式推荐了一个网络体系结构–七层参考模型,也被叫做开放系统互连模型 这七层网络模型在传输过程中还会对数据进行封装,过程如图所示 在ISO七层网络模型中,当一台主机需要传送用户数据时,数据先进入应用层。在应用层中,数据被加上应用层报头(AH),形成应用层协议数据单元(PDU),然后被递交到表示层。表示层不关心上层应用数据格式而是把整个数据包看成一个整体(应用层数据)进行封装,及加上表示层报头(PH)。下层分别加上自己的报头,其中数据链路层还会封装一个链尾,形成一帧数据。 当一帧数据通过物理层传输到目标主机物理层时,主机递交到数据链路层,同样经历上述相反的过程一层一层解包拿到数据。","text":"网络模型1 七层网络模型国际标注化组织ISO于1981年 正式推荐了一个网络体系结构–七层参考模型,也被叫做开放系统互连模型 这七层网络模型在传输过程中还会对数据进行封装,过程如图所示 在ISO七层网络模型中,当一台主机需要传送用户数据时,数据先进入应用层。在应用层中,数据被加上应用层报头(AH),形成应用层协议数据单元(PDU),然后被递交到表示层。表示层不关心上层应用数据格式而是把整个数据包看成一个整体(应用层数据)进行封装,及加上表示层报头(PH)。下层分别加上自己的报头,其中数据链路层还会封装一个链尾,形成一帧数据。 当一帧数据通过物理层传输到目标主机物理层时,主机递交到数据链路层,同样经历上述相反的过程一层一层解包拿到数据。 2 五层网络模型大学教科书一般采用了一个五层的网络模型 1 应用层 : 确定进程之间的通信性质以满足用户需求。 2 运输层:负责主机间不同进程的通信。即TCP与UDP 3 网络层:负责分组交换不同主机间的通信 IP数据报 4 数据链路层:负责将网络层的IP数据报组装成帧 5 物理层:透明的传输比特流 3 四层网络模型不论是七层还是五层,都是学术上的概念,实际应用的网络模型是一个四层的TCP/IP模型。 TCP|IP 被组织成四个概念层,其中三个与ISO中模型对应。TCP、IP协议簇不包含链路层和物理层,其需要与其他协议协同工作。 网络接口层 网络接口层包括用于IP数据在已有网络介质上的传输协议。实际上TCP、IP并不定义与ISO链路物理层相对应的功能,相反它定义了ARP这样的协议,提供TCP、IP与硬件间的接口 网间层 网间层对应网络层,本层包含IP、RIP协议,负责数据包装、寻址、路由 传输层 网间层对应OSI参考模型的传输层,它提供两种端到端的通信服务。其中TCP提供可靠的数据流传输服务,UDP提供不可靠的数据报服务。 应用层 应用层对应应用层和表示层。 TCP头部 TCP头部里每一个字段都为管理TCP连接和控制起了重要作用 16位端口号:告知报文段来自哪里,传到哪里。进行TCP通信时,客户端通常使用临时端口号,服务器则使用知名端口号 32位序号:一次TCP通信(TCP连接建立到断开)过程中传输字节流中字节的标号,序号值被初始化为某随机值,之后该传输方向上TCP报文的序号值加上该随机值的偏移 32位确认号:用作对一方发来TCP报文段的相应。其值是TCP报文段值加1 4位头部长度 标识TCP头部有多少(4字节),这样TCP首部最大是60字节 6位标志位包含如下几项 1)URG 紧急指针是否有效 2)ACK 确认号是否有效 3)PSH 提示接收端立即从缓冲区读走数据 4)RST 表示要求重新建立连接 5) SYN 标识请求开始一个连接 (同步报文段) 6)FIN标志 表示通知对方要关闭连接了 (结束报文段) 16位窗口大小 TCP控制流量的手段,接受窗口(告诉对方TCP缓冲区还能容纳的字节数) 16位验校和 检验TCP报文段在传输过程中是否损坏 16位紧急指针 正偏移量 有几点需要注意 TCP包没有IP地址,这属于网络层、一个TCP连接需要4个元组(略)来保证是同一连接。 SequenceNubmer是包序号,来解决网络包乱序问题ACK用于确认不丢包 Window是滑动窗口用来解决流控问题 TCP状态流转首先了解注明的三次握手与四次挥手 TCP建立连接 1) 第一次握手:建立连接时,客户端发送SYN包(SYN=J)到服务器,并且进入SYN_SEND状态等待服务器确认 2) 第二次握手:服务器收到SYN包,确认客户端的SYN(ACK=J+1)同时也发送一个自己的SYN包(SYN=K)即SYN+ACK包,服务器进入SYN_RECV状态 3) 第三次握手:客户端收到SYN+ACK包,向服务器发送确认包ACK(ACK=K+1)发送完毕客户端和服务器进入ESTABLISHED状态 TCP断开连接 TCP有个特殊的概念叫半关闭,这个概念是说TCP连接是全双工连接,因此关闭连接必须关闭两个方向上的连接。客户机给服务器发送一个FIN的TCP报文,然后服务器回一个ACK报文,并且发送一个FIN报文,之后客户端再回ACK就结束了。 在建立连接的时候,通信双方要确认最大报文长度,一般这个SYN是MTU减去IP和TCP的首部长度,对于以太网一般可以达到1460字节,当然非本地IP只有536字节,中间传输的MSS更小的话这个值更小 2MSL 等待状态 (TIMEWAIT)发送了最后一个ACK后,防止最后一次的数据报没有达到对方那里。这个状态保证了双方都可以正常结束,由于socket(IP和端口对)使得应用程序在这个时间(2MSL)无法使用同一个服务。这对于客户端无所谓,但是服务程序(httpd)总要用一个端口来服务。而这个时间,启动httpd会出现错误(插口被使用)。为了避免这个错误,服务器给出了平静时间的概念,虽然可以重新启动服务器,但是要平静的等待2MSL才能进行下一次连接 FIN_WAIT_2 状态 这是注明的半关闭状态,这个状态应用程序还可可以接受数据但是不能发送数据。还有一种可能是 客户端一直 FIN_WAIT_2 ,服务器一直 CLOSE_WAIT 状态 直到应用层来关闭这个状态 RST 同时打开和同时关闭,概率很小 TCP超时重传本节讨论异常网络状况下,TCP如何控制数据保证其承诺的可靠服务 数据顺利到对端,对端顺利响应ACK 数据包中途丢失 数据包顺利达到,但是ACK报文丢失 数据报数据达到,但是对异常未响应 出现这些异常情况时,TCP就会超时重传。TCP每发一个报文段,就对这个报文段设一个计时器,如果确认的时间到了而没有收到确认,就会重传报文段,这被称为超时重传 利用tcpdump调试出以下信息 可以看出重传时间为2、4、8、6(可能是被终止了) 客户端一直没有得到ACK报文,客户端会一直重传,影响重传效率的是RTO。RTO指发送数据后,传送数据等待ACK的时间。RTO非常重要。 设长了,重发慢,没有效率 设短了,重发快,网络拥塞 如果底层传输特性已知,则重传相对简单,但是TCP体层完全异构,因此TCP必须适应时延差异。 RFC973规定了经典的自适应算法 12SRTT = α * SRTT + (1 - α) * RTTRTO = min(UBOUND, max(LBOUND,β * SRTT)) 但是这个算法有个问题,主要是由于ACK传输导致的RTT多义性问题 1987年出现了一种carn算法, 忽略重传,不采样重传的RTT。 该算法规定,一但发生重传,就对现有的RTO翻倍。 当不发生重传时,才根据上式计算平均往返时间RTT和重传时间 TCP滑动窗口TCP滑动窗口主要有两个作用 1. 提供TCP的可靠性 2. 提供TCP的流控特性 同时滑动窗口还体现了TCP面向字节流的设计 对于TCP会话的发送方,任何时候其缓存数据可以分为四类: 已经发送并受到对方的ACK 已经发送但未收到ACK 未发送但是对方允许发送 不允许发送 其中,已经发送还未收到ACK和未发送但是对方允许发送的部分称为发送窗口 当对方接收到ACK后续的确认字节时,便会滑动 对于TCP的接收方,某一时刻其有三种状态 1 已接收 2 未接受准备接受 3 未准备接受 TCP是双工的协议,会话的双方可以同时接受、发送数据。TCP会话双方都各自维护一个发送窗口和接收窗口。滑动窗口实现面向流的可靠性来源于“确认重传机制”,TCP滑动窗口的可靠性也来源于确认重传。发送窗口只有收到对方对于本段ACK的确认,才会移动左边界。前面还有字节未接受的情况下,窗口不会移动。 TCP拥塞控制计算机中带宽、交换节点中的缓存、处理机都是网络资源。某段时间,网络需求超过了可用部分,网络性能就会变坏,这被称为拥塞。拥塞控制就是防止过多的网络流量注入到网络中。TCP拥塞控制由四个核心算法组成:慢开始、拥塞避免、快速重传和快速恢复 慢开始和拥塞避免发送方维持一个拥塞窗口的状态变量,拥塞窗口取决于网络的拥塞程度,发送方让自己的发送窗口等于拥塞窗口 慢开始的思路就是一开始不发送大量的数据,先探测网络的拥塞程度。由小至大的增加拥塞窗口 当主机发送数据时,如果将较大的发送窗口全部注入到网络中,可能引起拥塞 可以试探一下,由小至大增大拥塞窗口的数量 (这里用报文段说明,实际上拥塞窗口以字节为单位) 慢开始从1开始指数增长,为了防止其增长过大, 设置一个门限,当其达到门限时,变为拥塞避免算法 拥塞避免算法是使得拥塞窗口缓慢增长,每经过一个RTT就将 拥塞窗口加一 TCP连接初始化,拥塞窗口设为1 执行慢开始算法,cwind指数增长,直到cwind=ssthress时,开始拥塞避免算法 当网络拥塞时, 将ssthress设为当前的一半,cwind重新设为1 开始 快重传和快恢复快重传要求接收方收到一个失序的报文段后立即发出重复确认(为使发送方尽早知道报文段未传到对方)快重传规定只有一连收到3个重复确认就立即重传对方尚未收到的报文段,而不必继续等待。 快重传还配合有快恢复,主要思想包括 一旦收到三个重复确认,执行乘法减小,ssthresh门限减半,但是并不执行慢开始 将cwind设为ssthresh, 然后执行拥塞避免算法 整体上,TCP拥塞窗口的原则是加法增大、乘法减小。可以看出TCP较好的保证了流之间的公平性,一旦丢包就减半退让。","categories":[],"tags":[{"name":"计算机网络","slug":"计算机网络","permalink":"http://yoursite.com/tags/计算机网络/"}]},{"title":"STL剖析(vector,string)","slug":"STL剖析(vector-string)","date":"2019-01-03T13:28:39.000Z","updated":"2019-01-03T13:37:05.846Z","comments":true,"path":"2019/01/03/STL剖析(vector-string)/","link":"","permalink":"http://yoursite.com/2019/01/03/STL剖析(vector-string)/","excerpt":"STL概论 长久以来软件届一直希望建立一种可复用的东西,以及一种得以造出“可重复运用东西”的方法。 子程序、程序、函数、类别、函数库、类别库、组件、结构模块化设计、模式、面向对象 … 都是为了 复用性的提升 复用性必须建立在某种标准之上,但是在许多环境下开发最基本的算法和数据结构还迟迟不能有标准。大量程序员从事重复劳动,完成前人完成而自己不拥有的代码。 为了建立数据结构算法的标准,降低耦关系,提升独立性、弹性,交互操作性,诞生了STL","text":"STL概论 长久以来软件届一直希望建立一种可复用的东西,以及一种得以造出“可重复运用东西”的方法。 子程序、程序、函数、类别、函数库、类别库、组件、结构模块化设计、模式、面向对象 … 都是为了 复用性的提升 复用性必须建立在某种标准之上,但是在许多环境下开发最基本的算法和数据结构还迟迟不能有标准。大量程序员从事重复劳动,完成前人完成而自己不拥有的代码。 为了建立数据结构算法的标准,降低耦关系,提升独立性、弹性,交互操作性,诞生了STL stringstring类的实现是基础的题目,首先给出string的原型 123456789101112class String{public: String(const char* str = NULL); //普通构造函数 String(const String &other); //拷贝构造函数 ~String(); //析构函数 String & operator = (const String &other); //重载运算符 String & operator + (const String &other); //重载运算符 bool operator == (const String &other); //重载运算符 int getLength(); //得到长度private: char *m_data;} 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748String::String(const char* str){ if (str == NULL) { m_data = new char[1]; *m_data = '\\0'; } else { int length = strlen(str); m_data = new char[length+1]; strcpy(m_data, str); }}String::~String(){ if (m_data) { delete[] m_data; m_data = 0; }}String::String(const String &other){ if (!other.m_data){ m_data = 0; } m_data = new char[strlen(other.m_data)+1]; strcpy(m_data, other.m_data);}String& String::operator = (const String &other){ if(this != &other){ delete[] m_data; if(!other.m_data){ m_data = 0; } else { m_data = new char[strlen(other.m_data)+1]; strcpy(m_data, other.m_data); } }}String & String::opreator+(const String &other){ String newString; if(!other.m_data){ newString = *this; }else if(m_data){ newString=other; }else{ newString.m_data = new char[strlen(m_data)+strlen(other.data)+1]; strcpy(newString.m_data,m_data); strcat(newString.m_data,other.m_data); } return newString;} vectorvector 是线性容器,它的元素线性排列,和动态数组类似。它的元素储存在一块连续的空间中,这意味着他不仅可以用迭代器访问、还可以通过指针偏移访问,vector能自动储存元素 下标访问个别元素 不同的方式遍历元素 容器末尾增加、删除元素 12345// 迭代器vector<int>::iterator iter;for(it = con.begin(); it!=con.end(); it++){ cout<<*it<<endl;} 关于STl容器,只要不超过最大值,其可以自动增长到足以容纳用户放进去的数据大小。(这个容量值,可以同过调用maxsize()来获得) 对于vector和string,如果需要更多的空间,类似realloc思想增长,vector支持随机访问,因此提高了效率,内部通过动态数组实现。 123456789101112131415161718//vector 查找vector<int>::iterator iter;iter = find(vec.begin(),vec.end(),3);//vector 删除vector<int>::iterator iter;while(iter!=vec.end()){ if(*iter==target){ iter = erase(iter); }else{ iter++; }}//vector 增加iterator insert(iterator loc, const TYPE &val);void insert(itrator loc, size_type num, const TYPE &val);void insert(itrator loc, itrator start, itrator end); vector 的内存管理1,使用reverse() 提前设定容量大小 STL最令人称赞的特性之一就是只要不超过最大值,其可以自动增长。如果需要更多空间就会类似realloc的思想来增长大小。vector容器支持随机访问,因此效率较高。 如果有大量的元素需要push_back,应提前使用reverse函数提前设定,避免多次的扩容操作,介绍一下只有vector与string提供的几个函数 函数名 介绍 size() 获得容器中的元素个数,但不能获得容器为他分配的容器大小 capacity() 获得容器已经分配的内存容纳多少元素。 resize() 用来强制把容器改为容纳n个元素。如果n小于当前大小,尾部元素会被销毁;如果n大于当前大小,默认构造元素添加到元素尾;如果大于当前容量,触发重新分配 reverse() 强制把容量改为不小于n,n不小于当前大小 交换技巧来修整内存 swap强行释放内存 12345template<class T> void VlearVector(vector<T>& v){ vector<T> vTemp; vTemp.swap(v);} vector类简单实现第一版是这本书的,感觉实现的不好,没有体现出空间配置器的作用,只写了构造函数 1234567891011121314151617181920212223242526template<typename T>class myVector{private: #define WALK_LENGTH 64;public: // 构造函数 myVector():array(0),theSize(0),theCapcity(0){} myVector(unsigned int n, const T& t):array(0),theSize(0),theCapacity(0){ while(n--){ push_back(t); } } // 拷贝构造函数 myVector(const myVector<T>& other):array(0),theSize(0),theCapcity(0){ *this = other; } // 重载赋值运算符 myVector<T>& opreator = (myVector<T>& other){ if(this == &ohter) return *this; clear(); theSize }} 第二版参考《stl源码》 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103template<class T, class Alloc=alloc>class vector{public: typedef T value_type; typedef value_type* pointer; typedef value_type* iterator; typedef value_type& reference; typedef size_t size_type; typedef ptrdiff_t difference_type;protected: typedef simple_alloc<value_type, Alloc> data_allocator; iterator start; iterator finish; iterator end_of_storage; void insert_aux(iterator posotion, const T& x); void deallocate(){ if(start) data_allocator::deallocate(start,end_of_storage-start); } void fill_initialize(size_type n, const T& vlaue){ start = allocate_and_fill(n, value); finish = start + n; end_of_storage = finish; }public: iterator begin(){return start;} iterator end(){return finish;} size_type size() const {return size_type(end()-begin());} size_type capacity() const{return size_type(end_of_storage-begin());} bool empty() const {return engin()==end()} reference operator[](size_type n){return *(begin()+n);} vector():start(0),finish(0),end_of_storage(0){} vector(size_type n,const T& value){fill_initialize(n,value);} vector(int n,const T& value){fill_initialize(n,value);} vector(long n,const T& value){fill_initialize(n,value);} explicit vector(size_type n) {fill_initlialize(n,T());} ~vector(){ destroy(start,finish); deallocate(); } reference front(){ return *begin();} reference back() { return *(end()-1);} void push_back(const T& x){ if(finish!=end_of_storage){ construct(finish, x); ++finish; }else insert_aux(end(),x); } void pop_back(){ --finish; destory(finish); } iterator erase(iterator position){ if(position+1 != end()) copy(position+1,finish,position); --finish; destory(finish); return position; } void resize(size_type new_size, const T& x){ if(new_size<size()) erase(begin()+new_size(),end()); else insert(end(),newsize()-size(),x); } void resize(size_type new_size){resize(new_size,T());} void clear(){erase(begin(),end());}protected:iterator allocate_and_fill(size_type n, const T& x){ iterator result = data_allocatpr:: }}template<class T, class Alloc>void vector<T, Alloc>::insert_aux(iterator position, const T& x){ if(finish != end_of_storage){ construct(finish,*(finish-1)); ++finish; T x_copy = x; copy_backword(position, finish-2,finish-1); *position = x_copy; }else{ const size_type old_size = size(); const size_type len = old_size != 0?2*oldsize:1; iterator new_start = data_allocator::allocate(len); iterator new_finish = new_start; try { new_finish = uninitialized_copy(start,posotion,new_start); construct(new_finish, x); ++new_finish; new_finish = uninitialized_copy(positon,finish,new_finish); } catch(...){ destroy(new_start,new_finish); data_allocator::deallocate(new_start,len); thow; } destroy(begin(),end()); deallocate(); start = new_start; finish = new_finish; end_of_storage = new_start+len; }} map、set map的本质 map本质是一类关联式容器,属于模板类关联的本质在于元素的值与某个特点的键相关联,而并非通过元素在数组的位置获取。它的特点是增加和删除节点对迭代器的影响很小,除了要操作的节点,对其他节点都没什么影响。对迭代器来数不能修改键值,只能修改实值。map内部自建一颗红黑树(非严格意义的平衡二叉树),该树内部数据有序 map的功能 自动建立Key-value的一一对应关系,map<int, string> mapStudent; 红黑树源码略(不研究了) hashtablehashtable被视为一种字典结构,提供对于任何有名项的存取操作和删除操作。 如何避免array过大?是用某种映射函数,使得元素映射至“大小可以接受的索引”,这个函数被称为散列函数。使用散列函数必然会带来一个问题:可能有不同的元素被映射到相同的位置,这便是所谓的碰撞问题。 碰撞问题一般有两种方案:拉链法、线性探测法 线性探测提出一个名词:负载系数,元素个数除以表格大小。负载系数永远在0~1之间, 当 散列函数 计算某个位置,而该位置不可用该怎么办? 最简单的方法就是循序接着往下找,只要表格足够大总会找到一个空间。而删除必须采取惰性删除(标记删除序号,实际删除待表格整理时再进行) 两个假设 1.表格足够大,2 每个元素独立 但实际情况往往不乐观,往往需要不断解决碰撞问题 二次探测二次探测主要用来解决 主集团问题, 该方法描述很简单,碰撞时尝试 H+1*1,H+2*2, H+ 3*3来代替H+1, H+2,H+3 等。幸运的是,假设表格大小为质数,永远保持0.5以下的负载系数,每个新元素的插入查找不大于2 另外,二次探测有个简便的计算技巧 12345hi = h0 + i\\*i (MOD M)hi-1 = h0 + (i-1) *(i-1) (MOD M)整理得,hi = hi-1 + 2*i-1 (MOD M) 这样每一次迭代并不需要太大的计算资源。 开链拉链法,在《源码》中被称为开链,及对于冲突的元素维护一个链表","categories":[],"tags":[{"name":"STL","slug":"STL","permalink":"http://yoursite.com/tags/STL/"}]},{"title":"C++常用编程技术","slug":"C-常用编程技术","date":"2018-12-23T07:42:36.000Z","updated":"2018-12-23T07:43:36.676Z","comments":true,"path":"2018/12/23/C-常用编程技术/","link":"","permalink":"http://yoursite.com/2018/12/23/C-常用编程技术/","excerpt":"[TOC] C++常用编程技术函数 函数定义 函数重载 C++允许同一函数名定义多个函数,但这些函数必须参数个数不同或类型不同,这就是函数重载 123456789int min (int a, int b) { return a < b ? a:b;}int min (long long a, long long b) { return a < b ? a:b;}int min (int a, int b, int c) { // something}","text":"[TOC] C++常用编程技术函数 函数定义 函数重载 C++允许同一函数名定义多个函数,但这些函数必须参数个数不同或类型不同,这就是函数重载 123456789int min (int a, int b) { return a < b ? a:b;}int min (long long a, long long b) { return a < b ? a:b;}int min (int a, int b, int c) { // something} 函数模板 函数模板建立一个通用函数,其函数类型和形参不具体指定而是用一个虚拟的类型来代表,这个通用函数就是模板。凡是函数体相同的函数都可以用模板代替,而不用定义多个函数,实际只需定义一次。调用函数时,系提供会根据实参的类型取代模板的虚类型。 123456#include <iostream>using namespace std;template<typename T>T min(T a, T b) { return a < b ? a:b;} 数组 数组 a[10] 字符数组 strlen 与 sizeofstrlen()是函数,运行时计算。参数必须是字符型指针(char*),而且必须是以\\0结尾当数组名作为参数传入时,其已经退化为指针了。 sizeof()是运算符,而不是一个函数,在编译时就计算好了,用于计算机数据空间的字节数。因此sizeof不能返回动态分配的内存大小,常用于返回类型和静态类型的分配对象,结构或数组所占空间。 数组–编译时分配数组空间大小 1char a[10] = \"hello\"; 由于char占1字节,所以sizeof(a)的值是10* 1 = 10 字节 指针–储存该指针占用空间大小 1char *str = \"I am from China\" 在32位的编译器上,指针一律4字节 类型– 类型所占空间大小 1int b = 10 32位机器上,int占4字节 对象–对象实际的占用空间 1234class class_Samlpe { int a,b; int func();}class_a; 函数–函数返回类型所占空间大小 引用引用作为函数参数:内存中没有产生实参的副本,而是对实参的直接操作。 常引用如果想提高程序的效率,又要使传递给函数的数据不在函数中被改变,就应该使用常引用。 1const 类型标识符 & 引用名 = 目标变量名; 这种方式声明的引用,不能通过对目标的变量值进行修改,保证了引用的安全 关于常引用典型的错误调用 123456string func1();void func2(string &s);// 以下表达式非法func2(func1);func2(\"hello\"); 原因在于,func1是函数返回值,”hello”是临时变量,二者都属于“临时变量”,C++中临时变量都是const类型的.因此上面试图将const转化为非const类型 引用类型应尽量被定义为const类型 结构体、共用体结构体共用体共用体用关键字union定义,它是一种特殊的类,在一个共用体多种不同的数据类型,共享空间 Union 可以用来判断计算机的大小端 枚举预处理C++提供的预处理包括4中: 宏定义、文件包含、条件编译和布局控制 常用宏定义命令#define 命令是一个宏定义命令,将标识符定义为一个字符串,该标识符被称为宏名 12345#define 宏名 字符串#define PI 3.1415#define 宏 (参数列表)宏#define A(x) x Do…while(0) 妙用1234#define Foo(x) do{\\ statement one;\\ statement two;\\}while(0) 条件编译12345#ifdef 标识符 程序段1#else 程序段2#endif extern C面向对象类与对象面向对象的主要思想是把构成问题的各个事务分解成对象,建立对象的目的是藐视一个事务在解决问题中经过的步骤和行为。对象作为程序的基本单元,将程序和数据封装其中,以提高软件的重用性、灵活性和扩展性。 1234567891011121314class CStudent {public: void display();private: int num; char name[20]; int age;};void CStudent::display() { cout<<\"num:\"<<num<<endl; cout<<\"name\"<<name<<endl; cout<<\"age:\"<<age<<endl;} 类的封装性C++中通过类实现封装性,把数据和数据有关的操作封装在一个类里。但是人们使用时往往不关心实现,为了实现类的封装性(数据隐藏、提供访问接口)类为成员提供了私有、公有和受保护三种基本的权限。 私有成员 private 公有成员 public 受保护成员 protect 构造函数数据成员不能在类中初始化,而构造函数为此而生,主要用来处理数据成员的初始化,且不需要调用,建立对象自动执行。 构造函数必须与类名相同,不能随意命名,以便编译系统识别作为构造函数处理。 123456789101112131415class Time{public: Time(); set_time(); get_time();private: int hour, minute, second;};Time::Time(){ hour = 0 minite = 0 second = 0}// C++还提供一种参数初始化表的方法Time::Time():hour(0), minite(0), second(0){} 构造函数不仅可以对数据成员赋值,也可以包含其他语句。如果用户没有定义构造函数 ,那么C++会自动为其生成一个构造函数,只是这个构造函数的函数体是空的。 构造函数可以被重载 调用构造函数时不必给出实参的构造函数,称为默认构造函数。无参的构造函数属于默认构造函数。一个类只能有一个默认构造函数。即使一个类中有多个构造函数,但建立对象时都只执行一个构造函数 构造函数可以添加默认参数 123456789101112131415161718class Box{public: Box(int h=2,int w=2,int l=2); int volume();private: int height,width,length;}Box::Box(int h,int w,int len){ height = h; width = w; len = length;}int Box::volume(){ return height*width*length;}Box box(1); // 显示指定第一个参数Box box(1,2); // 显示指定1,2参数 使用默认参数的好处在于:调用构造函数时就算没有提供参数也不会出错,对每一个对象有相同的初始化状况。 一个类全是默认参数的构造函数后,就不能再重载构造函数了 析构函数析构函数在函数名前加一个 ~ 它在对象的生命周期结束后自动执行 程序执行析构函数的时机有以下四种 如果函数定义了一个对象,这个函数调用结束时对象会被释放,且在对象释放前自动执行析构函数。 static 局部对象在函数调用结束时对象不释放,所以也不执行析构函数,只有在main函数结束调用exit函数结束程序时,才调用static局部对象析构函数 全局对象则是在程序流离开其作用域(main函数结束或调用exit()函数时)才会执行全局对象的析构函数 用new建立的对象,用delete释放对象时,会调用对象的析构函数 析构函数的作用不是删除对象,而是在撤销对象占用的内存前完成一些清理工作,使得这些内存可以供新对象使用。析构函数的作用也不限于释放资源,它还可以被用来执行用户最后一次使用对象时执行的任何操作。 123456789101112131415161718class Box{public: Box(int h=2,int w=2,int l=2); ~Box(){ cout<<\"Destructor called.\"<<endl; } int volume();private: int height,width,length;}Box::Box(int h,int w,int len){ height = h; width = w; len = length;}int Box::volume(){ return height*width*length;} 静态数据成员有时需要为某个类分类单一的储存空间,在C语言中可以使用全局变量,但这样很不安全(被修改),而且名字易冲突。如果可以把数据当成全局变量去储存,但又被隐藏在类内部,而且清楚的与这个类相联系,这种处理方法较为理想。这个可以用静态数据成员实现。类的静态数据成员共享静态储存空间,不管创建了多少这个类的对象,所有的对象静态数据共享这个储存空间,这就为对象之间提供了一种通信方法。静态数据属于类,他在类的范围内有效。 由于不管产生了多少对象, 类的静态数据都有单一的储存空间,所以储存空间必须在一个单一的地方。 静态数据成员必须出现在类的外部而且只能定义一次 1234567// xx.hclass base{public: static int var;}// xx.cppint base::var = 10; 在头文件中定义(初始化)静态成员容易引起重复定义的错误,比如这个头文件被多个cpp包含的时候 C++静态数据成员被所有的类对象共享,包括该类的派生类对象。派生类对象与基类对象共享基类的静态数据成员。静态数据成员只占一份空间。如果改变它的值,则各个对象中这个数据成员的值都变了 1234567891011121314151617class Base{public: static int var;};int Base::vat = 10;class Derived:public Base{};int main(){ Base base1; base1.var++; //11 Base base2; base2.var++; //12 Derived derived1; derived1++; //13 Derived derived2; derived2++; //14 return 0;} 如果只声明类而未定义对象,类一般数据成员不占内存空间,只有在定义对象时才会为对象分配空间。但是静态数据成员不属于某一个类的对象,所以为对象分配的空间不包括静态数据成员所占的空间,静态数据成员在所有对象之外开辟的一段空间。 静态成员函数与数据成员类似,成员函数也可以定义为静态,在函数前加入static关键字 与静态数据成员不同,静态成员函数是不是为了对象间的沟通,而是为了能处理静态数据成员。 当调用一个对象的成员函数时(非静态成员函数),系统会把该对象的起始地址付给this指针,而静态成员函数不属于某一对象,其没有this指针。既然他没有指向某一对象,也就无法对一个对象中的非静态成员进行默认访问。 静态成员函数与非静态成员函数的根本区别是:非静态成员函数有this指针,而静态成员函数没有。 静态成员函数可以直接引用本类中的静态数据成员,因为静态数据成员同样数据类 对象的储存空间结论: 1 非静态成员变量总和 2 加上编译器为了CPU计算做出的数据对其处理和 3 支持虚函数所产生的负担总和。 空类的大小占用1字节 成员函数、构造函数、析构函数 不占用空间 编译器为了支持虚函数,会产生额外的负担,这正是指向虚表的指针大小,64位机器上占8字节。 this指针每个对象中数据成员分别占有储存空间,如果对同一类定义了n个对象,则有n个同样大小的空间存放这些数据成员,不同对象引用数据成员时,提供了this指针 this指针有如下特点 只能在成员函数中使用,在全局函数、静态成员函数中都不能使用 this指针成员金函数前构造,成员函数后清楚 this指针会因为编译器不同而有不同的位置 this是类的指针 因为this指针只有在成员函数中才能有意义,所以获得一个对象后,不能通过对象使用,也无法获取指针的位置 普通的类函数不会创建表来保存指针,只有虚函数会被放到函数表中 类模板1234567891011121314151617181920// 一个典型计算的类模板template<class T>class Operation {public: Operation (T a,T b):x(a),y(b){} T add(){ return x+y; } T subtract(){ return x-y; }private: T x,y;}// 类模板的成员函数在类外定义这么写template<class T>T Operation<T>::add(){ return x+y;} 析构函数与构造函数的执行顺序一般情况下,调用析构函数的次序与调用构造函数的次序相反:最先被调用的构造函数,其析构函数被最后调用; 最后调用的构造函数,其析构函数最先被调用 在全局范围中定义的对象(所有函数之外的对象)他的构造函数在文件所有函数之前。但是一个程序有多个文件,其不同文件的都定义了全局对象,这构造的顺序使不确定的。他们在main函数结束时析构 如果定义的是局部自动对象,建立对象时调用构造函数,如果函数被多次调用,则每次都调用构造函数 如果函数定义静态(static)局部对象,则只在第一次调用构造函数,在main函数结束后才析构 继承与派生继承方式包括 public、private、protected,默认为private public 公用继承 共用成员、保护成员在派生类中保持访问属性 private 私有继承,基类的公用、保护成员在派生类中称为private成员 protect 受保护继承 基类共有、保护在派生类中成protected成员(保护成员的意思是 不能被外界引用,但是可以被派生类引用) 派生类包括,从基类继承而来的部分和声明派生类增加的部分 基类的成员函数只能访问基类的成员,不能访问派生类 派生类的成员函数可以访问基类、派生类的成员 派生类外可以方位基类成员、共有的成员 可以发现,无论哪一种继承方式,派生类不能访问基类的私有成员,私有成员只能被成员函数访问,毕竟派生类和基类不是一个类 派生类的构造与析构函数派生类的基类数据成员与新增的数据成员共同组成,如果派生类新增成员包括其他子对象,派生类数据成员还间接的包括了这些对象的数据成员,因此派生类必须对这些数据成员初始化。 对基类成员和子对象成员的初始化必须在初始化列表中进行,新增的成员初始化既可以在初始列表,也可以在在构造函数体进行 派生类构造必须对这三类成员初始化1、调用基类构造函数 2、子对象的构造函书 3、派生类的构造函数体 派生类有多个基类时,同一层次的调用顺序取决于定义派生类时顺序(从左至右) 如果基类也是派生类、依次回溯 派生类构造函数与析构函数的调用顺序1、基类构造函数 –》派生表中的顺序 2、成员类构造函数 –》 类中的声明顺序 3、 派生类构造函数 –》 类的多态1.多态 C++中 多态是指不同功能函数可以用同一个函数名,这样一个函数名可以调用不同内容的函数,在面向对象中多态这么表述:向不同的对象发送同一个消息,不同的对象在接收时会产生不同的行为;也就是说,每个对象可以用自己的方式去相应共同的消息。 设想: 能否用同一个调用形式,既用派生类又能调用基类同名函数。 C++ 虚函数解决这个问题,虚函数允许派生类重新定义与基类同名的函数,并且可以通过基类指针访问基类和派生类中的同名函数。 12345678910111213141516171819202122232425262728class Box{public: Box(int, int, int); virtual void display();protected: int length, height, width; }Box::Box (int l, int h, int w) { length = l; height = h; width = w;}void Box::display(){ cout<< ... << endl;}class FilledBox:public Box{public: FilledBox (int,int,int,int,string); virtual void display();private: int weight; string fruite;};void FilledBox::display(){ cout << ... <<nedl;}FilledBox::FilledBox(int l,int h, int w, int we, string f):\\ Box(l, h, w), weight(we), fruite(f){} 基类中使用virtual关键字声明成员函数为虚函数 派生类中重新定义此函数,要求函数名、函数类型、参数个数、类型全部与基类相同 C++规定当基类声明虚函数时,派生类中的同名函数自动成为虚函数,派生类定义该函数可加可不加virtual,但一般习惯每一层都加,这样程序更清晰 定义一个基类指针,使它指向统一类族中需要调用的对象 通过虚函数需基类指针配合使用,便可以调用同一类族中的同名函数 纯虚函数纯虚函数是在基类中生命的函数,在基类中没有定义,但要求任何派生类都定义实现方法 1virtual void function() = 0; 编译器要求派生类必须重载以实现多态性,同时含有纯虚函数的类称为抽象类 析构函数C++ 中,构造函数不能被声明为虚函数 。然而析构函数可以被声明为虚函数。 如果想要通过父指针来销毁派生类,必须定义虚析构函数 12345678910111213141516class Base{public: Base(){cout<<'Base::Base()'<<endl;} virtual ~Base(){cout<<'Base:~Base()'<<endl;}}class Derived::public Base{public: Derived(){cout<<'D'<<endl;} ~Derived(){cout<<'~D'<<endl;}}int main(){ Base* p = new Derived(); delete p; return 0;} 单例模式设计思想: 定义了一个单例类,使用私有的静态指针指向唯一的实例,并通过共有的方法获取这个指针。 单例模式保证了程序运行的任何时刻,该单例类的实例只存在一个 1234567891011121314class Sinst{private: Sinst(){}; static Sinst* pinst;public: static Sinst* getSinstance(){ if (pinst == NULL) { pinst = new Sinst(); } return pinst; }}Sinst* Sinst::pinst = NULL;","categories":[],"tags":[{"name":"C++","slug":"C","permalink":"http://yoursite.com/tags/C/"}]},{"title":"MySQL数据库","slug":"MySQL数据库","date":"2018-12-23T07:38:24.000Z","updated":"2018-12-23T07:41:18.782Z","comments":true,"path":"2018/12/23/MySQL数据库/","link":"","permalink":"http://yoursite.com/2018/12/23/MySQL数据库/","excerpt":"[TOC] 数据库MySQL事务事务的概念、隔离级别、死锁见「数据库系统原理」","text":"[TOC] 数据库MySQL事务事务的概念、隔离级别、死锁见「数据库系统原理」 事务日志事务日志可以帮助提高事务的效率,使用事务日志,在储存引擎上修改表时数据只需要修改其内存拷贝,再把修改行为记录到硬盘上的持久事务中,而不用每次都将修改的数据本身持久到磁盘 事务日志采用追加方式,因此日志操作是顺序I/O。采用事务日志速度快。事务日志持久后,内存中被修改的数据主键被刷回磁盘,大多数储存引擎都是这么实现的。这被称为预写日志,修改数据两次写磁盘 如果数据的修改已经持久化,而数据本身没被写入磁盘,系统崩溃后储存引擎在重启时自动恢复修改的数据。 MySQL自动提交MySQL默认采用自动提交,不是显示的开始一个事务,每个查询都被当成一个事务提交。 有一些命令会强制提交当前的活动事务,如Alter MySQL可以设置所有的四个隔离级别 1set session transaction isolation level read committed 事务中混用储存引擎MySQL不管理事务,事务由引擎实现,所以在一个中混用引擎是不可靠的 事务回滚对于非事务型表(如MyISAM)无法撤销,数据库会失去一致性 隐式和显示锁定InnoDB采用两阶段锁定协议,事务执行过程中随时可以执行锁定,只有在commit和rollback时释放,并且所有的锁在同一时刻释放,这些属于隐式锁,InnoDB在需要时自动加锁 同样InnoDB支持显示加锁,一般来说显示使用LOCK TABLES语句不但没有必要还会严重影响性能,实际上InnoDB行锁性能更好 MySQL并发控制读写锁详见「数据库系统原理」 锁粒度一种提高资源并发的方式是让锁的对象更有选择性。尽量只锁需要修改的部分数据,而不是所有的资源。任何时候,互相之间不发生冲突,锁的数量越少并发程度越高。 加锁也需要消耗资源,获得锁、检查锁、释放锁都会增大开销。所谓锁策略,就是在所的开销和安全性之间寻求一种平衡。大多数商业数据库没有提供更多的选择,一般是在表上施加行级锁。而MySQL提供可多种选择,每种储存引擎都可以实现自己的锁策略和锁粒度 表锁 表锁是MySQL中最基本的锁策略,并且是开销最小的锁。它锁定整张表,用户对表的写操作都需要获得整个锁 尽管储存引擎管理自己的锁,MySQL本身还是会使用有效的表锁,例如Alter table语句使用表锁 行级锁 行级锁可以最大程度的支持并发处理(最大的锁开销),InnoDB及XtraDB实现了行级锁,行级锁只在引擎层面实现,而服务器层面并不了解锁的情况 MySQL多版本并发控制(MVCC)MySQL实现了自己的多版本并发控制(MVCC),可以认为MVCC是行级锁的一个变种,它在很多情况下避免了加锁操作,因此开销更低。 MVCC的实现是通过保存数据在某个时间点的快照来实现的。不管需要执行多长时间,每个事务看到的数据都是一致的。根据事务开始的时间不同,事务对同一张表,同一时刻数据可能是不一样的 InnoDB的MVCC是通过在每个记录后面保存两个隐藏的列来实现的。这两个列一个保存了行的创建时间,一个保存了行的过期时间。当然储存的是系统版本号。每开始一个事务,系统版本号都会递增,事务开始时刻的系统版本号作为事务的版本号。 SELECT InnoDB会根据以下两个条件检查记录 A. InnoDB只查找版本早于当前事务版本的数据行,这样可以确保事务读取的行要么在开始前存在,要么自身插入或修改 B. 行的删除版本要么未定义,要么大于当前事务版本号。这样可以确保事务读到的行在事务开始前未被删除 INSERT InnoDB为新插入的每一行保存当前系统的版本号 DELETE InnoDB为每一行保存当前系统的版本号作为删除标识 UPDATE InnoDB插入一行新的记录,保存当前系统的版本号,同时保存当前系统到原来的行作为删除标识 这样通过版本号的方法使得大多数读操作不需要加锁。 MVCC只在可重复读和已提交读两个隔离级别工作,其他的隔离级别与MVCC不兼容 MySQL引擎InnoDB 储存引擎InnoDB是MySQL默认的事务型引擎,也是最重要,最广泛使用的储存引擎。它被设计用来处理大量的短期事务,短期事务大部分情况下都是正常提交的,很少被回滚。InnoDB的性能和崩溃自动回复的特性使其在非事务型储存的需求中也很流行。 InnoDB数据储存在表空间中,表空间是InnoDB管理的一个黑盒子,由一系列数据文件组成。 InnoDB通过MVCC来支持高并发,并且实现了四个标准的隔离级别,其默认级别是可重复读,并且通过间隙锁策略防止幻读的出现。间隙锁使得InnoDB不仅仅锁定查询涉及的行,还会对索引的间隙进行锁定。 InnoDB表基于聚簇索引建立。InnoDB和MySQL其他引擎有很大不同,聚簇索引对主键的查询有很高的性能。不过他的二级索引必须包含主键列,所以如果主键很大的话,其他的二级索引就会很大。所以索引较多的话主键应尽可能的小 InnoDB内部有很多优化,从磁盘读数据的可预测性读,自动创建hash索引,插入缓冲器操作。 MyISAM引擎在MySQL5.1之前的版本,MyISAM都是默认的储存引擎,MySQL有几个致命的缺点 不支持事务和行级锁 崩溃后无法安全恢复 但其优点在于对于只读数据,速度较快 MyISAM将表储存在两个文件中,数据文件和索引文件,采用非聚簇索引(详见索引部分) 比较 事务: I 是事务型的,支持提交和回滚 并发:M 只支持表级锁, I 还支持行级锁 外键: I 支持外键 备份: I支持在线热备份 崩溃恢复: M崩溃后损坏的几率较高,I 有完善的日志恢复机制 MySQL索引索引(在MySQL中也称键)是储存引擎用于快速找到记录的一种数据结构。索引对于良好的性能非常关键。 索引优化是对查询性能优化最有效的手段,能够轻易将查询性能提升若干个数量级。 索引基础B-树索引B-树索引是人们经过长期探索发展出目前最适合数据库系统的数据结构,它高效的利用了索引以及机械磁盘的空间。在大多数情况下爱,其不需要全表扫描来获取需要的数据,取而代之的是从根节点进行搜索,依次根据指针向下查找。B树索引有以下特点: 全值匹配 与所有列匹配 匹配最左前缀 索引查找性为“Allen”的人 匹配列前缀 like “J%” 匹配范围值 精确匹配某一列与并范围匹配某一列 只访问索引查询(覆盖索引) B树索引的不足 如果不是从最左列开始,则无法使用查找 不能跳过索引中的列 范围列的右侧无法再使用索引 可以发现,索引列的顺序十分重要 hash索引hash索引基于哈希表实现,只有精确匹配所有列的查询才有效。每一行数据索引都会计算一个hash码 hash查找速度很快,但也有以下限制: hash索引只包含hash值和行指针,而不储存字段的值 hash索引并不是按照索引值储存,无法排序 hash索引不支持部分索引的匹配查找,只支持等值查找 值得注意的是,InnoDB有一个特殊的功能叫自适应哈希索引,当InnoDB某些值使用非常频繁时,他会在内存中基于B树再创建一个hash索引 索引的优点 索引大大减小了服务器需要扫描的数据量 索引可以帮助服务器避免排序和临时表 索引可以将随机IO变为顺序IO 聚簇索引聚簇索引并不是一种单独的索引类型,而是一种数据储存方式。当表有聚簇索引时,表示数据行和相邻的键值紧凑的储存在一起。一个表只能有一个聚簇索引。 InnoDB默认通过主键来聚集数据 如果没有定义主键、InnoDB会选择一个唯一的非空索引,如果没有这样的索引,InnoDB会隐式的定义一个主键作为聚簇索引的主键 聚簇索引的优点 可以把相关数据保存在一起,例如根据用户ID来聚集数据,这样可以最小化磁盘读取数据页 数据访问更快,举措索引将索引和数据保存在统一个B树中,因此聚簇比非聚簇索引的查找更快 覆盖索引世界使用主键值 聚簇索引的缺点 聚簇索引最大限度提高IO密集应用的性能,但是数据如果都在内存中(例如缓存)访问顺序就不重要了 插入速度依赖于插入顺序 更新聚簇索引列的代价很高,强制每个更新移动到位 页分裂问题 聚簇索引可能导致全表扫描变慢(尤其是稀疏表时) 二级索引可能比想象的大,因为二级索引叶子节点包含了主键列 InnoDB和MyISAM的数据分布对比MyISAM 数据分布很简单,数据按照插入数据储存在磁盘上 它很容易创建索引,并且,索引没有什么不同 这是索引1号 Col1 这是索引2号Col2 而InnoDB支持聚簇数据,使用非常不同的方式储存同样的数据。 该图显示整个表,而非只有索引,InnoDB中聚簇索引就是表 聚簇索引的每一个指针都包含了主键值、事务ID、事务及MVCC的回滚指针及所有的剩余列。 还有一个重要的不同, InnoDb的二级索引和聚簇索引有很多不同,其叶子节点储存的不是行指针而是主键值。 覆盖索引索引是一种查找数据的方式,但是MySQL也可以使用索引来直接获取列的数据,这样就不需要读取数据的行。 如果一个索引包含(或者说覆盖)所有要查询字段的值,我们就称之为“覆盖索引。!!覆盖索引从辅助索引中即可得到查询数据,简单的说就是辅助索引就包含了所要查询的值!! 例如创建某个辅助索引(name、age)查询数据时,select username,age from user where username=’Java’ and age = 22 要查询的列叶子节点都存在,不需要回表索引若干问 索引若干问使用索引的原因?通过创建唯一性索引,可以保证数据库表中每一行数据的唯一性; 大大加快数据的检索速度(这是创建索引最重要的原因); 帮助服务器避免排序和临时表,将随机IO变为顺序IO; 加速表与表之间的连接,在实现数据参考完整性方面有重要意义; 为什么不对每一列创建索引?当数据增加、删除和修改时,索引也需要动态维护,这样就降低了数据的维护速度 除了数据表占数据空间之外,索引还要需要占据一定的物理空间,如果建立聚簇索引,空间占用更大 创建、维护索引耗费时间、这种耗费随数据量增加而增加 索引提高查询速度的原因?将无序的数据变成相对有序的数据(像查目录一样) 常用索引使用的数据结构? 哈希索引 对于哈希索引来说,底层函数就是一个hash表,因此在大多数需求为单条记录的查询时,可以选择hash索引,查询性能较快 BTree索引 Mysql使用的是B+树索引,但是对于两种引擎的实现不同 MyISAM和InnoDB实现B+树索引的区别MyISAM: B+树叶节点data域存放的是数据记录的地址,在索引检索的时候,首先按照B+树 算法搜索索引,如果指定的Key存在,则取出其data域的值,然后以data域的值为地址读取相应的数据记录,被称为”非聚簇索引” InnoDB: 数据文件本身就是索引文件,相比于MyISAM 索引文件和数据文件是分离的,其表结构本身就是按B+Tree组织的一个索引结构,树的叶节点data域保存了完整的整条数据记录,这个索引的key正是数据表的主键,因此InnoDB本身就是主索引,这被称为“聚簇索引(聚集索引)”,而其余的索引被作为辅助索引,辅助索引data域储存记录主键的值而不是地址。这样根据辅助索引查找时,先取出主键的值,再走一遍主索引。因此设计表时主键的选择十分重要,主键不宜过长也不宜非单调。 索引的注意事项 在经常需要索引的列上加快搜索速度 在经常使用where子句上索引加快条件判断速度 在经常需要排序的列上创建索引,因为索引已经排序,这样加快排序的查询时间 中、大型表的排序都是有效的,但是特大型表不适合建索引 在经常用到的连接上建索引,这些外键索引加快速度 避免where子句对字段加函数,这样无法命中索引 在InnoDB中使用与业务无关的自增主键作为主键,使用逻辑主键,不使用业务主键 索引列 NOT NULL,否者引擎将放弃索引 删除长期不使用索引、不使用索引的存在会造成比必要的性能损耗 索引原则(高性能Mysql) 单行访问很慢,特别是机械硬盘。如果从数据中读取的一个数据库只获取一行,浪费很多工作,最好读取尽可能多的行。 顺序访问范围数据是很快的。第一,顺序I/O不需要多次的磁盘寻道,比随机I/O快的多。第二、服务器按顺序读取数据,不需要额外的排序操作,group by 也无需再次排序 索引的覆盖查询时很快的,如果一个索引包含了查询所有的列,那么储存引擎就不需要再回表查询行,这避免了大量的单行访问","categories":[],"tags":[{"name":"数据库","slug":"数据库","permalink":"http://yoursite.com/tags/数据库/"}]},{"title":"图书指南","slug":"图书指南","date":"2018-12-23T07:37:03.000Z","updated":"2018-12-23T07:38:43.919Z","comments":true,"path":"2018/12/23/图书指南/","link":"","permalink":"http://yoursite.com/2018/12/23/图书指南/","excerpt":"算法 《算法》 《算法导论》 《剑指offer》 《程序员面试金典》 《编程之法:面试和算法心得》 《程序员代码面试指南》","text":"算法 《算法》 《算法导论》 《剑指offer》 《程序员面试金典》 《编程之法:面试和算法心得》 《程序员代码面试指南》 计算机网络 《TCP/IP详解》 《计算机网络:自顶向下方法》 《HTTP权威指南》 操作系统 《深入理解计算机系统》 《计算机程序的构造和解释》 数据库 《数据库系统概念》 《高性能MySQL》 《Redis设计与实践》 C++ 《C++Primer》 《STL源码剖析》 《深度探索C++对象模型》 《Effective C++》 Linux 《鸟哥的Linux私房菜》 《Unix环境高级编程》 《Unix网络编程》 其他 《后台开发:核心技术与应用实践》 《HeadFirst设计模式》 《王道程序员面试宝典》","categories":[],"tags":[{"name":"书籍","slug":"书籍","permalink":"http://yoursite.com/tags/书籍/"}]},{"title":"数据库系统原理","slug":"数据库系统原理","date":"2018-12-23T07:16:31.000Z","updated":"2018-12-23T07:35:14.718Z","comments":true,"path":"2018/12/23/数据库系统原理/","link":"","permalink":"http://yoursite.com/2018/12/23/数据库系统原理/","excerpt":"[TOC] 数据库系统原理事务事务指满足ACID特性的一组操作,可以通过commit 提交,也可以通过rollback回滚 事务的ACID特性 原子性(Atomicity) 事务被视为不可分割的最小单元,事务的所有操作要么全部成功提交,要么全部失败回滚,回归可以使用回滚日志来实现,回滚事务记录着执行的修改操作,回滚时反向执行即可 一致性(Consistency) 数据在事务的执行前后都保持一致性状态,在一致性状态下,所有事务对一个数据的读取结构都是相同的 隔离性(Isolation) 一个事务所做的修改在最终提交以前对其他事务不可见 持久性(Durability) 一旦事务提交,则其所做修改会永远保存到数据库中,即使系统崩溃事务执行结果也不能丢失,使用重做日志来保障持久性","text":"[TOC] 数据库系统原理事务事务指满足ACID特性的一组操作,可以通过commit 提交,也可以通过rollback回滚 事务的ACID特性 原子性(Atomicity) 事务被视为不可分割的最小单元,事务的所有操作要么全部成功提交,要么全部失败回滚,回归可以使用回滚日志来实现,回滚事务记录着执行的修改操作,回滚时反向执行即可 一致性(Consistency) 数据在事务的执行前后都保持一致性状态,在一致性状态下,所有事务对一个数据的读取结构都是相同的 隔离性(Isolation) 一个事务所做的修改在最终提交以前对其他事务不可见 持久性(Durability) 一旦事务提交,则其所做修改会永远保存到数据库中,即使系统崩溃事务执行结果也不能丢失,使用重做日志来保障持久性 事务的ACID特性比较简单,但是并不容易理解,他们并不平级 只有满足一致性,事务的执行结果才是正确的的 在无并发的情况下,事务串行执行,隔离性一定能满足。此时只要满足原子性,一致性即可满足 并发的情况下,多个事务并行执行,事务不仅满足原子性,还要满足隔离性 才能满足一致性 事务满足持久性是为了维护数据库崩溃的情况 事务的自动提交 AUTO_COMMITMySQL默认采用自动的提交模式,即,如果不显示使用START TRANSACTION来开始一个事务,每个查询都会被当做一个事务自动提交 事务的模型抽象成功完成的事务称为已提交,而非成功完成的事务被中止了,为了确保数据的原子性,中止事务对数据库状态不可以造成影响,因此这些影响必须被撤销。一旦中止事务的影响被撤销,我们称事务已回滚 ,数据库通过日志来支持回滚操作 一个简单事务抽象模型包括: 活动的:初始状态,事务执行时处于这个状态 部分提交的:最后一条语句执行后 失败的:发现正常的执行不能继续后 中止的:事务回滚并恢复到事务开始执行的前后 提交的:成功完成后 事务的隔离性事务处理系统通常允许多个事务并发的执行,并发有两条无法拒绝的理由 提高吞吐量和资源利用率 减少等待时间 数据库中并发的动机和多道程序设计的动机是相同的,而为提升效率的同时,我们必须控制事务的交互,来保证数据库的一致性,这被称为系统的并发控制 在并发执行时,通过保证所执行的任何调度效果都与没有并发效果一样,我们可以保持数据库的一致性。调度在某种意义上等价于一个串行调度,这种调度被称为可串行化调度 事务的并发可串行化串行化顺序可以通过拓扑排序得到。 对于优先图来说,读写、写读、写写被称为一条边,考察这个有向图中是否有环,无环的优先图被称为可串行化调度 并发一致性问题在并发环境下,事务的隔离性很难保证,因此可能出现并发一致性问题 问题 原因 图例 丢失修改 T1和T2 两个事务都对一个事务进行修改,T1先修改T2随后修改,T2的修改覆盖了T1的修改 读脏数据 T1修改了一个数据,T2最后读取了这个数据。如果T1撤销了这次修改,那么T2读取的数据是脏数据 不可重复读 T2读取了一个数据,T1对其进行了修改,如果T2再次读取这个数据,此时读取结果和第一次不同 幻影读 T1读取某个范围内的数据,T2 在这个范围内插入新的数据,T1再次读取这个范围内的数据,此时读取的结果和第一次读取的不同 事务的隔离级别 隔离级别 简介 可串行化 即可串行化调度 可重复读 只允许读取已提交数据,事务两次读取的间隙其他事务不得更新 已提交读 只允许读取已提交的数据,允许同一事务读数据的前后不一致 未提交读 允许读取未提交数据 事务的隔离级别与对应的并发问题 隔离级别/并发问题 脏读 不可重复读 幻影读 加锁读 未提交读 √ √ √ × 提交读 × √ √ × 可重复读 × × √ × 可串行化 × × × √ 隔离级别的实现锁通过封锁来实保证事务的可串行化。通过共享、排他锁及两阶段封锁协议来保证串行化下的并发读 时间戳另一类用来实现隔离性的技术为为每一个事务分配一个时间戳,系统维护两个时间戳来保证冲突情况下按照顺序访问数据项 多版本和快照隔离快照隔离中,我们可以想象每个事务开始时尤其自身的数据库版本或快照,它从这个私有的版本中读取数据,因此它和其他事务的更新隔开。事务的更新只在私有数据库中进行,只有提交时才将信息保存,写入数据库。 并发控制读写锁锁一般被分为两种 共享锁:简称为S锁,又称为读锁 排他锁:简称为X锁,又称为写锁 这两种锁有以下规定 一个事务数据对象加了X锁,就可以对数据A进行读取和更新,加锁期间其他事务不能获得A的锁 一个事务数据对象加了S锁,可以对A进行读取操作,加锁期间其他事务可以对其加S锁,但是不能加X锁 意向锁使用意向锁来支持多粒度的封锁 在行级锁、表级锁的情况下,事务想要对表A加X锁,就要检测其他事务是否对表A和表A的任意一行加了锁,那么就需要对A的每一行都检测,这非常耗时 在X/S锁之外引入了IX、IS,IX和IS都是表锁,用来表示一个事务想在某个表上加X或S锁,有以下规定: 一个事务在获得某个数据行的S锁之前,必须先获得表的IS锁或更强的锁 一个数据在获得某个数据航的X锁之前,必须获得表的IX锁 通过引入意向锁,输入想要对某个表A加锁,只需检测事务是否对表A加了x/Ix/S/IS锁 - X IX S IS X × × × × IX × √ × √ S × × √ √ IS × √ √ √ 任意IX/IS 之间都是相容的,因为它只表示要加锁,并没有真正的加锁 S锁只与S和IS兼容 两阶段封锁协议保证事务可串行化的一个协议是两阶段封锁协议,该协议分两个阶段 增长阶段:事务可以获得锁,但是不能释放锁 缩减阶段:事务可以释放锁,但是不能获得新锁","categories":[],"tags":[{"name":"数据库","slug":"数据库","permalink":"http://yoursite.com/tags/数据库/"}]},{"title":"操作系统之文件","slug":"操作系统之文件","date":"2018-12-07T13:25:21.000Z","updated":"2018-12-07T13:28:35.964Z","comments":true,"path":"2018/12/07/操作系统之文件/","link":"","permalink":"http://yoursite.com/2018/12/07/操作系统之文件/","excerpt":"[TOC] 内存部分虚拟内存虚拟内存的概念内存管理中进程有如下特征: 一次性:作业必须一次性全部装入内存后才能开始运行 驻留性:作业被装入内存后驻留在内存中,任何部分都不会被唤出直至作业结束 局部性原理 高速缓存是计算机科学中唯一重要的思想。事实上告诉缓存技术极大的影响了计算机系统的设计 -- Bill Joy「SUN 公司 CEO」 时间局部性: 如果程序某条指令一旦执行,不久之后可能会再次被执行;如果某个数据被访问过,不久以后它可能再次被访问 空间局部性:一旦程序访问某个储存单元,不久之后其附近的单元也将被访问。","text":"[TOC] 内存部分虚拟内存虚拟内存的概念内存管理中进程有如下特征: 一次性:作业必须一次性全部装入内存后才能开始运行 驻留性:作业被装入内存后驻留在内存中,任何部分都不会被唤出直至作业结束 局部性原理 高速缓存是计算机科学中唯一重要的思想。事实上告诉缓存技术极大的影响了计算机系统的设计 -- Bill Joy「SUN 公司 CEO」 时间局部性: 如果程序某条指令一旦执行,不久之后可能会再次被执行;如果某个数据被访问过,不久以后它可能再次被访问 空间局部性:一旦程序访问某个储存单元,不久之后其附近的单元也将被访问。 基于局部性原理,程序装入内存时,可以先装入一部分内存,其余部分留在外存。在程序运行过程中,当访问信息不存在时,操作系统将需要信息调入内存;另一方面操作系统将暂时不适用的内容放到外存之中。这样系统好像为用户提供了一个大的多的储存器,称为虚拟储存器 之所以称为虚拟储存器,是因为这种储存器并不存在,只是由于系统提供了部分装入请求和置换功能后,给用户感觉是一个比实际物理内存大的多的存储器。 页面置换算法最佳置换算法「OPT」最佳置换算法选择被淘汰页面是以后用不使用的,或者在最长时间未被访问的页面,这样可以保证最低的缺页率。 该算法是理想算法 ,「无法实现」 先进先出算法「FIFO」优先淘汰最早进入内存的页面,亦即在内存中驻留最久的页面。 算法实现较简单,但是可能出现页故障数随物理块数增大不降反增的状况「Belady」异常 最近最久未使用算法「LRU」选择最近最长时间未访问过的页面予以淘汰,他认为最近一段时间未访问的页面在将来一段时间也不会被访问。 时钟置换算法「CLOCK」「NRU」某次装入内存时,其使用位被置位 1 ,当再次被访问到时,其再被置为 1 当需要替换页时,从缓冲区头部开始扫描,遇到 1 置 0 ;遇到 0 就选择该页置换;依次循环 改进 CLOCK 算法 将使用位再细分为 访问与修改两种状态,修改优先级大于访问 文件部分磁盘的结构磁盘由表面涂有磁性物质的金属或塑料构成圆形盘片,通过一个名为磁头的导线圈从磁盘中读取数据。在读写操作期间, 磁头固定,磁片高速旋转。 磁盘调度算法先来先服务 「FCFS」FCFS根据请求磁盘的先后顺序调度,该算法较为简单 最短寻道时间优先「SSTF」优先调度当前磁头所在磁道最近的磁道 平均寻道时间较低,但是较为不公平。如果新的磁道总是比一个等待的磁道近,等待的磁道会一直等待下去出现饥饿现象 电梯算法当前移动方向上选择当前磁头最近请求作为下一次服务方向。即总是面向一个方向运行,直到该方向没有请求位置,改变运行方向。 程序编译与链接编译系统以下是一个 hello.c 程序 123456#include <stdio.h>int main(){ printf(\"hello, world\\n\"); return 0;} 在 unix 上, 编译器把源文件转换为目标文件 gcc -o hello hello.c 预处理阶段:预处理以 # 开头的预处理命令 编译阶段:翻译成汇编文件 汇编阶段:将汇编文件翻译成可重定向的目标文件 连接阶段:将可重定向目标文件和 printf.o 等单独编译好的文件进行合并,得到可执行的目标文件 静态链接静态链接器以一组可重定向目标文件为输入,生成一个完全链接的可执行目标文件为输出,链接器主要完成以下任务: 符号解析:每个符号对应一个函数,一个全局变量或静态变量,符号解析的目的是将每个符号引用与一个符号定义关联起来 重定位:链接器通过吧每个符号定义与一个内存位置关联起来,修改对这些符号的引用,使他们指向这个内存位置 目标文件 可执行目标文件:直接在内存中执行 可重定向目标文件:可以其他重定向文件在链接阶段合并,传建一个可执行目标文件 共享目标文件:特殊的可重定向文件,在运行时被动态加载进内存并链接 动态链接静态库有以下两个问题: 当静态库更新时,整个程序重新链接 对于 printf 这种标准库函数,每个程序都要都代码,极大浪费资源 共享库是为了解决静态库的问题而设计的,在linux中用.so文件来表示,windows 中被称为dll 它们具有以下特点 在给定的文件系统中一个库只有一个文件,所有引用库的可执行目标都共享这个文件,他不会被复制到他的可执行文件中 内存中共享的副本可以被不同正在运行的进程共享","categories":[],"tags":[{"name":"操作系统","slug":"操作系统","permalink":"http://yoursite.com/tags/操作系统/"}]},{"title":"数据库之SQL","slug":"数据库之SQL","date":"2018-12-07T13:14:07.000Z","updated":"2018-12-07T13:22:21.186Z","comments":true,"path":"2018/12/07/数据库之SQL/","link":"","permalink":"http://yoursite.com/2018/12/07/数据库之SQL/","excerpt":"[TOC] SQL初级SQL概览SQL最早版本是由IBM开发的,最初被叫做Sequel。其发展至今被称为SQL(结构化查询语言),最新的SQL标准是SQL:2008 SQL语言有以下部分: 数据定义语言(DDL) 数据操纵语言(DML) 完整性 保存在数据库中的数据必须满足完整性约束 视图定义 事务控制 嵌入SQL和动态SQL 授权","text":"[TOC] SQL初级SQL概览SQL最早版本是由IBM开发的,最初被叫做Sequel。其发展至今被称为SQL(结构化查询语言),最新的SQL标准是SQL:2008 SQL语言有以下部分: 数据定义语言(DDL) 数据操纵语言(DML) 完整性 保存在数据库中的数据必须满足完整性约束 视图定义 事务控制 嵌入SQL和动态SQL 授权 SQL数据定义基本类型 char(n) 固定长度字符串,用户指定长度。如果字符长度不到n,在字符串后面补空格使其至n个字符串长度 varchar(n) 变长字符串 int 整数类型 smallint 小整数类型 numeric(p,d) 定点数,精度有用户指定 float(n) 精度至少为n的浮点数 基本模式12345create table department(dept_name varchar(20),building varchar(15),budget numeric(12,2),primary key(dept_name)); 这里简单讨论几个完整性约束 primary key (A1,A2…Am) primary key 声明属性构成关系主码,主码必须非空且唯一。 foreign key(A1,A2…Am) reference : × foreign key 表示声明关系中的任意元组必须在对应的外键属性上取值 not null 非空 SQL禁止破坏完整性约束的任何数据库更新,例如主码上的空值和重复将会被SQL标记错误并阻止更新 插入、删除和修改 的基本模式: 12345678910111213# 删除模式,(保留关系,只删满足条件的数据)delete from student# 删除模式,(表结构整体删除)drop table r# 修改表结构# 表增加一列alter table r add A D# 表删除一列alter table r drop A# 修改某一列属性(不改名)alter table r modify A D <Y># 修改某一列属性 (改名)alter table r change A1 A2 D <Y> SQL查询基本结构SQL的基本查询结构由三个基本的子句组成:select from where 我们对其进行一个总结 select 子句用于列出查询结果中需要的属性 from 子句是一个查询求值中需要访问的关系列表 where 子句一个作用在from关系中的谓词 其基本的查询有如下形式 123select A1,A2,...,Anfrom r1,r2,...,rmwhere P 为了较好的理解这个关系的顺序是 from –> where –> select 首先通过from子句定义了一个在子句中列出关系的笛卡尔积,一般来说这个结果可能是一个相当大的关系,这样的笛卡尔积一般是没有意义的。而where子句谓词用来限制from子句所建立的集合,只留下对答案有意义的集合,最后我们可以用select限制结果中需要的子集 自然连接组合信息是一种通用的过程,SQL支持一种被称为自然连接的运算。自然连接运算作用于两个关系,并产生一个关系作为结果。不同于两个关系上的笛卡尔积将第一个关系的每个元组与第二个关系的所有元组都进行连接;自然连接只考虑两个关系模式中都出现且属性相同的元素对。 123select A1,A2,...An from r1 join r2, ... join rnwhere P; 附加基本运算更名运算字符串运算字符串可以使用like来实现模式匹配 % 匹配任意子串 _ 匹配任意一个字符 排序运算where子句谓词集合运算SQL在关系上的union、intersect 、except 分别对应了∪、∩ 及 - 聚集函数基本聚集分组聚集有时候我们不仅希望聚集函数作用力在单个元组集上,也希望作用到一组元组集合上,可以采用group by子句 并利用 having子句限定元组的条件 嵌套查询数据库修改删除12delete from rwhere P; 插入 12insert into rvalues (A1,A2,...) 更新12update instructor set salary = salary * 1.05; 小节SQL 语言分为数据定义语言(DDL)和数据操纵语言(DML) DDL: 定义关系模式、删除、修改关系模式 create alter drop DML: 包括查询语言、插入、修改、删除元组的命令 select insert delete update 连接表达式参与连接的任何一个或两个关系中的某些元组可能会以某种方式丢失,外链接以创建包含空值元组的方式保留了在连接中丢失的元组。 一般的,外链接有三种类型: 左外连接: 只保留出现在左外连接运算之前(左边)关系中的元组 右外连接:只保留出现在右外连接之后(后边)关系中的元组 全外连接:保留出现在两个关系中的元组 事务事务 由查询和更新语句的序列组成,SQL标准规定当一条SQL语句被执行,就隐式的开始了一个事务,下列SQL之一会结束一个事务 Commit work: 提交当前事务,将该事务所做功能新在数据库中持久保存。事务被提交后,一个新的事务自动刚开始 Rollback work:回滚当前事务,即撤销事务中所有的SQL语句对数据库的更新。这样数据库就恢复到执行事务第一条语句前的状态 当系统检测到错误时,事务回滚是有用的。而事务提交就像存盘,一旦commit他的影响就不能用rollback来撤销。数据库保证在发生SQL语句错误、断电、系统崩溃这样的故障情况下,如果事务没有完成commit,其影响将被回滚。如果断电和系统崩溃,回滚会在重启后进行。 一个事务在完成所有步骤后的提交行为,或者在不完成所有动作情况下回滚其动作,这种方式数据库提供了了对事务的 原子性 抽象,原子性也就是不可分割性,事务的影响被反映到数据库中,或是任何影响都没有。","categories":[],"tags":[{"name":"数据库","slug":"数据库","permalink":"http://yoursite.com/tags/数据库/"}]},{"title":"操作系统之进程","slug":"操作系统之进程","date":"2018-12-07T13:11:54.000Z","updated":"2018-12-07T13:22:35.897Z","comments":true,"path":"2018/12/07/操作系统之进程/","link":"","permalink":"http://yoursite.com/2018/12/07/操作系统之进程/","excerpt":"[TOC] Introduction本节总结了操作系统的相关概念,操作系统的知识点基本上是围绕着进程展开。 进程进程的概念与特征 进程是程序的一次执行 进程是一个程序及数据在处理机上顺序执行时所发生的活动 进程是系统进行资源分配和调度的一个独立单位。进程的独立运行由进程控制块(PCB)控制和管理。程序段、相关数据、PCB三部分构成了进程映像。进程映像是静态的进程。 进程具有动态性(有着创建、活动、暂停、终止等过程,具有生命周期)、并发性(多个进程在一段时间内同时运行)、独立性(进程是一个独立运行、获得资源和接收调度的基本单位)、异步性(进程按照独自的、不可预知的速度前进)、结构性(每个进程都有一个PCB对其描述)","text":"[TOC] Introduction本节总结了操作系统的相关概念,操作系统的知识点基本上是围绕着进程展开。 进程进程的概念与特征 进程是程序的一次执行 进程是一个程序及数据在处理机上顺序执行时所发生的活动 进程是系统进行资源分配和调度的一个独立单位。进程的独立运行由进程控制块(PCB)控制和管理。程序段、相关数据、PCB三部分构成了进程映像。进程映像是静态的进程。 进程具有动态性(有着创建、活动、暂停、终止等过程,具有生命周期)、并发性(多个进程在一段时间内同时运行)、独立性(进程是一个独立运行、获得资源和接收调度的基本单位)、异步性(进程按照独自的、不可预知的速度前进)、结构性(每个进程都有一个PCB对其描述) 进程的状态 运行状态:进程在处理机上运行 就绪状态:进程已处于准备运行的状态,即进程获得了除处理机以外的一切所需资源,只需得到处理机即可执行 阻塞状态(等待/封锁状态):进程正在等待某一事件而暂停运行。特点是即使处理机空闲也不能运行 创建状态:进程正在创建尚未转到就绪状态。创建进程通常需要经过几个步骤:申请空白PCB、向PCB写入控制和管理进程的信息,然后为该进程分配运行时所必须的资源,最后将其转入就绪状态 结束状态:进程从系统中消失,这可能是因为正常结束或其他原因中断退出。当进程结束运行时,系统首先置该进程为结束状态,进一步处理资源释放和回收等工作。 进程控制 进程控制是指对系统中的进程实施有效管理。一般把控制进程的程序段称为原语,原语的特点是执行期间不允许中断,它是不可分割的单位。 进程的创建引起进程创建的事件1. 用户登录:分时系统中,每一个用户登录都可以被看做是一个新的进程。系统为该终端建立一个进程并插入就绪队列 2. 作业调度:批处理系统中,当系统按照一定算法调度到某作业时,便将该作业调入内存并为其分配资源,创建进程,插入就绪队列 3. 提供服务:运行中的用户提出某种请求后,系统为其创建一个进程来提供用户需要服务 4. 应用请求;前三种是系统创建进程,而用户基于自己的需求可以创建新进程,以便用户并发的完成特定任务。 进程的创建过程 进程创建原语create 1. 为进程申请一个唯一的进程识别号与一个空白的PCB 2. 为进程分配资源,为新进程的程序和数据、用户栈分配内存空间 3. 初始化PCB,主要包括初始化标志信息,状态信息及处理机控制信息 4. 如果就绪队列能够接纳新进程,插入就绪队列等待被调度运行 进程的终止引起进程终止的事件1. 正常结束 2. 异常结束:出现某种错误或故障导致程序无法进行,如:越界错误、非法指令、运行超时、等待超时、IO故障 3. 外界干预:进程应外界请求而终止 进程的终止过程 进程终止原语destroy 1. 根据被终止进程的标识符,从PCB集合中检索出进程的PCB,并读取进程状态 2. 若进程处于执行状态,立即终止该进程,并置调度标志为真 3. 若进程还有子孙进程,将其所有子孙进程终止,以防其不可控 4. 将终止进程的所有资源释放给系统或父进程 5. 将被终止进程移出所在队列 进程的阻塞与唤醒进程的阻塞 阻塞原语block 正在执行的进程,由于期待某些事件未发生,如请求系统资源失败、等待某种操作的完成、新数据尚未到达,系统自动执行阻塞原语(block),是自己由运动态转为阻塞态,可见阻塞是一种主动现象 阻塞过程 1. 找到要阻塞的标识号对应的PCB 2. 若进程为运行态,则保护现场,将其运行状态转为阻塞,停止运行 3. 将PCB插入相应的时间等待队列中去 进程的唤醒 唤醒原语wakeup 当被阻塞的进程所期待的出现时,如它启动的I/O操作所期待的数据已到达,则有关进程调用唤醒原语(wakeup)将该进程唤醒 唤醒过程 1. 在事件的等待队列中找到进程的PCB 2. 将其从等待序列中移除,并置为就绪状态 3. PCB插入就绪队列,等待进程调度 进程的挂起与激活进程的挂起挂起原语:suspend() 当出现了进程挂起事件时,比如用户请求挂起自己的进程,或父进程挂起子进程。 进程的激活激活原语:active() 当激活的事件发生时,例如父进程或用户进程请求激活子进程,若进程驻留在外存而内存有足够的空间时,将外存的进程换入内存 进程通信进程间通信主要包括三种:共享储存、消息传递、管道通信 共享储存在通信的进程之间存在一块可以直接访问的共享空间(内存),这块内存由一个进程创建但是多个进程都可以访问。共享内存是最快的IPC方式,它是专门针对其他通信方式的低效而设计的。与其他通信机制配合(如信号量)来实现进程的同步和通信 消息传递消息以格式化的形式为单位,通过一个缓冲队列发送至另一个进程。该缓冲队列可能由操作系统提供。 管道通信管道是进程通信的一种特殊方式,指连接一个读进程和一个写进程实现他们通信的共享文件。为了协调双方通信,管道机制必须提供以下协调能力:互斥、同步、确定对方存在。 linux中管道是一种频繁使用的机制,本质上管道是一种文件,克服通信上的两个问题: 当写进程较快,限制管道大小,linux中该管道大小为4KB,这样缓存大小不会无限制增长。当管道满时,管道对write的调用被阻塞 当读进程较快,管道空后,read操作被阻塞 管道是半双工的,同一时刻只能单向传输。管道可以作为一共享储存的一个优化,利用缓冲区实现了读写的同步。 线程线程的概念与特征引入线程的目的是为了更好的使多道程序并发执行,以提高资源利用率与吞吐量,减小程序并发执行付出的时空开销,提高操作系统的并发性能。 线程是“轻量级进程”,是一个基本的CPU执行单元。由线程ID、程序计数器、寄存器和堆栈组成。线程是进程中的一个实体,是被系统调度和分派的独立单位。线程不拥有系统资源,线程只有就绪、阻塞、运行三种状态 引入线程后,进程只作为除CPU外系统资源的分配单元,线程则作为CPU的分配单元。这样同一进程内线程的切换开销很小。 线程的属性多线程的操作系统中,线程作为独立运行的基本单位,进程的执行实际上是进程的某个线程在执行 线程是一个轻型实体,不拥有系统资源,每个线程有唯一的标识符和线程控制块。 不同的线程可以执行相同的程序,同一个服务程序被不同用户调用时,操作系统建成不同的线程 同一进程各个线程共享进程拥有的资源 线程是处理机调度的独立单位,多个线程可以并发执行。在单CPU计算机中线程交替的占用CPU,多CPU中线程可以同时的占有不同的CPU 线程的实现线程分为两类:用户级线程 和 内核级线程 用户级线程中,线程的管理工作由应用程序完成,内核意识不到线程的存在。 内核级线程中,线程的管理工作由内核完成,引用程序没有进行线程管理的代码,只有一个内核级线程的编程接口,线程的调度也是在内核线程的基础上完成的。 还有组合式的方式 进程与线程的比较调度引入线程的操作系统中,线程是调度和分派的基本单位。在同一个进程的中,线程的切换不会引起进程的切换。 拥有资源进程是拥有除CPU外其他资源的基本单位,程序运行所需要的必要资源(程序、PCB、堆栈)都由进程所有。一般而言线程不占有系统资源(除了一些必不可少的资源),其访问隶属于进程的资源 并发性进程之间可以并行运行,同一进程的线程间也可以并发运行 创建和开销进程的创建和撤销,系统都要为之创建、回收PCB,分配和回收资源。操作系统付出的代价比较大。而线程的创建和撤销比较简单。 进程调度调度层次作业调度高级调度,主要任务是按一定原则从外存中将处于后备状态的作业挑选1个或几个,分配内存、输入输出等资源,建立相应进程。使得他们拥有竞争处理机的权力(内存与辅存之间的调度) 中级调度进程的挂起与就绪 进程调度低级调度,某种方法和策略从就绪队列中选取一个进程,为其分配处理机。进程调度是最基本的调度,频率很高,一般几十毫秒一次 调度算法先来先服务(FCFS)算法FCFS是一种最简单的调度算法,从后备作业队列中选择最先进入该队列作业调度 FCFS是不可剥夺算法,长作业会使后到的短作业长期等待。 特点:算法简单,效率低,对长作业有利,有利于CPU繁忙性作业 短作业优先(SJF)算法从后背队列中选择一个或若干个估计运行时间最短的作业调入内存运行 特点:对长作业不利,如果短作业源源不断,会使得长作业一直处于饥饿状态。 优先级调度算法优先级调度算法每次从后背队列中选取优先级最高的一个或几个作业 特点:优先级调度可以剥夺式占有,也可以非剥夺式占有 高响应比优先高响应比有限主要用于作业调度,该算法是对FCFS和SJF算法的一种平衡,计算每个作业的响应比。 响应比的计算为(等待时间+要求服务时间)/ 要求服务时间 时间片轮转调度算法时间片轮转算法适用于分时系统,系统将所有就绪的进程按照到达时间排成一个序列,进程调度总是选择就绪队列中的第一个进程执行。但是仅能运行一个,如100ms。 特点:受系统响应时间影响、队列进程数目、进程长短影响较大 多级反馈队列调度算法多级反馈队列调度算法是时间片轮转调度算法和优先级调度算法的综合和发展 1) 设置多个就绪队列,为各个队列赋予优先级,1、2、3等等 2) 赋予各个队列中时间片大小不同,优先级高时间片越小 3) 一个进程进入内存后首先放入1级队列末尾,FCFS原则等待,如果其能够完成,则撤离系统,否则放入2级队列的末尾,依次向下执行 4) 仅当1级队列为空时,调度程序调度2级队列中的进程,依次类推。 进程同步临界区虽然多个进程可以共享系统中的资源,但许多资源一次只能被一个进程使用,把一次仅允许一个进程使用的资源称为临界资源。 // entry // critical section // exit section 同步与互斥同步:进程之间具有直接制约关系,进程之间需要按照一定的次序进行 互斥:进程之间的间接制约关系,不能同时访问临界区 信号量信号量是一个整形变量,可以被定义为两个标准的原语wait(S) signal(S) 即P、V操作 P操作 如果信号量大于0,执行 -1操作,如果等于0,执行等待信号量大于0 V操作 对信号量完成加1操作,唤醒睡眠的进程 123456789101112typedef int semaphoresemaphore mutex = 1 void P1(){ P(&mutex); //临界区 V(&mutex);}void P2(){ P(&mutex); //临界区 V(&mutex);} 使用信号量实现生产者-消费者问题问题描述:使用一个缓冲区来保存物品,只有缓冲区没满,生产者才可以放入物品;只有缓冲区不空,消费者可以拿走物品 由于缓冲区输入临界资源,需要一个互斥量mutex来完成缓冲区的互斥访问 为了同步生产者和消费者的行为,需要记录缓冲区物品数量,数量可以用信号量表示,empty记录空缓冲区,full记录满缓冲区 12345678910111213141516171819202122232425262728# define N 100typedef int semahporesemaphore mutex = 1;semaphore empty = N;semaphore full = 0;void producer(){ while(True){ int item = produceItem(); P(&empty); P(&mutex); Item.push(item); V(&mutex); V(&full); }}void consumer(){ while(True){ P(&full); P(&mutex); int item = Item.top(); Item.pop(); consume(item); V(mutex); V(&empty()) }} 管程使用信号量机制生产消费问题客户端代码需要很多控制,管程作用是把控制的代码独立出来。 管程有一个重要作用:一个时刻只能有一个进程使用。进程不能一直占用管程,不然其他程序都无法使用 管程的生产者消费者实现 12345678910111213141516171819202122232425262728293031323334353637383940monitor ProducerConsumer condition full, empty; integer cout :=0; function insert(item:integer); begin if count = N then wait(full) insert_item(item); count := count + 1; if count = 1 then signal(empty); end; function remote(item:integer); begin if count = 0 then wait(empty); item = remove_item(item); count := conut-1; if count = N-1 then signal(full); return item; end;end monitor;//生产者客户端procedure producerbegin while true do begin item = produce_item; ProducerConsumer.insert(item) endend;procedure consumerbegin while true do begin item = ProducerConsumer.remove() consume(item); endend; 读者-写者问题问题描述: 控制多个进程对数据进行读、写操作,但是不允许读-写和写-写操作同时进行 用一个count表示读进程数量,分别用read_mutex 和write_mutex 作为读锁和写锁 12345678910111213141516171819202122typedef int semaphoresemaphore count = 0;semaphore read_mutex = 1;semaphore write_mutex = 1;void read(){ P(&read_mutex); count++; if(count==1) P(&write_mutex); V(&read_mutex); read(); p(&read_mutex); count--; if(count==0) V(&write_mutex); V(&read_mutex);}void write(){ P(&write); write(); V(&write);} 哲学家进餐问题问题描述:五个哲学家围着一张圆桌,每个哲学家面前放着食物,哲学家有两种活动:吃饭与思考,吃饭时,他拿起左边及右边的筷子,并且一次只能拿一根 如果所有哲学家都拿左边的筷子,就会出现死锁,这样只需加一步,当哲学家拿起筷子时检查是否能同时拿起两根筷子,不然就等待 1234567891011121314151617181920212223242526typedef int semaphoresemaphore chop[5] = {1,1,1,1,1};semaphore mutex = 1;void process(){ while(true){ P(&mutex); /* if(chop[i]&&chop[(i+1)%5]) { P(chop[i]); P(chop[(i+1)%5]); } else{ V(&mutex); break; } */ P(chop[i]); P(chop[(i+1)%5]); V(&mutex); eat(); V(chop[i]); V(chop[(i+1)%5]); }} 死锁死锁的定义:多个进程因为竞争资源而造成的一种僵局(互相等待),若无外力作用,所有的进程都无法向前推进。 死锁四个必要条件: 互斥条件:进程要求对所分配的资源进行排他性控制,在一段时间内资源仅为一个进程所有。 不剥夺条件:进程所获得资源未使用完毕之前,不能被其他进程强行夺走,只能等获得资源的进程自己主动释放 请求和保持条件:进程已经至少保持了一个资源,但是又提出了新的资源请求,而该资源已被其他进程占有。此时进程被阻塞,但是对自己资源不释放。 循环等待条件:存在某一进程的循环等待链,链中每个进程已获得资源下个进程的请求。 死锁的处理策略死锁的处理便是破坏四个必要条件,使得死锁无法发生 鸵鸟策略把头埋在沙子里,假装问题没有发生 由于解决死锁问题的代价往往很高,鸵鸟策略在很多情况下可以取得更高的性能。 大多数操作系统,Unix、Linux、windows处理死锁都是采用鸵鸟策略 死锁预防 破坏互斥条件 对于可共享的资源竞争,不会发生死锁 破坏不剥夺状态 当一个进程无法获取其需要的资源时,将之前已获得的资源释放,待需要是再重新申请 破坏请求 和 保持条件 预先分配的静态方法,在进程运行前一次申请完它需要的所有资源。在资源不满足前不运行,一旦运行这些资源都归期所有。 破坏循环等待 资源顺序分配法,例如为资源编号,每个进程申请分配某个资源以后,再之后只能申请该编号以后的资源 死锁避免系统的安全状态:所谓安全状态,是系统能按照某种进程推进顺序(P1,P2,,)为每个进程分配资源,直至满足每个进程对资源的最大需求,使每个系统进程都能顺序完成,则(P1、P2,,)称为安全序列。如果无法找到安全序列,则系统处于不安全状态。 允许进程池动态的申请资源,但是每次分配资源前系统都会计算资源分配的安全性,如果分配资源不会导致系统进入不安全状态,将资源分配给进程;否则,进程等待 银行家算法 银行家算法是最著名的死锁避免算法。它的思想是,把操作系统看成银行家,操作系统管理的资源当成银行家管理的资金,向操作系统请求资源相当于向银行请求贷款。 进程申请资源时,系统评估该进程的最大需求资源,检查资源分配后系统是否还处于安全状态,由此来决定是否分配该资源 死锁检测和接触死锁检测死锁解除 资源剥夺法 挂起死锁进程,抢占其资源分配给其他进程 撤销进程法 强制撤销一些死锁进程 进程回退法 借助历史信息使一个或多个进程回退到系统不再死锁的地步","categories":[],"tags":[{"name":"操作系统","slug":"操作系统","permalink":"http://yoursite.com/tags/操作系统/"}]},{"title":"欢迎来到Eyc的博客!","slug":"欢迎来到Eyc的博客!","date":"2018-11-11T12:06:38.000Z","updated":"2018-12-07T02:04:45.894Z","comments":true,"path":"2018/11/11/欢迎来到Eyc的博客!/","link":"","permalink":"http://yoursite.com/2018/11/11/欢迎来到Eyc的博客!/","excerpt":"","text":"欢迎来到我的博客!","categories":[],"tags":[{"name":"other","slug":"other","permalink":"http://yoursite.com/tags/other/"}]},{"title":"HTTP","slug":"HTTP","date":"2018-11-11T11:54:23.000Z","updated":"2018-12-07T13:08:58.983Z","comments":true,"path":"2018/11/11/HTTP/","link":"","permalink":"http://yoursite.com/2018/11/11/HTTP/","excerpt":"HTTPHTTP方法客户端发送的请求报文第一行包含了方法字段 GET 获取资源,绝大多数请求是GET方法 HEAD 获取报文首部,和get方法一样但是不返回报文实体部分。主要用户URL有效性及资源更新日期时间 POST POST主要用来传输数据 PUT 上传文件(由于本身不带验证机制,存在安全机制一般不用) PATCH 部分修改资源 DELETE 与PUT功能相反,同样不带验证机制 OPTIONS 查询URL支持的方法 CONNECT 要求在与代理服务器通信时建立隧道,使用SSl和TSL TRACE 追踪路径,服务器会把通信路径返回客户端(通常不会用,易受攻击)","text":"HTTPHTTP方法客户端发送的请求报文第一行包含了方法字段 GET 获取资源,绝大多数请求是GET方法 HEAD 获取报文首部,和get方法一样但是不返回报文实体部分。主要用户URL有效性及资源更新日期时间 POST POST主要用来传输数据 PUT 上传文件(由于本身不带验证机制,存在安全机制一般不用) PATCH 部分修改资源 DELETE 与PUT功能相反,同样不带验证机制 OPTIONS 查询URL支持的方法 CONNECT 要求在与代理服务器通信时建立隧道,使用SSl和TSL TRACE 追踪路径,服务器会把通信路径返回客户端(通常不会用,易受攻击) GET与POST的区别作用GET用于获取资源,POST用于传输实体主体 参数GET和POST都能使用额外的参数,但是GET参数以查询字符串的形式出现在URL中,如http://127.0.0.1/Test/login.action?name=admin&password=admin 这个过程用户可见。而POST的参数存储在实体主体中。通过HTTP的POST机制。POST参数可以通过一些抓包工具(Fiddler)查看 因为URL只支持ASCLL码,因此GET参数中文会被编码,空格被编码为%20 安全安全的HTTP方法不会改变服务器状态,可以说他是只读的 GET方法安全的,POST不是,POST方法传输主体内容,这个内容可能是某个表单数据,服务器可能把其存入数据库中,这样状态就发生了改变 幂等性幂等性的HTTP方法,同样的请求被执行一次和连续执行多次的效果是一样的,服务器状态也是一样的。 所有安全的方法是幂等的,GET、HEAD、PUT、DELETE方法都是幂等的 而POST方法不是幂等的 XMLHTTPRequestXMLHTTP Request是一个API,它为客户端和服务器之间传输数据的功能。他提供了一个URL来获取数据的简单方式,而且不会使整个页面刷新。这使得网页只更新一部分不会打扰到用户,在AJAX中被大量使用 在xmlhttprequest中POST会先发送header再发送data(当然也和浏览器的做法相关) 而get方法会一起发送 其他Get传输数据量小,因为受URL的限制,但是效率高;POST可以传输大量数据,文件只能通过POST传递 Get方式只支持ASCII字符,向服务器传输的中文字符可能会乱码。POST支持标准字符集,可以正确的传递中文字符 HTTP 首部 Request Header 解释 某度首页示例 Accept 客户端能够接受的内容类型 text/html Accept-Encoding 浏览器可以支持的web服务器返回内容压缩编码类型 gzip Accept-Language 浏览器可以接受的语言 zh-CN Cache-Control 指定请求和相应遵循的缓存机制 max-age=0 Connection 表示是否需要持久连接 keep-alive Cookie HTTP请求发送时,会把保存在请求域名下的所有cookie值一起发送给web服务器 键值对 Host 请求服务器的域名和端口号 www.baidu.com Upgrade-Insecure-Requests 浏览器可以处理HTTPS协议 1 User-Agent 发出请求的用户信息 Mozilla/5.0 HTTP 首部 Response Header 解释 某度首页示例 Cache-Control 告诉所有的缓存机制是否可以缓存及缓存哪种类型 private Connection 是否保持持久连接 keep-alive Content-Encoding 返回内容压缩编码类型 gzip Content-type 返回内容的MIME类型 text/html charset=utf-8 Date 原始服务器消息发出时间 Wed 03 Oct2018 12:04:45 GMT Expires 响应过期的时间 Wed 03 Oct2018 12:04:45 GMT Server Web服务器软件名称 BWS1.1 Set-Cookie 设置浏览器缓存 BDSVRTM=114; path=/ Transfer-Encoding 文件传输编码 chunked HTTP状态码HTTP中状态码大致分为五大类: 100-199 信息性状态码 100 continue: 收到了请求的初始部分,请客户端继续 200-299 成功状态码 200 OK:请求被正常处理 204 No Content: 请求被接受,但是响应报文只有首部和状态行,没有实体部分 206 Partial Content: 客户端只请求了一部分的资源,服务器只针对请求的部分资源进行返回 300-399 重定向状态码 301 Moved Permanently: 永久重定向 302 Found: 临时重定向,资源被临时移动了 303 See Other: 表示用户请求的资源代表着另一个URI,应该使用GET去获取资源 304 Not Modified: 当客户端发送的请求附带条件时,服务器找到资源但未符合请求 307 Temporary Redirect: 同302,临时重定向 400-499 客户端错误状态码 400 Bad Request: 告知客户端它发送了一个错误的请求 401 Unauthorized: 请求需进行认证 403 Forbidden: 请求被服务器拒绝了 404 Not Found:服务器无法找到对应资源 405 Method Not Allowed:请求中带有不支持的方法 500-599 服务器错误状态码 500 Internet Server Error: 服务器内部错误 502 Bad GateWay: 代理或网关服务器从下一条链路收到了伪响应 503 Server Unavailable: 服务器正忙 504 GateWay Timeout: 一个代理网关等待另一服务器时超时了 持续性连接和非持续性连接1. 定义非持续连接:每个请求、相应都经一个而单独的TCP连接发送 持续连接:所有的请求相应通过相同的TCP连接发送 2. 区别如果采用非持续连接,打开包含一个HTML文件和10个内联对象网页,HTTP需要建立11次TCP连接才能把文件从服务器传到客户机。而如果采用持续连接,HTTP建立一次TCP就把文件从服务器传到客户机 每次TCP连接必需建立和断开(通过三次握手建立、四次挥手断开),这都需要占用CPU资源,这样占用客户机和服务器的CPU时间大大减少 每次连接,客户端和服务器都必须分配发送和接收缓存,这意味着影响服务器和客户机的资源,着同样要占用CPU时间 对于大量对象组成的文件,TCP低速启动算法会限制服务机向客户机传送对象的速度。使用HTTP/1.1后,大多数对象以最大速率传输 3. HTTP/1.0 + keep-aliveHTTP/1.1 中默认保持持久连接,但是1.0版本的HTTP需要设置 connection:keep-live connection:keep-live 是HTTP1.0 浏览器和服务器的实验性扩展 Cookie 与 Session由于HTTP是无状态协议,为了保持客户端与服务器的一些关系,便有了cookie和session 1.cookiecookie是服务器在本地机器上存储的小段文本并随每一个请求发送至同一个服务器。网络服务器用HTTP头想客户端发送cookies, 在客户终端,浏览器解析这些cookies 并将它们保存为一个本地文件,它们在会在下一次对服务器的请求时附上这些cookies。 内容 过期时间会话cookie:若不设置过期时间,则表示这个cookie的生命周期为浏览器会话期间,若关闭浏览器,cookie就会消失。这种生命周期的cookie被称为会话cookie 持久cookie:若设置了过期时间,浏览器会把cookie存储到硬盘上(当然用户可以选择拒绝),关闭后再打开这些cookie仍然有效 2.sessionsession机制是一种服务端的机制,服务器利用一种类似于散列表的结构来保存信息 当程序需要为某个客户端的请求创建session时,服务器检查这个客户端是否包含了一个session标志,称为session_id, 如果检测到说明该客户曾创建过ID,服务器会把这个ID检索出来使用(或者未检测到新建一个),session_id 既不会重复也不容易被找到仿造。 session_id的储存 保存这个session_id可以采用cookie,这样交互过程中浏览器可以把这个标志返回给服务器。一般该变量名与session有关,如github的session ID即名为user_session 由于cookie可以被认为的禁止,必须有其他机制保证session_id传回服务器,经常使用的一种方法是URL重写,即直接把session_ID附在URL后面。作为路径的附加信息或查询字符 另一种技术是表单隐藏字段,服务器自动修改表单加入一个隐藏字段,便于传回session_id 3. cookie与session的区别存取方式不同cookie只能保存ASCII字符,Unicode及二进制数据需要编码,cookie不能直接存取java对象,存储略微复杂的信息较为艰难。而session中能够存取任何类型的数据,包括不限于string、integer、list、map,而且可以直接存取java的类对象,十分方便 隐私策略不同cookie存储在客户端阅读器中,对客户端可见,客户端可以窥探甚至是修改cookie内容。而session存储在服务器上对用户透明,不存在泄露风险。cookie可以像google及baidu一样将敏感信息加密后保存,在服务器上进行解密。 有效时间不同由于session依赖于session_id的cookie,而session_id过期时间默许为-1,关闭浏览器即消失。而cookie可以设置长期的保存 服务器压力不同由于不同的储存方式,储存在客户点的cookie不会给服务器造成压力,而session由于存在服务器上,对服务器压力较大 浏览器支持不同cookie是需要客户端浏览器支持的,假如客户端禁用了cookie或者不支持cookie,则会话跟踪会失效。 假如客户端不支持cookie,就需要运用session及url地址重写。需要注意的是一切用到session的程序url都需要进行重写,否则session会话还是会失效 跨域支持不同cookie支持跨域名访问,一切以相同后缀的域名均可以访问该cookie,跨域名cookie被广泛应用 session仅在当前域名有效 其他一个web页面的请求过程1.DHCP配置主机信息(找本机IP) 主机生成一个DHCP请求报文,并将这个报文放入目的端口67和源端口68的UDP报文段中 该报文段放在一个广播IP地址(255.255.255.255)和源IP地址(0.0.0.0)的IP数据报中 该数据报被放在MAC帧中,目的地址FF:FF:FF:FF:FF:FF,广播到交换机连接的所有设备 交换机的DHCP服务器收到广播帧后,不断向上解析得到IP、UDP、DHCP报文,之后生成DHCP的ACK报文,该报文包括 IP地址、DNS服务器IP地址、默认网关路由器的IP地址和子网掩码,再经过层层封装到MAC帧中 该帧的目的地址是主机的mac地址,主机收到该帧后分解得DHCP报文,之后配置IP地址,子网掩码、DNS服务器IP地址,安装默认网关 2.ARP解析网关MAC地址(找网关MAC地址) 主机通过浏览器生成一个TCP套接字,为了发送HTTP请求,需要知道网站对应的IP地址 生成一个DNS查询报文,端口53(DNS服务器) DNS查询报文放入目的地址为DNS服务器IP地址的IP数据报中 IP数据报放入一个以太网帧中,发送至网关路由器 DHCP过程只知道网关的IP地址,为了获取网关的MAC地址,需要用ARP协议 主机生成一个目的地址为网关路由器IP的ARP查询报文,放入一个广播帧中,并发送这个以太网帧,交换机将其发送给所有的连接设备 网关接收到该帧后,分解得到ARP报文,发现IP地址与自己想匹配,发送一个ACK报文回应自己的MAC地址 3.DNS解析域名(找服务器IP) 知道了DNS的MAC地址后就可以继续DNS解析过程 网关接收到DNS查询报文后,抽出IP数据报,并根据该表选择该转发的路由器 路由器根据内部网关协议(RIP、OSPF)和外部网关协议(BGP)配置路由器到DNS的路由表项 之前的DNS报文到DNS服务器后,照常依次抽出报文,在DNS库中查找解析域名 找到DNS记录后发送DNS回答报文,然后将其放入UDP报文段、IP数据报,通过路由器反转发回网关路由器,经过交换机到主机 4. HTTP请求页面 有了HTTP服务器的IP地址后,主机便可以生成TCP套接字,向web服务器发送HTTP get报文 建立HTTP连接前需要进行TCP连接,进行三次握手,过程略 建立连接后发送HTTP的GET报文,交付给HTTP服务器 HTTP服务器从TCP中读出报文,生成HTTP相应报文,将web页面放入HTTP报文主体中发挥主机 浏览器收到HTTP相应报文后抽取WEB页面内容进行渲染,显示web页面","categories":[],"tags":[{"name":"计算机网络","slug":"计算机网络","permalink":"http://yoursite.com/tags/计算机网络/"}]},{"title":"TCP与UDP","slug":"TCP与UDP","date":"2018-11-11T11:49:28.000Z","updated":"2018-12-07T13:09:24.376Z","comments":true,"path":"2018/11/11/TCP与UDP/","link":"","permalink":"http://yoursite.com/2018/11/11/TCP与UDP/","excerpt":"TCPTCP 的连接与断开TCP与UDP部分感觉与网络考研书上讲的比较接近,重点内容包括TCP的三次握手、四次挥手、TCP可靠传输、流量控制、拥塞控制","text":"TCPTCP 的连接与断开TCP与UDP部分感觉与网络考研书上讲的比较接近,重点内容包括TCP的三次握手、四次挥手、TCP可靠传输、流量控制、拥塞控制 TCP三次握手 第一步:客户机的TCP首先向服务器的TCP发送一个请求连接报文段。这个特殊的报文段不包含应用层数据,其中首部的SYN标志位被置位1.另外客户机会随机选择一个起始序号seq=x 第二步:服务器的TCP收到请求的报文后,如同意建立连接,就向客户机发回确认,并为该TCP分配TCP缓存和变量。在确认报文段中,SYN和ACK被置为1,确认号的字段值为x+1 第三步:客户机收到确认报文段后向服务器给出确认,并且给该连接分配内存和变量,ACK置1,确认号为y+1 为什么TCP进行三次握手?本质上TCP协议的三次握手需要解决这样一个问题:在不可靠的信道上(IP数据报尽最大努力交付)完成可靠的数据传输。而全双工的连接建立需要双方的连接请求和确认,这样最少需要使用三次握手才能建立连接 至于为什么三次是最少,客户端服务器二者最少都需要向对方发送一个同步报文(SYN),但是如果只有这两次握手,服务器就只能接受连接,无法确认连接;设想如果服务器接受一个SYN报文就建立连接,那客户端因为阻塞原因重发了N个SYN同步报文 ,服务器每接受到一个就需要建立一次连接,这是不堪设想的。所以只有当服务器接收到客户端第二次的ack确认报文后才会建立连接 TCP四次挥手断开连接 客户端请求断开FIN1(客户端无更多数据) 服务器ACK确认(收到,但是仍有数据传输) 服务器请求断开FIN2(服务器无更多数据) 客户端ACK确认 四次挥手中的状态 FINWAIT1 : 当客户端在established状态想要断开连接,主动向服务器器发送FIN报文,此时该socket便进入FINWAIT1状态 FINWAIT2: 这时客户端已经收到对方的确认ack,但是服务器可能还有数据未发送完成,还需要接受对方的数据,处于半连接(半关闭)状态 TIME_WAIT: 客户端接受到了对方的确认报文、也接收到了对方的fin报文,现在发出了最后的确认报文,进入一个2MSL的等待状态,如果等待状态没有重发便进入结束状态 CLOSE_WAIT : 服务器确认了客户端的终止请求,还有数据待发送 LAST_CHECK: 服务器方发送FIN后,最后等待对方的ACK报文 CLOSED : 连接中断 为什么需要四次握手TCP是全双工的协议,通信中双方都需要知道对方的存在,而在结束时,双方也同时需要发送断开与确认对方的断开信息。当主机1发送FIN希望断开连接时,主机1已经没有要发送的数据了,但是其还是有可能接受主机2发送的数据,此时单方面的连接断开了,这时处于半连接状态。而只有主机2也向主机1发送断开请求并确认,双方才完全的断开。 TCP的半打开状态如果TCP连接中一方已经关闭或异常终止另一方还不知道,这样的连接称为半打开状态。任何一端的主机都可能检测到这一情况,如果双方没有在半打开的连接上传输数据,双方就无法获悉异常。 半打开的一个常见原因是一方程序的非正常结束(断电、断网)如果A已经没有向B发送的数据,则B永远无法获悉A是否已经消失了。而当一方获取到异常的数据连接后(比如重启)直接进行复位(RST)处理 同时打开与同时关闭两个程序的同时打开与同时关闭是有可能的,例如A:port1 向B:port2 发送SYN同步信息的同时,B:port2 也向A:port1发送了一个SYN同步信息,此时双发收到对方的SYN后各自向对方回一个ack表示,确认,连接就正常建立了,这样一个打开需要四个报文段。 而同时关闭同理,也是双方同时发送FIN报文段,双方在ack确认,这样还是使用4个报文段双方完成了连接的关闭只不过此时双方都跳过了FINWAIT2阶段 TCP可靠传输TCP的任务是在IP层不可靠的、尽力而为的服务基础上建立的一种可靠的传输服务。TCP使用了校验、序号、确认和重传来达到这个目的,这里主要说一下重传,重传分两种 超时重传TCP 每发送一个报文段,就对这个报文段进行一次计时,只要计时器到期而没有收到确认的的报文,就会重传这一报文。一个报文发出到确认的时间成为RTT(Round-Trip Time),TCP使用一了一种动态适应算法来调整RTT,而时间阈值与该RTT相关 冗余ACK(快速重传)超时重传存在一个问题就是超时周期往往太长。幸运的是存在另一种方法,在超时前通过冗余ACK来较好的检测丢包情况。 TCP规定每当比期望序号大的失序报文段到达时,发送一个冗余ACK,指明下一接期待的序号.例如发送方发送了1、2、3、4、5的TCP报文段,其中2号报文段在传输中丢失,这样3、4、5号报文段对于B来说称为了失序报文段。在本例中,3、4、5都会发送一个针对1号报文段的冗余ACK表示自己想要接收2号报文段。TCP规定发送方收到同一个报文段的3个冗余ACK时,就可以认为跟在这个报文段之后的报文已经丢失,这种技术被称为快速重传 TCP流量控制TCP流量控制服务消除接受方缓存区溢出的可能,可以说是一个速度匹配服务。 流量控制基于滑动窗口。发送方维护一个接受窗口,在TCP首部的窗口字段中声明,表示接收方可以接受的窗口大小,防止接受方报文队列溢出。 TCP流量控制与链路层流量控制的区别是,TCP是端到端的流量控制,由发送方与接收方商议。而链路层的流量控制是相邻中间节点的流量控制。而且TCP滑动窗口可变,后者不可变 TCP拥塞控制慢开始和拥塞避免 慢开始算法 TCP连接建立时,先令cwnd窗口=1,即一个最大的报文段长度。每次收到一个确认报文段,将cwnd+1。使用慢开始算法,会使得每一个传输轮次后cwnd加倍,这样它会一种指数增长到一个限定值 拥塞避免算法 拥塞避免算法的做法是每经过一个往返时延RTT就增加一个MSS的大小,这样cwnd按线性规律增长 网络拥塞的处理 使用慢开始算法,cwnd从1开始指数增大 当cwnd达到ssthresh时,启用拥塞避免算法,cwnd线性增大 当网络发生拥塞时,将cwnd置1从头慢开始算法;将ssthresh设置为拥塞值的一半 快重传和快恢复 快重传 当发送方收到3个冗余ACK的报文时,直接重传对方尚未收到的报文段而不必等待 快恢复 当发送端连续接收到3个冗余ACK后,执行乘法减小算法。将慢开始的额门限ssthresh设置成拥塞cwnd的一半,将cwnd值设置为拥塞cwnd的一半,并执行拥塞避免算法 UDPUDP简介UDP只做传输协议能做的最少工作,只在IP数据服务上增加了两个最基本的服务:复用和分用 以及差错检测 UDP首部只有8个字节,分为四个字段:源端口、目的端口、UDP长度、UDP校验和 UDP无需建立连接 UDP不维护连接状态 UDP分组首部开销小 UDP没有拥塞控制,适合容许一些数据丢失但是不允许有较大时延的应用 TCP和UDP的区别TCP和UDP协议特性的区别,主要从连接性、可靠性、有序性、拥塞控制、传输速度、头部大小来讲 TCP面向连接,UDP无连接。TCP3次握手建立连接,UDP发送前不需要建立连接 TCP可靠,UDP不可靠,TCP丢包有确认重传机制,UDP不会 TCP有序,会对报文进行重排;而UDP无序,后发送信息可能先到达 TCP必须进行数据验校,UDP的校验可选 TCP有流量控制(滑动窗口)和拥塞控制,UDP没有 TCP传输慢,UDP传输快,因为TCP要建立连接、保证可靠有序,还有流量、拥塞控制 TCP包头较大(20字节)UDP较小(8字节) 基于TCP协议:HTTP/HTTPS、Telnet、FTP、SMTP 基于UDP的协议 DHCP、DNS、SNMP、TFTP、BOOTP","categories":[],"tags":[{"name":"计算机网络","slug":"计算机网络","permalink":"http://yoursite.com/tags/计算机网络/"}]},{"title":"Python测试","slug":"Python测试","date":"2018-11-10T09:49:22.000Z","updated":"2018-11-10T09:53:30.766Z","comments":true,"path":"2018/11/10/Python测试/","link":"","permalink":"http://yoursite.com/2018/11/10/Python测试/","excerpt":"测试测试指通过运行程序以确定它是否按预期工作。我们通常需要将代码和规范结合起来,进行各种路径探索,并在基础上探究一种启发式方法。基于代码探索的路径启发式方法称为白盒测试,基于规范路径的启发式方法称为黑盒测试 测试一般分为两个阶段。第一个阶段称为单元测试。这个阶段测试者构建并执行测试,来确定代码每个独立单元是否正常工作。第二个阶段称为集成测试,用来确定整个程序是否能按预期进行。实际工作中需要不断重复这两个阶段 调试是一种需要学习的技能,好消息是学会调试并不难 人们至少花费了40年建立被称为‘调试器’的工具,所有流行的Python IDE中都带有调试器工具,这些调试工具帮助人们找到程序中的错误,但事实上帮助并不大。更重要的是接近问题,经验丰富的程序员可能根本不用调试工具,也许他们认为最重要的调试工具是 print()","text":"测试测试指通过运行程序以确定它是否按预期工作。我们通常需要将代码和规范结合起来,进行各种路径探索,并在基础上探究一种启发式方法。基于代码探索的路径启发式方法称为白盒测试,基于规范路径的启发式方法称为黑盒测试 测试一般分为两个阶段。第一个阶段称为单元测试。这个阶段测试者构建并执行测试,来确定代码每个独立单元是否正常工作。第二个阶段称为集成测试,用来确定整个程序是否能按预期进行。实际工作中需要不断重复这两个阶段 调试是一种需要学习的技能,好消息是学会调试并不难 人们至少花费了40年建立被称为‘调试器’的工具,所有流行的Python IDE中都带有调试器工具,这些调试工具帮助人们找到程序中的错误,但事实上帮助并不大。更重要的是接近问题,经验丰富的程序员可能根本不用调试工具,也许他们认为最重要的调试工具是 print() Python中提供了单元测试函数1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950```pythonimport unittestclass TestIntSet(unittest.TestCase): def setUp(self): print('before Testing...') def test__init__(self): s = IntSet() self.assertTrue(isinstance(s.getMembers(), list)) def test_insert(self): s = IntSet() s.insert(1) self.assertEqual(s.getMembers(), [1]) s.insert(2) self.assertEqual(s.getMembers(), [1,2]) def test_member(self): s = IntSet() s.insert(1) self.assertTrue(s.member(1)) self.assertFalse(s.member(2)) def test_remove(self): s = IntSet() s.insert(1) self.assertTrue(s.member(1)) s.remove(1) self.assertFalse(s.member(1)) def test_getMembers(self): s = IntSet() s.insert(1) s.insert(2) self.assertEqual(s.getMembers(), [1,2]) def test__str__(self): s = IntSet() s.insert(5) s.insert(1) s.insert(2) self.assertEqual(s.__str__(), '{1,2,5}') def tear_Down(self): print('after Testing...')if __name__ == '__main__': unittest.main(argv=['ignored', '-v'], exit=False)","categories":[],"tags":[{"name":"Python","slug":"Python","permalink":"http://yoursite.com/tags/Python/"}]},{"title":"Python面向对象","slug":"Python面向对象","date":"2018-11-10T08:48:28.000Z","updated":"2018-11-10T09:49:09.130Z","comments":true,"path":"2018/11/10/Python面向对象/","link":"","permalink":"http://yoursite.com/2018/11/10/Python面向对象/","excerpt":"类与面向对象编程面向对象编程——Object Oriented Programming,简称OOP,是一种程序设计思想。OOP把对象作为程序的基本单元,一个对象包含了数据和操作数据的函数。 面向过程的程序设计把计算机程序视为一系列的命令集合,即一组函数的顺序执行。为了简化程序设计,面向过程把函数继续切分为子函数,即把大块函数通过切割成小块函数来降低系统的复杂度。 而面向对象的程序设计把计算机程序视为一组对象的集合,而每个对象都可以接收其他对象发过来的消息,并处理这些消息,计算机程序的执行就是一系列消息在各个对象之间传递。 在Python中,所有数据类型都可以视为对象,当然也可以自定义对象。自定义的对象数据类型就是面向对象中的类(Class)的概念。","text":"类与面向对象编程面向对象编程——Object Oriented Programming,简称OOP,是一种程序设计思想。OOP把对象作为程序的基本单元,一个对象包含了数据和操作数据的函数。 面向过程的程序设计把计算机程序视为一系列的命令集合,即一组函数的顺序执行。为了简化程序设计,面向过程把函数继续切分为子函数,即把大块函数通过切割成小块函数来降低系统的复杂度。 而面向对象的程序设计把计算机程序视为一组对象的集合,而每个对象都可以接收其他对象发过来的消息,并处理这些消息,计算机程序的执行就是一系列消息在各个对象之间传递。 在Python中,所有数据类型都可以视为对象,当然也可以自定义对象。自定义的对象数据类型就是面向对象中的类(Class)的概念。 面向对象思想的优点 可重用性 编写可重用模块,比如类 可扩展性 要求应用软件能够很方便、很容易的进行扩充和修改 可管理性 采用封装了数据和操作的类作为构建系统的部件,使项目组织更加方便合理 抽象数据类型与类抽象数据类型概念十分简单,抽象数据类型是由一个由对象及对象上操作组成的集合,对象和操作被捆绑成一个整体,可以从程序的一部分传递到另一部分。这个过程中不仅可以使用对象的数据属性,还可以使用对象上的操作。 类的基本思想是数据抽象和封装,数据抽象是一种依赖于接口和实现的分离编程技术。类的接口包含用户所能执行的操作,类的实现则包括类的数据成员、负责接口实现的函数以及定义类的各种私有函数。封装实现了类接口和实现的分离,封装后的类隐藏了实现细节,类用户只能访问接口而无法访问实现部分————-《c++ Primer》 继承在OOP程序设计中,当我们定义一个class的时候,可以从某个现有的class继承,新的class称为子类(Subclass),而被继承的class称为基类、父类或超类(Base class、Super class) Exp11234567891011121314151617181920212223242526272829class IntSet: \"\"\"实现一个整数集合\"\"\" def __init__(self): \"\"\"创建一个空的整数集合\"\"\" self.__vals = [] def insert(self, e): if e not in self.__vals: self.__vals.append(e) def member(self, e): return e in self.__vals def remove(self, e): try: self.__vals.remove(e) except: raise ValueError(str(e) + 'not found') def getMembers(self): return self.__vals[:] def __str__(self): self.__vals.sort() result = '' for e in self.__vals: result = result + str(e) + ',' return '{'+ result[:-1] + '}' Exp212345678910111213141516171819202122232425262728293031323334import datetimeclass Person: def __init__(self, name): self.name = name try: lastBlank = name.rindex(' ') self.lastName = name[lastBlank+1:] except: self.lastName = name self.birthday = None def getName(self): return self.name def getLastName(self): return self.lastName def setBirthday(self, birthdate): self.birthday = birthdate def getAge(self): if self.birthday == None: raise ValueError('not set birthday') return datetime.date.today().year-self.birthday.year def __lt__(self, other): if self.lastName == other.lastName: return self.name < other.name return self.lastName < other.lastName def __str__(self): return self.name Exp312345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061class Student(Person): nextIdNum = 0 def __init__(self, name): Person.__init__(self, name) self.idNum = Student.nextIdNum Student.nextIdNum += 1 def getIdNum(self): return self.idNum def __lt__(self,other): return self.idNum < other.idNumclass Grades: def __init__(self): self.students = [] self.grades = {} self.isSorted = True def addStudent(self, student): if student in self.students: raise ValueError('Duplicate student') self.students.append(student) self.grades[student.getIdNum()] = [] self.isSorted = False def addGrade(self, student, grade): try: self.grades[student.getIdNum()].append(grade) except: raise ValueError('Student not in mapping') def getGrades(self,student): try: return self.grades[student.getIdNum()][:] except: raise ValueError('Student not in mapping') def getStudents(self): if not self.isSorted: self.students.sort() self.isSorted = True return self.students[:] def gradeReport(self): report = '' for s in self.getStudents(): tot = 0.0 numGrades = 0 for g in self.getGrades(s): tot += g numGrades +=1 try: average = tot/numGrades report = report + '\\n' + str(s) + '\\'s mean grade is '+ str(average) except ZeroDivisionError: report = report + '\\n' + str(s) + 'has no grade ' return report","categories":[],"tags":[{"name":"Python","slug":"Python","permalink":"http://yoursite.com/tags/Python/"}]},{"title":"Python异常与断言","slug":"Python异常与断言","date":"2018-11-10T08:13:57.000Z","updated":"2018-11-10T08:29:48.665Z","comments":true,"path":"2018/11/10/Python异常与断言/","link":"","permalink":"http://yoursite.com/2018/11/10/Python异常与断言/","excerpt":"异常与断言异常“异常”通常被定义为“不符合规范的东西”但是在Python中,异常十分常见,简直到处都是。实际上,Python库中所有的模块都使用了异常 我们经常将异常当做致命错误处理,异常发生时程序会终止,我们回到代码试图搞清楚为什么出错,程序因为一个异常被抛出而终止时,我们称程序抛出了一个未处理异常 很多时候,异常是程序员应该预料到的事情,比如程序试图打开一个不存在的文件。","text":"异常与断言异常“异常”通常被定义为“不符合规范的东西”但是在Python中,异常十分常见,简直到处都是。实际上,Python库中所有的模块都使用了异常 我们经常将异常当做致命错误处理,异常发生时程序会终止,我们回到代码试图搞清楚为什么出错,程序因为一个异常被抛出而终止时,我们称程序抛出了一个未处理异常 很多时候,异常是程序员应该预料到的事情,比如程序试图打开一个不存在的文件。 下面是一个典型异常的例子 12345SuccessNum = input('Enter SuccessNum: ')totalNum = input('Enter totalNum: ')ac = int(SuccessNum)/int(totalNum)print('The AC ratio is',ac)print('Now here') 该段程序在大多数情况下是运行良好的,但是如果用户输入中出现了0,(或者干脆直接跳过)程序就会崩溃,这显然不是我们乐意看到的我们可以以下方式改写这段程序 12345678910try: SuccessNum = input('Enter SuccessNum: ') totalNum = input('Enter totalNum: ') ac = int(SuccessNum)/int(totalNum) print('The AC ratio is',ac)except ZeroDivisionError: print('Failure ! totalNum is 0')except ValueError: print('Error ! No input')print('Now here') try语句按照如下方式工作; 首先,执行try子句(在关键字try和关键字except之间的语句) 如果没有异常发生,忽略except子句,try子句执行后结束。 如果在执行try子句的过程中发生了异常,那么try子句余下的部分将被忽略。如果异常的类型和 except 之后的名称相符,那么对应的except子句将被执行。最后执行 try 语句之后的代码。 如果一个异常没有与任何的except匹配,那么这个异常将会传递给上层的try中。一个 try 语句可能包含多个except子句,分别来处理不同的特定的异常。最多只有一个分支会被执行。处理程序将只针对对应的try子句中的异常进行处理,而不是其他的 try 的处理程序中的异常。 异常的讨论异常看上去不太友好(毕竟如果不处理程序会崩溃),但总好于其他方式。使用异常时,程序员需要编写一些代码来处理特定异常。如果忘记处理某个异常那么这异常抛出时,程序便立刻停止。当然这也是好事,线性错误总好于隐士错误,我们来讨论BUG的问题。 显性->隐性 显性错误有明显表现,如程序崩溃或运行时间长;隐性错误没有正常表现,程序正常结束,不会出任何问题–错了给出一个错误答案。 持续->间歇 持续性错误在每次相同的运行输入时都会发生,间歇性错误仅在某些时候出现,即使是相同的输入和环境 显性错误和持续性错误是最好的,开发者不会对这种程序抱任何幻想,没有人愚蠢到使用这种程序。优秀的程序员编写程序时,会尽量使程序时显性和持续性的,这种编程方式称为防御性编程 以隐性方式出错的程序特别危险,因为他们表面没有任何问题。人们使用它并对他产生依赖。逐渐的我们的人类社会将对软件产生依赖,这些软件来执行超过人类能力的计算,我们甚至不能判断这些软件的计算是否正确。因此我们根本意识不到这个情况。这样的程序可能已经造成了严重危害。 断言Python 语言为程序员提供了一种确保程序运行状态符合预期的简单方法。 123def cal(num): assert num>=0 return num**0.5","categories":[],"tags":[{"name":"Python","slug":"Python","permalink":"http://yoursite.com/tags/Python/"}]},{"title":"Python调试","slug":"Python调试","date":"2018-11-10T08:02:05.000Z","updated":"2018-11-10T08:29:13.077Z","comments":true,"path":"2018/11/10/Python调试/","link":"","permalink":"http://yoursite.com/2018/11/10/Python调试/","excerpt":"本教程内容主要来源于《Python编程导论》 6-8章 供2018年小学期 Python数据科学 课程使用 PowerBy 刘相 调试调试是一种需要学习的技能,好消息是学会调试并不难 人们至少花费了40年建立被称为‘调试器’的工具,所有流行的Python IDE中都带有调试器工具,这些调试工具帮助人们找到程序中的错误,但事实上帮助并不大。更重要的是接近问题,经验丰富的程序员可能根本不用调试工具,也许他们认为最重要的调试工具是 print()","text":"本教程内容主要来源于《Python编程导论》 6-8章 供2018年小学期 Python数据科学 课程使用 PowerBy 刘相 调试调试是一种需要学习的技能,好消息是学会调试并不难 人们至少花费了40年建立被称为‘调试器’的工具,所有流行的Python IDE中都带有调试器工具,这些调试工具帮助人们找到程序中的错误,但事实上帮助并不大。更重要的是接近问题,经验丰富的程序员可能根本不用调试工具,也许他们认为最重要的调试工具是 print() 调试过程可以看做是一个搜索过程,每次试验尽力缩减搜索空间。一个有效方法是,设计一个实验。该实验大致分为以下几步: 调查测试结果,弄清楚什么样的数据通过测试,什么样的数据未通过测试 建立一个符合现有数据的假设,假设可以很具体(403行x\\<y改成x=y即可)也可以很宽泛(50-100行的循环出了问题) 设计一个推翻上述假设的实验,你应该能够明确解释出实验每种可能的结果 将你的实验记录下来 Tips for 调试 排除常见错误(这个需要经验) coding everyday 不要问自己为什么没有像你想的那样做,而是考虑程序为什么像现在这样做 错误可能不在你认为会出错的地方 试着向他人解释你的问题 不要盲目相信书上的东西(不要相信文档)大家应该首先学会读文档:) 暂停调试,先写文档 出去散散步,回头接着做 调试案例123456789101112131415161718192021222324252627282930# 调试案例# 下面程序有若干处bugdef isPal(x): \"\"\" 假设x是列表 如果列表是回文,则返回True,否则返回False \"\"\" temp = x temp.reverse() if temp == x: return True else: return Falsedef silly(n): \"\"\" 假设n是正整数 接收用户的n个输入 如果所有的输入组成一个回文列表,则返回‘Yes’ 否则返回‘No’ \"\"\" for i in range(n): result = [] elem = input('Enter element: ') result.append(elem) if(isPal(result)): return True else: return False Python的可变数据类型与不可变数据类型python中的不可变数据类型,不允许变量的值发生变化,如果改变了变量的值,相当于是新建了一个对象,而对于相同的值的对象,在内存中则只有一个对象,内部会有一个引用计数来记录有多少个变量引用这个对象;可变数据类型,允许变量的值发生变化,即如果对变量进行append、+=等这种操作后,只是改变了变量的值,而不会新建一个对象,变量引用的对象的地址也不会变化. 可变数据类型(mutable) Dictionary(字典) List(列表) 不可变数据类型(unmutable) Number(数字) String(字符串) Tuple(元组) Bool(布尔值) 1234567891011121314151617181920212223242526272829303132# 不可变数据类型a = 15b = 15print(\"a的地址是\"+ str(id(a)))print(\"b的地址是\"+ str(id(b)))print(\"**********友爱的分割线************\")b = 20print(\"a的值是\"+ str(a))print(\"b的值是\"+ str(b))print(\"a的地址是\"+ str(id(a)))print(\"b的地址是\"+ str(id(b)))print(\"**********友爱的分割线************\")# 可变数据类型a = [1,2,3]b = ac = [1,2,3]print(\"a的地址是\"+ str(id(a)))print(\"b的地址是\"+ str(id(b)))print(\"c的地址是\"+ str(id(c)))print(\"**********友爱的分割线************\")b[0] = 4c[0] = 5print(\"a的值是\"+ str(a))print(\"b的值是\"+ str(b))print(\"c的值是\"+ str(c))print(\"a的地址是\"+ str(id(a)))print(\"b的地址是\"+ str(id(b)))print(\"c的地址是\"+ str(id(c)))","categories":[],"tags":[{"name":"Python","slug":"Python","permalink":"http://yoursite.com/tags/Python/"}]},{"title":"面经问题总结","slug":"面经问题总结","date":"2018-11-09T14:07:10.000Z","updated":"2018-12-07T13:10:22.433Z","comments":true,"path":"2018/11/09/面经问题总结/","link":"","permalink":"http://yoursite.com/2018/11/09/面经问题总结/","excerpt":"计算机网络1 http是长连接还是短连接,head里面有什么参数 ;put 和 post 有什么区别 ;http页面缓存机制 ;302code 含义 2 socket 编程 tcp三次握手 四次挥手 三次握手和四次挥手画图 为什么3次握手 2次挥手后客户端在做什么 3 访问一个网站的全过程 4 一个用户登录的全部逻辑 5 UDP、TCP、HTTP拥塞控制算法、慢启动算法","text":"计算机网络1 http是长连接还是短连接,head里面有什么参数 ;put 和 post 有什么区别 ;http页面缓存机制 ;302code 含义 2 socket 编程 tcp三次握手 四次挥手 三次握手和四次挥手画图 为什么3次握手 2次挥手后客户端在做什么 3 访问一个网站的全过程 4 一个用户登录的全部逻辑 5 UDP、TCP、HTTP拥塞控制算法、慢启动算法 6 GET/POST 区别与联系 7 防火墙和DNS劫持 8 http 介绍 http请求头、响应头都有哪些字段 9 netstat查看端口相关命令 10 TCP为什么是可靠的,如何进行拥塞控制 11 长连接与短连接 12 介绍一下常见的网络协议 13 http状态码有哪些 14 一个网站进入速度较慢,可以从哪些方面优化 15 web常见的安全问题有哪些、如何预防 16 http协议请求首部介绍一下 http正向代理和反向代理什么意思 17 GET\\POST 区别,form表单默认的请求方法是get、post?能用get完成的请求都能用post完成吗 18 输入一个网址后回车发生的全过程 19 TCP和UDP 区别,哪一个快一些?UDP丢包怎么处理,TCP拥塞控制机制,慢启动的缺点 20 TCP四次挥手的过程,为什么要主动关闭方等到2MSL的时间 21 http 头部字段keep-alive了解过吗?什么时候服务器知道可以断开连接呢? 22 http 状态码有哪些? 502、504 状态码是是么? 23 TCP和UDP 区别、TCP流量控制和拥塞控制、慢启动 24 访问taobao.com的全过程,如何获取每一阶段的耗时 25 ARP协议 ping命令 操作系统1 虚拟内存的作用是什么?分页有什么好处?分段呢 2 同步和异步的区别 3 进程和线程的区别,谁调度的进程 4 死锁的条件,如何检测死锁 5 死锁的必要条件,银行家算法 6 内核态线程和用户态线程的区别 7 非阻塞IO 8 程调度算法有哪些 9 通俗的语言,面对一个非程序员,解释进程与线程的区别 10 多线程中如何保证线程安全,如何创建守护进程 11 死锁是什么,为什么会产生死锁,怎么解决死锁问题,预防死锁、避免死锁 12 进程的同步进制有哪些? 进程的通信机制有哪些? 13 进程的状态转换图及转换事件 数据库1 数据库建立索引的原则有哪些 2 B+树与数据库索引 3 4 聚簇索引和非聚簇索引,一个连环索引对 a 升序排列,对 b 降序排雷,这个时候可以使用索引吗? 5 数据库加行锁怎么加? DML语句? 6 group by, having, order by limit 执行顺序 7 left join 和 inner join 的区别 8 mysql 的非运算如何去掉 not in 和 != 这样的过滤条件 9 聚簇索引和非聚簇索引,能不能对 like 出的东西加索引,能不能函数加索引 10 数据库事务的隔离级别,如何对数据库加锁,内连接和外连接 11 mysql 的索引 12 数据库如果有一张表特别大怎么办?(索引优化、加缓存、读写分离、业务拆分) 13 哪些字段可以建索引 14 mysql 储存引擎 15 数据库死锁的概念 16 数据库引擎有哪些,有什么区别,索引分别是怎么实现的? 17 介绍下数据库的索引 18 乐观锁和悲观锁 19 数据库索引,不同引擎的内部实现,B+树的具体性质 20 数据库索引,底层实现、mysql 两种引擎的区别,数据库查询特别慢怎么优化 21 主要使用的数据类型、数据库引擎了解吗,有什么区别 22 数据库事务、事务的隔离级别 SQL1 (1)写sql,Table T Sid:学号 Cid:课程编号 score:成绩,q1:查询平均成绩大于60分的同学的学号和平均成绩 q2:查询所有课程成绩小于60的同学的学号 2 给一张表,找出成绩在10-15名学生 分布式数据库1 redis 是线程安全的吗? 为什么是单线程 2 介绍 redis 3 redis 和 mysql 的不同,应用场景 4 用户什么格式的数据格式和用 redis 储存 5 redis mybatis 二级缓存","categories":[],"tags":[{"name":"面试基础","slug":"面试基础","permalink":"http://yoursite.com/tags/面试基础/"}]},{"title":"博客的重新开始","slug":"博客的重新开始","date":"2018-11-09T13:39:42.000Z","updated":"2018-12-23T07:29:11.995Z","comments":true,"path":"2018/11/09/博客的重新开始/","link":"","permalink":"http://yoursite.com/2018/11/09/博客的重新开始/","excerpt":"今天把写简历的时候参考了一下Dyc的简历,看到大家还是都有自己的博客的。想想自己也总结过东西,但是由于懒一直没有写博客的习惯。考虑了一下下定决心重新养成写博客的习惯,未来的几个月可能会总结很多东西,正好适合我去写一些东西。","text":"今天把写简历的时候参考了一下Dyc的简历,看到大家还是都有自己的博客的。想想自己也总结过东西,但是由于懒一直没有写博客的习惯。考虑了一下下定决心重新养成写博客的习惯,未来的几个月可能会总结很多东西,正好适合我去写一些东西。 目前离最早的实习面试还有四个月左右,我也已经投出第一篇论文,这一段时间要专注的去复习计算机基础知识了。趁着这一段时间去丰富自己的理论知识。感觉目前工程经历和科研经历都不少了,一路走过来奖项也拿了不少了,所欠缺的主要是对于理论知识的理解,另外对于一些较为需求的方向例如爬虫、redis做一个demo项目。加油!","categories":[],"tags":[{"name":"other","slug":"other","permalink":"http://yoursite.com/tags/other/"}]}]}