首页 文章 精选 留言 我的

精选列表

搜索[前端设计],共10000篇文章
优秀的个人博客,低调大师

前端学习笔记(7)css选择器(一)

选择器分类 简单选择器:针对某一特征判断是否选中元素。 复合选择器:连续的简单选择器,根据元素特征判断是否选中单个元素。 复杂选择器:由“(空格)“ “ >” “ ~” “ +” “ ||” 等符号连接的复合选择器,根据父元素或者前序元素检查单个元素。 选择器类别:用逗号分隔的复杂选择器,表示“或”。 简单选择器 简单选择器分类: 类型选择器 全体选择器 id选择器 class选择器 属性选择器 伪类选择器 伪元素选择器 类型选择器 根据标签名来选中元素。 div{ } 全体选择器 “*”可以选中任意元素。 *{ } id选择器 “#”后面跟id名。 #myid{ } class选择器 “.”后面跟class名 c 一个元素可以拥有多个类。下例中“.a” “.b” “ .c”都可以选中div标签。 <div class="a b c"></div> .a{ } 属性选择器 第一种,[att]: 选中带有att属性的元素 <input type="text"/> [type]{ } 第二种,[att=val]: 选中att属性值为val的元素 <input type="text"/> [type=text]{ } 第三种,[att~=val]: 选中属性att的值中包含指定词汇的元素 <img src="" title="img1 img2" /> [type~=img1]{ } 第四种,[att|=val]: 选中属性att值以val开头的元素,val必须为整个单词 <p lang="en-us">Hello World!</p> [type|=en]{ } 第五种,[att^=val]: 选中属性att值以val开头的元素 <input type="text"/> [type^=te]{ } 第六种,[att$=val]: 选中属性att值以val结束的元素 <input type="text"/> [type^=xt]{ } 第七种,[att*=val]: 选中属性att值包含val的元素 <input type="text"/> [type*=ex]{ } 属性选择器中val可能包含特殊字符,所以可以使用单双引号包裹起来规避特殊字符,也可以使用反斜杠转义。 伪类选择器 伪类选择器是一系列由 CSS 规定好的选择器,它们以冒号开头。伪类选择器有普通型和函数型两种。 树结构关系伪类选择器 :root表示树的根元素,html文档中根元素为html标签。 :empty表示没有子节点的元素,拥有空白文本也不能被选中。 :nth-child和:nth-last-child,nth-child规则如下。nth-last-child为从后向前。 :first-child和:last-child分别表示第一个和最后一个元素。 :only-child表示唯一子元素 S:nth-of-type和S:nth-last-of-type表示只从S只选中。(of-type系列同理) <div> <p>1</p> <a>2</a> <p>3</p> <p>4</p> </div> div p:nth-child(1){ //选中 1 } div p:nth-child(2){ // 没有选中 } div p:nth-of-type(2){ // 选中3 } :first-of-type和:last-of-type <div> <p>1</p> <a>2</a> <p>3</p> <p>4</p> </div> div p:first-child{ //选中1 } div a:first-child{ //没有选中 } div a:first-of-type{ //选中2 } :only-of-type <div> <p>1</p> <a>2</a> </div> div p:only-child{ //没有选中 } div p:only-of-type{ //选中p } 链接与行为伪类选择器 :any-link 表示任意的链接,包括 a、area 和 link 标签都可能匹配到这个伪类。 :link 表示未访问过的链接, :visited 表示已经访问过的链接。 :hover 表示鼠标悬停在上的元素。 :active 表示用户正在激活这个元素,如用户按下按钮,鼠标还未抬起时,这个按钮就处于激活状态。 :focus 表示焦点落在这个元素之上。 :target 用于选中浏览器 URL 的 hash 部分所指示的元素。 逻辑伪类选择器 :not目前只支持简单选择器 <div> <p class="p1">1</p> <p>2</p> </div> div p:not(.p1){ //选中2 } 伪元素选择器 伪元素选择器选中的是不存在的元素。常用的伪元素选择器有以下几种。 first-line 选用元素的第一行 first-letter 选中元素的第一个字母 before after first-line 必须出现在最内层的块级元素之内。所以下例代码中第一行为蓝色。 <div> <p id=a>First paragraph</p> <p>Second paragraph</p> </div> div>p#a { color:green; } div::first-line { color:blue; } span标签为行级元素,所以伪元素在span之外,第一行为绿色。 <div> <span id=a>First paragraph</span> <span>Second paragraph</span> </div> div>span#a { color:green; } div::first-line { color:blue; } ::first-letter 的行为又有所不同,它的位置在所有标签之内。我们把前面的代码换成::first-letter,首字母为蓝色。 <div> <span id=a>First paragraph</span> <span>Second paragraph</span> </div> div>span#a { color:green; } div::first-letter { color:blue; } ::before 表示在元素内容之前插入一个虚拟的元素,::after 则表示在元素内容之后插入。这两个伪元素所在的 CSS 规则必须指定 content 属性才会生效,我们看下例子: <p class="hi">hellow world!</p> p.hi::before { display: block; content: "hi! "; }

优秀的个人博客,低调大师

前端学习笔记(8)css选择器(二)

选择器的组合 选择器列表是一个由逗号分隔的复杂选择器序列;复杂选择器则是用“空格”、“~”、“+”、“>”、“||”连接的复合选择器;复合选择器则是连写的简单选择器。选择器的连接方式可以理解为四则运算一样有优先级。 第一优先级:无连接符号,表示“且” 第二优先级:“空格”、“~”、“+”、“>”、“||” 第三优先级 :“,”表示“或”复杂选择器规定了五种连接符号。 “空格”:后代,表示选中所有符合条件的后代节点, 例如“ .a .b ”表示选中所有具有 class 为 a 的后代节点中 class 为 b 的节点。 “>” :子代,表示选中符合条件的子节点,例如“ .a>.b ”表示:选中所有“具有 class 为 a 的子节点中,class 为 b 的节点”。 “~”: 后继,表示选中所有符合条件的后继节点,后继节点即跟当前节点具有同一个父元素,并出现在它之后的节点,例如“ .a~.b ”表示选中所有具有 class 为 a 的后继中,class 为 b 的节点。 “+”:直接后继,表示选中符合条件的直接后继节点,直接后继节点即 nextSlibling。例如 “.a+.b ”表示选中所有具有 class 为 a 的下一个 class 为 b 的节点。 “||”:列选择器,表示选中对应列中符合条件的单元格。实际使用中“空格”、“>” 使用的比较常见。 选择器的优先级 id选择器的数目记为a; 伪类选择器、class选择器和属性选择器的数目记为b; 伪元素选择器和标签选择器的数目结尾c; “*”不影响优先级。 specificity = base * base * a + base * b + c 其中base是一个足够大的正整数。行内属性的优先级永远大于css规则。浏览器提供了“!important”关键字优先级高于行内属性,不推荐使用。不同属性选择器的优先级相同。例如下面一段代码中三个属性选择器的优先级是相同。 <input type="text"/> [type]{ } [type="text"]{ } [type^="t"]{ } 同一优先级的选择器遵循后面的覆盖前面的原则。

优秀的个人博客,低调大师

前端学习笔记(9)css grid网格布局

Grid 布局的属性分成两类。一类定义在容器上面,称为容器属性;另一类定义在项目上面,称为项目属性。 容器属性 1. display属性 指定一个容器采用网格布局 div { display: grid; } div { display: inline-grid; //设置为行内元素 } 注意,设为网格布局以后,容器子元素(项目)的float、display: inline-block、display: table-cell、vertical-align和column-*等设置都将失效。 2. grid-template-columns 属性,grid-template-rows 属性 grid-template-columns属性定义每一列的列宽,grid-template-rows属性定义每一行的行高。 .container { display: grid; grid-template-columns: 100px 100px 100px; grid-template-rows: 100px 100px 100px; } .container { display: grid; grid-template-columns: 33.33% 33.33% 33.33%; grid-template-rows: 33.33% 33.33% 33.33%; } .container { display: grid; grid-template-columns: repeat(3, 100px); grid-template-rows: repeat(3, 100px); } //grid-template-columns: repeat(2, 100px 20px 80px); repeat()接受两个参数,第一个参数是重复的次数(上例是3),第二个参数是所要重复的值。repeat()重复某种模式也是可以的。 auto-fill auto-fit关键字 .container { display: grid; grid-template-columns(auto-fill,100px); grid-template-columns(auto-fit,100px); } 有时,单元格的大小是固定的,但是容器的大小不确定。如果希望每一行(或每一列)容纳尽可能多的单元格,这时可以使用auto-fill关键字表示自动填充。假设容器可以容纳n个单元格,但是只有m个项目时(n>m),atuo-fill会生成n个单元格,auto-fit只会生成m个单元格。项目数多于容器单行可容纳单元格的情况下两个属性作用相同。 fr关键字 为了方便表示比例关系,网格布局提供了fr关键字(fraction 的缩写,意为"片段")。如果两列的宽度分别为1fr和2fr,就表示后者是前者的两倍。fr可以与绝对长度单位一起使用。 grid-template-columns: 1fr 2fr 3fr; grid-template-columns: 150px 1fr 2fr; //第一列的宽度为150像素,第二列的宽度是第三列的一半。 grid-template-columns: 1fr 1fr minmax(100px, 1fr); //表示列宽不小于100px,不大于1fr 网格线的名称 grid-template-columns属性和grid-template-rows属性里面,还可以使用方括号,指定每一根网格线的名字,方便以后的引用。上面代码指定网格布局为3行 x 3列,因此有4根垂直网格线和4根水平网格线。方括号里面依次是这八根线的名字。网格布局允许同一根线有多个名字,比如[fifth-line row-5]。.container { display: grid; grid-template-columns: [c1] 100px [c2] 100px [c3] auto [c4]; grid-template-rows: [r1] 100px [r2] 100px [r3] auto [r4];} 3.row-gap 属性,column-gap 属性,gap 属性 row-gap属性设置行与行的间隔(行间距),column-gap属性设置列与列的间隔(列间距)。gap属性是column-gap和row-gap的合并简写形式。 grid-gap: <grid-row-gap> <grid-column-gap>; grid-gap:20px 20px; 4.grid-template-areas 属性 网格布局允许指定"区域"(area),一个区域由单个或多个单元格组成。grid-template-areas属性用于定义区域。 .container { display: grid; grid-template-columns: 100px 100px 100px; grid-template-rows: 100px 100px 100px; grid-template-areas: 'a b c' 'd e f' 'g h i'; } //上面代码将9个单元格分成a、b、c三个区域。 grid-template-areas: 'a a a' 'b b b' 'c c c'; //如果某些区域不需要利用,则使用"点"(.)表示。 grid-template-areas: 'a . c' 'd . f' 'g . i'; 注意,区域的命名会影响到网格线。每个区域的起始网格线,会自动命名为区域名-start,终止网格线自动命名为区域名-end。比如,区域名为header,则起始位置的水平网格线和垂直网格线叫做header-start,终止位置的水平网格线和垂直网格线叫做header-end。 5.grid-auto-flow 默认值是row,即"先行后列"。也可以将它设成column,变成"先列后行"。 grid-auto-flow: column; grid-auto-flow属性除了设置成row和column,还可以设成row dense和column dense。这两个值主要用于,某些项目指定位置以后,剩下的项目怎么自动放置。 grid-auto-flow: row; grid-auto-flow: row dense; 6.justify-items 属性,align-items 属性,place-items 属性 justify-items属性设置单元格内容的水平位置(左中右),align-items属性设置单元格内容的垂直位置(上中下)。 .container { justify-items: start | end | center | stretch; align-items: start | end | center | stretch; } start:对齐单元格的起始边缘。 end:对齐单元格的结束边缘。 center:单元格内部居中。 stretch:拉伸,占满单元格的整个宽度(默认值)。 place-items属性是align-items属性和justify-items属性的合并简写形式。如果省略第二个值,则浏览器认为与第一个值相等。 place-items: <align-items> <justify-items>; 7.justify-content 属性,align-content 属性,place-content 属性 justify-content属性是整个内容区域在容器里面的水平位置(左中右),align-content属性是整个内容区域的垂直位置(上中下)。 .container { justify-content: start | end | center | stretch | space-around | space-between | space-evenly; align-content: start | end | center | stretch | space-around | space-between | space-evenly; } place-content属性是align-content属性和justify-content属性的合并简写形式。 place-content: <align-content> <justify-content> 8.grid-auto-columns 属性,grid-auto-rows 属性 有时候,一些项目的指定位置,在现有网格的外部。比如网格只有3列,但是某一个项目指定在第5行。这时,浏览器会自动生成多余的网格,以便放置项目。 grid-auto-columns属性和grid-auto-rows属性用来设置,浏览器自动创建的多余网格的列宽和行高。它们的写法与grid-template-columns和grid-template-rows完全相同。如果不指定这两个属性,浏览器完全根据单元格内容的大小,决定新增网格的列宽和行高。 grid-auto-rows: 200px; 9. grid-template和grid grid-template属性是grid-template-columns、grid-template-rows和grid-template-areas这三个属性的合并简写形式。 grid属性是grid-template-rows、grid-template-columns、grid-template-areas、 grid-auto-rows、grid-auto-columns、grid-auto-flow这六个属性的合并简写形式。 容器属性 1.grid-column-start 属性,grid-column-end 属性,grid-row-start 属性,grid-row-end 属性 用于指定项目位置 grid-column-start属性:左边框所在的垂直网格线 grid-column-end属性:右边框所在的垂直网格线 grid-row-start属性:上边框所在的水平网格线 grid-row-end属性:下边框所在的水平网格线 .item-1 { grid-column-start: 2; grid-column-end: 4; } .item-1 { grid-column-start: 1; grid-column-end: 3; grid-row-start: 2; grid-row-end: 4; } 这四个属性的值,除了指定为第几个网格线,还可以指定为网格线的名字。 .item-1 { grid-column-start: header-start; grid-column-end: header-end; } 这四个属性的值还可以使用span关键字,表示"跨越",即左右边框(上下边框)之间跨越多少个网格。 .item-1 { grid-column-start: span 2; // 效果等同于上面(start、end同时存在时优先使用start) // grid-column-end: span 2; } 使用这四个属性,如果产生了项目的重叠,则使用z-index属性指定项目的重叠顺序。 2. grid-column 属性,grid-row 属性 grid-column属性是grid-column-start和grid-column-end的合并简写形式,grid-row属性是grid-row-start属性和grid-row-end的合并简写形式。 .item-1 { grid-column: 1 / 3; grid-row: 1 / 2; } /* 等同于 */ .item-1 { grid-column-start: 1; grid-column-end: 3; grid-row-start: 1; grid-row-end: 2; } 3. grid-area 属性 grid-area属性指定项目放在哪一个区域。 .container{ grid-template-areas: 'a b c' 'd e f' 'g h i'; } .item-1{ grid-area: e; } grid-area与grid-column、grid-row同时存在时后指定的生效。 .container{ grid-template-areas: 'a b c' 'd e f' 'g h i'; } .item-1{ grid-column: 1/2; grid-area: e; grid-row: 1/2; } grid-area属性还可用作grid-row-start、grid-column-start、grid-row-end、grid-column-end的合并简写形式,直接指定项目的位置。 .item { grid-area: <row-start> / <column-start> / <row-end> / <column-end>; } 4. justify-self 属性,align-self 属性,place-self 属性 参考 justify-items 属性,align-items 属性,place-items 属性,针对当前项目使用。

优秀的个人博客,低调大师

前端学习笔记(4) - JavaScript宏观与微观任务

宏观和微观任务 在 ES3 和更早的版本中,JavaScript 本身还没有异步执行代码的能力,这也就意味着,宿主环境传递给 JavaScript 引擎一段代码,引擎就把代码直接顺次执行了,这个任务也就是宿主发起的任务。 但是,在 ES5 之后,JavaScript 引入了 Promise,这样,不需要浏览器的安排,JavaScript 引擎本身也可以发起任务了。 由于我们这里主要讲 JavaScript 语言,那么采纳 JSC 引擎的术语,我们把宿主发起的任务称为宏观任务,把 JavaScript 引擎发起的任务称为微观任务。 宏观任务的队列就相当于事件循环。在宏观任务中,JavaScript 的 Promise 还会产生异步代码,JavaScript 必须保证这些异步代码在一个宏观任务中完成,因此,每个宏观任务中又包含了一个微观任务队列。 有了宏观任务和微观任务机制,我们就可以实现 JavaScript 引擎级和宿主级的任务了,例如:Promise 永远在队列尾部添加微观任务。setTimeout 等宿主 API,则会添加宏观任务。 如何分析异步执行的顺序 首先我们分析有多少个宏任务; 在每个宏任务中,分析有多少个微任务; 根据调用次序,确定宏任务中的微任务执行次序; 根据宏任务的触发规则和调用次序,确定宏任务的执行次序; 确定整个顺序。 setTimeout(()=>{console.log('c')},0) let x = new Promise((resolve,reject)=>{ console.log('a') resolve() }).then(res=>{ console.log('b') setTimeout(()=>{console.log('d')},0) })

优秀的个人博客,低调大师

前端开发:谁说JavaScript不能读取文件?

一说到读取文件,很多小伙伴第一时间想到的是php,asp.net这些后台语言来实现。 javascript一直都没有提供相关的文件读取接口,但有时我们确实需要读取本地文件内容,下面是两种可能的读取方法。使用 HTML5 中的FileReader对象 HTML5 引入的 FileReader 可以帮助我们读取本地文件,但是有一个限制,就是我们不能直接使用文件路径的方式来访问文件,而是首先需要用户选择文件(通过input标签)。 首先你需要在 html 文件中添加文件选择表单,如下: <input type="file" id="file-input" /> <div id="file-content"></div> 接着就可以在 JavaScript 中进行(用户选择的)文件读取了: function readSingleFile(e) { //获取选择的文件对象 var file = e.target.files[0]; if (!file) { return; } // 创建FileReader对象 var reader = new FileReader(); // load 回调 reader.onload = function(e) { var contents = e.target.result; alert(contents) // 内容处理 }; // 读取文件 reader.readAsText(file,"utf-8"); } // 添加监听事件 var fileInput = document.getElementById("file-input"); fileInput.addEventListener("change", readSingleFile, false); 运行结果如下:

优秀的个人博客,低调大师

从虚拟化前端Bug学习分析Kernel Dump

前言 也许大家都知道,分析 Kernel Dump 有个常用的工具叫 Crash,在我刚开始学习分析 Kernel Dump 的时候,总是花大量的时间折腾这个工具的用法,却总是记不住这个工具的功能。后来有一次在参加某次内部分享的时候,有位大佬说了一句话让我印象非常深刻:这些工具怎么用的大家不用记,等到真正开始用的时候你就会猜到这个工具有什么功能。 这篇文章我想通过分析一个实际的案例,尽量把学习Kernel Dump需要用到的知识串起来,虽然某些知识也许只会在这个案例中用到,但是我相信所用到的方法是可以应用到各个地方的。 起 线上有一台 VM 宕机了,刚好有抓到 dump,拿到一台测试机上就可以开始分析了。首先需要的是 kernel 版本对应的 symbol,如果事先不知道 kernel 的版本,可以通过 `strings corefile | grep "Linux version"' 获取到当前 corefile 的 kernel 版本,例如 3.10.0-862.14.4.el7.x86_64 在获取到内核版本之后,根据相应的发行版以及系统架构到特定的 symbol 发布页面下载 symbol,这里的发行版是 Centos,可以到http://debuginfo.centos.org/下载。如果是 Ubuntu 发行版,可以到http://ddebs.ubuntu.com/下载。要找到指定 kernel 版本的 symbol 很简单,只需要拿着 kernel 版本 3.10.0-862.14.4.el7.x86_64 搜一下就能找到了,通常我们需要的 symbol 的只有下面这三个中的两个,但是我总是记不住是哪两个,所以我会把三个都下载下来并安装:kernel-debug-debuginfo-3.10.0-862.14.4.el7.x86_64.rpm、kernel-debuginfo-3.10.0-862.14.4.el7.x86_64.rpm、kernel-debuginfo-common-x86_64-3.10.0-862.14.4.el7.x86_64.rpm。在安装的时候由于依赖的关系需要先安装 common 的 symbol 才能安装其它 symbol,另外如果测试机上的 kernel 版本比 corefile 的版本新,需要加上 --force 选项才能安装上。 承 在 symbol 安装完之后,就可以通过 crash 载入 corefile 和 symbol 了。 [root@iZxxx1Z crash]# crash /lib/debug/lib/modules/3.10.0-862.14.4.el7.x86_64/vmlinux i-2xxxpe.236725344.208176-2019-02-27-22\:44\:20 crash 7.2.3-8.el7 Copyright (C) 2002-2017 Red Hat, Inc. Copyright (C) 2004, 2005, 2006, 2010 IBM Corporation Copyright (C) 1999-2006 Hewlett-Packard Co Copyright (C) 2005, 2006, 2011, 2012 Fujitsu Limited Copyright (C) 2006, 2007 VA Linux Systems Japan K.K. Copyright (C) 2005, 2011 NEC Corporation Copyright (C) 1999, 2002, 2007 Silicon Graphics, Inc. Copyright (C) 1999, 2000, 2001, 2002 Mission Critical Linux, Inc. This program is free software, covered by the GNU General Public License, and you are welcome to change it and/or distribute copies of it under certain conditions. Enter "help copying" to see the conditions. This program has absolutely no warranty. Enter "help warranty" for details. GNU gdb (GDB) 7.6 Copyright (C) 2013 Free Software Foundation, Inc. License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html> This is free software: you are free to change and redistribute it. There is NO WARRANTY, to the extent permitted by law. Type "show copying" and "show warranty" for details. This GDB was configured as "x86_64-unknown-linux-gnu"... WARNING: kernel relocated [168MB]: patching 82671 gdb minimal_symbol values KERNEL: /lib/debug/lib/modules/3.10.0-862.14.4.el7.x86_64/vmlinux DUMPFILE: i-2xxxpe.236725344.208176-2019-02-27-22:44:20 CPUS: 4 [OFFLINE: 3] DATE: Wed Feb 27 22:44:18 2019 UPTIME: 11:34:59 LOAD AVERAGE: 0.20, 0.07, 0.06 TASKS: 436 NODENAME: iZ2xxxpeZ RELEASE: 3.10.0-862.14.4.el7.x86_64 VERSION: #1 SMP Wed Sep 26 15:12:11 UTC 2018 MACHINE: x86_64 (2500 Mhz) MEMORY: 16 GB PANIC: "kernel BUG at drivers/virtio/virtio_ring.c:278!" PID: 278 COMMAND: "kworker/2:1H" TASK: ffff917c6d3b3f40 [THREAD_INFO: ffff917c64798000] CPU: 2 STATE: TASK_RUNNING (PANIC) 通过 crash 载入 corefile 的时候,crash 会自动输出一段信息,这段信息包含了系统的一些基本信息,如 CPU、内存、架构等。如果后面分析的时候还想看这部分信息,可以通过 sys 命令来查看。这里我们需要重点关注的信息是 PANIC: "kernel BUG at drivers/virtio/virtio_ring.c:278!" 这个信息告诉我们,系统触发了位于 drivers/virtio/virtio_ring.c 这个文件的第 278 行的 Bug,这里系统之所以知道是 Bug,是因为编写这段代码的大佬在这里埋了一个检测的点,这个待会我们会在源码里看到。 在看完上面的信息后,我的习惯是先看看当时系统在做什么,通过 bt 命令可以看到当时的调用堆栈: crash> bt PID: 278 TASK: ffff917c6d3b3f40 CPU: 2 COMMAND: "kworker/2:1H" #0 [ffff917c6479b8a0] die at ffffffff8b82f96b #1 [ffff917c6479b8d0] do_trap at ffffffff8bf1cea0 #2 [ffff917c6479b920] do_invalid_op at ffffffff8b82c284 #3 [ffff917c6479b9d0] invalid_op at ffffffff8bf28aee [exception RIP: virtqueue_add+1186] RIP: ffffffffc023a382 RSP: ffff917c6479ba80 RFLAGS: 00010097 RAX: 0000000000000081 RBX: ffff917c6c67d000 RCX: 0000000000000002 RDX: 0000000000000081 RSI: ffff917c6479bc00 RDI: ffff917c6c67d000 RBP: ffff917c6479bae8 R8: 0000000000000001 R9: ffff917c6b48a380 R10: ffff917c6b410000 R11: 0000000000000002 R12: ffff917c6479bc18 R13: ffff917c6479bc18 R14: ffff917c6479bc00 R15: 0000000000000001 ORIG_RAX: ffffffffffffffff CS: 0010 SS: 0018 #4 [ffff917c6479ba80] __kmalloc at ffffffff8b9fbeb0 #5 [ffff917c6479baf0] virtqueue_add_sgs at ffffffffc023a437 [virtio_ring] #6 [ffff917c6479bb48] __virtblk_add_req at ffffffffc03874b2 [virtio_blk] #7 [ffff917c6479bc68] virtio_queue_rq at ffffffffc03876f9 [virtio_blk] #8 [ffff917c6479bcb8] blk_mq_dispatch_rq_list at ffffffff8bb2a8b6 #9 [ffff917c6479bd50] blk_mq_do_dispatch_sched at ffffffff8bb2f76e #10 [ffff917c6479bd98] blk_mq_sched_dispatch_requests at ffffffff8bb3012e #11 [ffff917c6479bde8] __blk_mq_run_hw_queue at ffffffff8bb29362 #12 [ffff917c6479be10] blk_mq_run_work_fn at ffffffff8bb295c5 #13 [ffff917c6479be20] process_one_work at ffffffff8b8b613f #14 [ffff917c6479be68] worker_thread at ffffffff8b8b71d6 #15 [ffff917c6479bec8] kthread at ffffffff8b8bdf21 上图打印的信息包含函数调用堆栈和各寄存器的值,这里挑几个比较重要的寄存器讲一下。 RIP 指向正在执行的指令地址,在发生宕机之前,系统最后执行的函数是 virtqueue_add,导致宕机的语句位于 virtqueue_add+1186。根据 x86_64 Linux 系统的函数调用约定,RDI, RSI, RDX, RCX, R8, R9为传入函数的前六个参数,如果参数超过六个,第七个以上的参数将通过栈传递。注意在实际函数执行的过程中,寄存器的值可能会改变。 现在来看看 ffffffffc023a382+1186 这行代码到底是什么,通过 dis 命令可以查看到对应的汇编。但当我们执行dis virtqueue_add+1186的时候,发现报错了,报错的内容是 symbol 有重复。 crash> dis virtqueue_add+1186 dis: virtqueue_add+1186: duplicate text symbols found: ffffffffc0239ee0 (t) virtqueue_add [virtio_ring] ffffffffc023ab73 (t) virtqueue_add [virtio_ring] 我们可以通过 RIP 的值来计算出当前的 virtqueue_add 对应的是哪个 symbol,计算方法很简单,只要把 RIP 的值减去偏移量 1186 即可。 crash> p/x 0xffffffffc023a382-1186 $1 = 0xffffffffc0239ee0 通过dis ffffffffc0239ee0可以打印出 virtqueue_add 的汇编,找到 virtqueue_add+1186 0xffffffffc023a378 <virtqueue_add+1176>: mov 0x60(%rbx),%eax 0xffffffffc023a37b <virtqueue_add+1179>: jmpq 0xffffffffc023a299 <virtqueue_add+953> 0xffffffffc023a380 <virtqueue_add+1184>: ud2 0xffffffffc023a382 <virtqueue_add+1186>: ud2 该行汇编实际上是 ud2 ,这是一条 undefined 的语句,也正是因为这条语句让系统宕机了。这个时候通常应该往上找,看看之前执行过的指令是什么。这里上一条指令是 jmpq ,这是无条件跳转语句,跳转到 virtqueue_add+953,也就是说 ud2 指令的上一条指令一定不是 jmpq 这条,可以猜到应该是前面有某个跳转直接跳到这里来了,往上找找就可以找到 0xffffffffc0239ef4 <virtqueue_add+20>: mov %rsi,-0x48(%rbp) 0xffffffffc0239ef8 <virtqueue_add+24>: mov %edx,-0x2c(%rbp) 0xffffffffc0239efb <virtqueue_add+27>: mov %ecx,-0x38(%rbp) 0xffffffffc0239efe <virtqueue_add+30>: mov %r8d,-0x58(%rbp) 0xffffffffc0239f02 <virtqueue_add+34>: mov %r9,-0x60(%rbp) 0xffffffffc0239f06 <virtqueue_add+38>: je 0xffffffffc023a384 <virtqueue_add+1188> 0xffffffffc0239f0c <virtqueue_add+44>: cmpb $0x0,0x59(%rdi) 0xffffffffc0239f10 <virtqueue_add+48>: mov %rdi,%rbx 0xffffffffc0239f13 <virtqueue_add+51>: jne 0xffffffffc023a05d <virtqueue_add+381> 0xffffffffc0239f19 <virtqueue_add+57>: mov -0x2c(%rbp),%eax 0xffffffffc0239f1c <virtqueue_add+60>: cmp %eax,0x38(%rdi) 0xffffffffc0239f1f <virtqueue_add+63>: jb 0xffffffffc023a382 <virtqueue_add+1186> 可以看到跳转的语句是jb 0xffffffffc023a382 <virtqueue_add+1186>,跳转的条件是 %eax 小于 [%rdi + 0x38]。那 %eax 和 [%rdi + 0x38] 里面存的值是什么呢?这个时候就需要对照源码来看了。通过dis -l可以看到函数所在的源文件,但是直接执行dis -l ffffffffc0239ee0会发现没有源文件的信息,这种情况通常是对应的 debug 模块没有导进来。通过 mod 命令可以看到当前的模块,找到我们需要的模块,通过 mod -s 找到模块对应的目录,再通过相同的命令导入即可,如: crash> mod -s virtio_ring MODULE NAME SIZE OBJECT FILE ffffffffc023c0e0 virtio_ring 22746 /usr/lib/debug/usr/lib/modules/3.10.0-862.14.4.el7.x86_64/kernel/drivers/virtio/virtio_ring.ko.debug crash> mod -s virtio_ring /usr/lib/debug/usr/lib/modules/3.10.0-862.14.4.el7.x86_64/kernel/drivers/virtio/virtio_ring.ko.debug MODULE NAME SIZE OBJECT FILE ffffffffc023c0e0 virtio_ring 22746 /usr/lib/debug/usr/lib/modules/3.10.0-862.14.4.el7.x86_64/kernel/drivers/virtio/virtio_ring.ko.debug 把所有缺少的模块导入进来之后,再次执行dis -l ffffffffc0239ee0就可以看到对应的源文件了。部分 virtqueue_add 源代码如下: 241 static inline int virtqueue_add(struct virtqueue *_vq, 242 struct scatterlist *sgs[], 243 unsigned int total_sg, 244 unsigned int out_sgs, 245 unsigned int in_sgs, 246 void *data, 247 gfp_t gfp) 248 { 249 struct vring_virtqueue *vq = to_vvq(_vq); 250 struct scatterlist *sg; 251 struct vring_desc *desc; 252 unsigned int i, n, avail, descs_used, uninitialized_var(prev), err_idx; 253 int head; 254 bool indirect; 255 256 START_USE(vq); 257 258 BUG_ON(data == NULL); 259 260 if (unlikely(vq->broken)) { 261 END_USE(vq); 262 return -EIO; 263 } 264 265 #ifdef DEBUG 266 { 267 ktime_t now = ktime_get(); 268 269 /* No kick or get, with .1 second between? Warn. */ 270 if (vq->last_add_time_valid) 271 WARN_ON(ktime_to_ms(ktime_sub(now, vq->last_add_time)) 272 > 100); 273 vq->last_add_time = now; 274 vq->last_add_time_valid = true; 275 } 276 #endif 277 278 BUG_ON(total_sg > vq->vring.num); 279 BUG_ON(total_sg == 0); 280 281 head = vq->free_head; 282 283 /* If the host supports indirect descriptor tables, and we have multiple 284 * buffers, then go indirect. FIXME: tune this threshold */ 285 if (vq->indirect && total_sg > 1 && vq->vq.num_free) 286 desc = alloc_indirect(_vq, total_sg, gfp); 287 else 288 desc = NULL; 289 290 if (desc) { 291 /* Use a single buffer which doesn't continue */ 292 indirect = true; 293 /* Set up rest to use this indirect table. */ 294 i = 0; 295 descs_used = 1; 296 } else { 297 indirect = false; 298 desc = vq->vring.desc; 299 i = head; 300 descs_used = total_sg; 301 } 通过dis -l ffffffffc0239ee0打印出的信息并结合之前的分析,可以知道:1. virtqueue_add 的前五个参数分别是 struct virtqueue、struct scatterlist、 unsigned int、 unsigned int、 unsigned int 类型的,对应的是 RDI, RSI, RDX, RCX, R8 这五个寄存器的值。2. 触发 bug 的语句是第 278 行的BUG_ON(total_sg > vq->vring.num); 通过struct virtqueue ffff917c6c67d000可以解析出第一个参数的结构: crash> struct virtqueue ffff917c6c67d000 struct virtqueue { list = { next = 0xffff917bbefa82c8, prev = 0xffff917bbefa82c8 }, callback = 0xffffffffc0387000, name = 0xffff917c6c7a1dcc "req.0", vdev = 0xffff917bbefa8000, index = 0, num_free = 1, priv = 0xffffadff81b5e010 } 回到刚刚我们讨论的 %eax 和 [%rdi + 0x38] ,[%rdi + 0x38] 实际上就是 virtqueue 中偏移量为 0x38 的值,通过struct -o virtqueue可以打印出 virtqueue 各成员的偏移: crash> struct -o virtqueue struct virtqueue { [0] struct list_head list; [16] void (*callback)(struct virtqueue *); [24] const char *name; [32] struct virtio_device *vdev; [40] unsigned int index; [44] unsigned int num_free; [48] void *priv; } SIZE: 56 这里又出现了一个问题,0x38 是十进制的 56,而这个结构体的大小总共只有 56 个字节,难道是“溢出”了?仔细阅读代码后发现,代码里有一句struct vring_virtqueue *vq = to_vvq(_vq);,to_vvq 是一个宏,定义如下#define to_vvq(_vq) container_of(_vq, struct vring_virtqueue, vq),实际上这就是对 container_of 的一个封装,container_of 的功能跟字面意思很接近,这里 virtqueue 类型的 _vq 变量实际上是 vring_virtqueue 类型的 vq 变量的一个成员变量,通过 container_of(_vq, struct vring_virtqueue, vq) 把 vq 计算出来。我们通过struct -o vring_virtqueue来查看 vring_virtqueue 的结构: crash> struct -o vring_virtqueue struct vring_virtqueue { [0] struct virtqueue vq; [56] struct vring vring; [88] bool weak_barriers; [89] bool broken; [90] bool indirect; [91] bool event; [92] unsigned int free_head; [96] unsigned int num_added; [100] u16 last_used_idx; [104] bool (*notify)(struct virtqueue *); [112] bool we_own_ring; [120] size_t queue_size_in_bytes; [128] dma_addr_t queue_dma_addr; [136] struct vring_desc_state desc_state[]; } SIZE: 136 可以看到实际上 virtqueue 结构就在 vring_virtqueue 偏移为 0 的地方,因此可以直接通过struct vring_virtqueue ffff917c6c67d000来解析 vring_virtqueue 结构: crash> struct vring_virtqueue ffff917c6c67d000 struct vring_virtqueue { vq = { list = { next = 0xffff917bbefa82c8, prev = 0xffff917bbefa82c8 }, callback = 0xffffffffc0387000, name = 0xffff917c6c7a1dcc "req.0", vdev = 0xffff917bbefa8000, index = 0, num_free = 1, priv = 0xffffadff81b5e010 }, vring = { num = 128, desc = 0xffff917c6bf68000, avail = 0xffff917c6bf68800, used = 0xffff917c6bf69000 }, weak_barriers = true, broken = false, indirect = true, event = false, free_head = 94, num_added = 0, last_used_idx = 38531, notify = 0xffffffffc0249a50, we_own_ring = true, queue_size_in_bytes = 5126, queue_dma_addr = 17917444096, desc_state = 0xffff917c6c67d088 } 因此 [%rdi + 0x38] 实际上获取的是 vring 结构里偏移量为 0 的即第一个成员的值,这里获取到的值就是 128。现在我们已经通过这种方法获取到触发 bug 的语句中BUG_ON(total_sg > vq->vring.num);的 vq->vring.num 值了,而 total_sg 实际上是 virtqueue_add 的第三个参数,保存在 RDX 寄存器里,是 0x81,即十进制的 129。 合 至此,我只是分析了这几个数据结构中相关的变量内容,还没有解释这些变量或者函数的含义,现在我们已经验证了触发 BUG 的条件 total_sg > vq->vring.num,但是为什么会出现这种情况呢?要分析这几个变量的值需要回溯到调用这个函数的函数,最终可能需要一直回溯到发起 IO 请求的应用层程序,这显然是一件非常麻烦的事情。换一个角度来想,total_sg 是 vm 需要的 scatterlist 的总数,scatterlist 是一个跟物理内存有关的结构,这里可以简单理解为 vm 所需要的物理内存,而 vring 是 virtio 前后端数据传输的载体,这里可以简单理解前后端数据传输的能力。直观的感觉是 total_sg 确实不应该比 vring.num 大,但实际上 total_sg 只比 vring.num 大 1,而且这里 total_sg 的值与我刚开始的想法不一样,由于 total_sg 是 unsigned int 类型的,而这里的比较是 total_sg > vq->vring.num,因此我开始的想法是 total_sg 下溢出了。分析到这里,我抱着试一试的态度把BUG_ON(total_sg > vq->vring.num);丢到 Google 上搜了一把,发现这行代码已经在某个版本中 patch 掉了,最新的 kernel 里把 BUG_ON 改成了低一级的 WARN_ON_ONCE,同时把条件改成了 total_sg > vq->vring.num && !vq->indirect。也就是说,在使用了 indirect descriptors 的情况下,是允许 total_sg > vq->vring.num 这中情况出现的,那如何验证 vm 有没有使用 indirect descriptors 呢?实际上在 vring_virtqueue 中的成员 indirect 标示了是否使用 indirect descriptors,在上面执行struct vring_virtqueue ffff917c6c67d000的时候已经把 indirect 的值打印出来了,vm 确实使用了 indirect descriptors,因此这个 bug 实际上触发得并不合理,仅仅只判断 total_sg > vq->vring.num 就触发宕机的条件太过严格了。 总结 这个宕机问题到这里就算分析完了,最后解决的方案是升级内核,考虑到目前 Centos 官方还未接受该 patch,需要手动编译修复或通过第三方 repo 升级。实际上很多奇奇怪怪的问题都可以通过升级内核来解决,但是最新的内核同样可能遇到奇奇怪怪的问题,谁知道下一个发现内核 bug 后写了 patch 最后被社区接受的会不会是自己呢?希望大家通过这篇文章能有所收获。文章写得不好或不对的地方请各位大佬不吝赐教。 作者:半人前 原文链接​ 本文为云栖社区原创内容,未经允许不得转载。

资源下载

更多资源
腾讯云软件源

腾讯云软件源

为解决软件依赖安装时官方源访问速度慢的问题,腾讯云为一些软件搭建了缓存服务。您可以通过使用腾讯云软件源站来提升依赖包的安装速度。为了方便用户自由搭建服务架构,目前腾讯云软件源站支持公网访问和内网访问。

Nacos

Nacos

Nacos /nɑ:kəʊs/ 是 Dynamic Naming and Configuration Service 的首字母简称,一个易于构建 AI Agent 应用的动态服务发现、配置管理和AI智能体管理平台。Nacos 致力于帮助您发现、配置和管理微服务及AI智能体应用。Nacos 提供了一组简单易用的特性集,帮助您快速实现动态服务发现、服务配置、服务元数据、流量管理。Nacos 帮助您更敏捷和容易地构建、交付和管理微服务平台。

Rocky Linux

Rocky Linux

Rocky Linux(中文名:洛基)是由Gregory Kurtzer于2020年12月发起的企业级Linux发行版,作为CentOS稳定版停止维护后与RHEL(Red Hat Enterprise Linux)完全兼容的开源替代方案,由社区拥有并管理,支持x86_64、aarch64等架构。其通过重新编译RHEL源代码提供长期稳定性,采用模块化包装和SELinux安全架构,默认包含GNOME桌面环境及XFS文件系统,支持十年生命周期更新。

WebStorm

WebStorm

WebStorm 是jetbrains公司旗下一款JavaScript 开发工具。目前已经被广大中国JS开发者誉为“Web前端开发神器”、“最强大的HTML5编辑器”、“最智能的JavaScript IDE”等。与IntelliJ IDEA同源,继承了IntelliJ IDEA强大的JS部分的功能。

用户登录
用户注册