Jekyll2022-05-19T05:57:41+00:00http://linbo.github.io/atom.xmlLinbo的博客Hello world!Linbo LiaoLaTeX环境配置2020-03-15T00:00:00+00:002020-03-15T00:00:00+00:00http://linbo.github.io/2020/03/15/latex-setup<blockquote>
<p><em>距离上次写LaTeX,已经过去整整6年了</em></p>
</blockquote>
<p>一直用<code class="language-plaintext highlighter-rouge">Markdown</code>写东西,业余时间学<a href="https://book.douban.com/subject/27055712/">《SQL基础教程》</a>,随手在纸上画的内容想搬到电脑上,<code class="language-plaintext highlighter-rouge">Markdown</code>有点捉襟见肘。不想用<code class="language-plaintext highlighter-rouge">Office</code>那一套庞然大物,好像<code class="language-plaintext highlighter-rouge">iPad</code>上的<code class="language-plaintext highlighter-rouge">Goodnotes</code>和<code class="language-plaintext highlighter-rouge">Notability</code>不错,手边没有,所以目光转到<code class="language-plaintext highlighter-rouge">LaTeX</code>上。</p>
<p>6年前曾经玩过<code class="language-plaintext highlighter-rouge">LaTeX</code>,写过一点东西,后面荒废了,几天前又重拾起来,体验挺好。受益于各种技术的发展,<code class="language-plaintext highlighter-rouge">LaTeX</code>的门槛又比6年前降低了很多。</p>
<p>工欲善其事,必先利其器,有一套快捷友好的配套工具,是用户愿意尝试的第一步。记得6年前,在<code class="language-plaintext highlighter-rouge">Linux</code>机器上折腾好久<code class="language-plaintext highlighter-rouge">texlive</code>,而且怎么写中文也被虐的不行。现在借助<code class="language-plaintext highlighter-rouge">Docker</code>,<code class="language-plaintext highlighter-rouge">VS Code</code>等利器,搭建一套<code class="language-plaintext highlighter-rouge">LaTeX</code>使用环境,是分分钟的事情。</p>
<p>原来想在Mac上安装一套<code class="language-plaintext highlighter-rouge">MacTex</code>,想着安装这么一个大件有点抗拒,网上一搜<code class="language-plaintext highlighter-rouge">texlive</code>居然有<code class="language-plaintext highlighter-rouge">Docker</code>镜像,配合<code class="language-plaintext highlighter-rouge">VS Code</code>挺香,于是便折腾了起来。</p>
<h1 id="docker环境">Docker环境</h1>
<p>首先机器上需要安装<code class="language-plaintext highlighter-rouge">Docker</code>,然后直接拉镜像,启动完事。为什么用这个镜像?好像下载量最高</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>docker pull mirisbowring/texlive_ctan_full:2019
<span class="nv">$ </span>docker run <span class="nt">-dit</span> 51fa8d279d30 <span class="c">#一串数字是image id</span>
</code></pre></div></div>
<p>建议最好能mount目录到容器里面,存放所写的内容。不过当时直接一顿操作了,幸好镜像里面带了<code class="language-plaintext highlighter-rouge">Git</code>,所以也无所谓了。</p>
<p>拷贝容器内容</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>docker <span class="nb">cp </span>a64a398683e3:/data/tex/demo.pdf ./
</code></pre></div></div>
<h1 id="vs-code环境">VS Code环境</h1>
<p>首先要安装<code class="language-plaintext highlighter-rouge">Remote-Containers</code>和<code class="language-plaintext highlighter-rouge">Latex Workshop</code>两个扩展。</p>
<p><code class="language-plaintext highlighter-rouge">VS Code</code>配置如下(里面很多配置项应该可以删掉,网上拷贝的,没去研究了)</p>
<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="err">//</span><span class="w"> </span><span class="err">Latex</span><span class="w"> </span><span class="err">workshop</span><span class="w">
</span><span class="nl">"latex-workshop.latex.tools"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
</span><span class="p">{</span><span class="w">
</span><span class="nl">"name"</span><span class="p">:</span><span class="w"> </span><span class="s2">"latexmk"</span><span class="p">,</span><span class="w">
</span><span class="nl">"command"</span><span class="p">:</span><span class="w"> </span><span class="s2">"latexmk"</span><span class="p">,</span><span class="w">
</span><span class="nl">"args"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
</span><span class="s2">"-synctex=1"</span><span class="p">,</span><span class="w">
</span><span class="s2">"-interaction=nonstopmode"</span><span class="p">,</span><span class="w">
</span><span class="s2">"-file-line-error"</span><span class="p">,</span><span class="w">
</span><span class="s2">"-pdf"</span><span class="p">,</span><span class="w">
</span><span class="s2">"%DOC%"</span><span class="w">
</span><span class="p">]</span><span class="w">
</span><span class="p">},</span><span class="w">
</span><span class="p">{</span><span class="w">
</span><span class="nl">"name"</span><span class="p">:</span><span class="w"> </span><span class="s2">"xelatex"</span><span class="p">,</span><span class="w">
</span><span class="nl">"command"</span><span class="p">:</span><span class="w"> </span><span class="s2">"xelatex"</span><span class="p">,</span><span class="w">
</span><span class="nl">"args"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
</span><span class="s2">"-synctex=1"</span><span class="p">,</span><span class="w">
</span><span class="s2">"-interaction=nonstopmode"</span><span class="p">,</span><span class="w">
</span><span class="s2">"-file-line-error"</span><span class="p">,</span><span class="w">
</span><span class="s2">"%DOC%"</span><span class="w">
</span><span class="p">]</span><span class="w">
</span><span class="p">},</span><span class="w">
</span><span class="p">{</span><span class="w">
</span><span class="nl">"name"</span><span class="p">:</span><span class="w"> </span><span class="s2">"pdflatex"</span><span class="p">,</span><span class="w">
</span><span class="nl">"command"</span><span class="p">:</span><span class="w"> </span><span class="s2">"pdflatex"</span><span class="p">,</span><span class="w">
</span><span class="nl">"args"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
</span><span class="s2">"-synctex=1"</span><span class="p">,</span><span class="w">
</span><span class="s2">"-interaction=nonstopmode"</span><span class="p">,</span><span class="w">
</span><span class="s2">"-file-line-error"</span><span class="p">,</span><span class="w">
</span><span class="s2">"%DOC%"</span><span class="w">
</span><span class="p">]</span><span class="w">
</span><span class="p">},</span><span class="w">
</span><span class="p">{</span><span class="w">
</span><span class="nl">"name"</span><span class="p">:</span><span class="w"> </span><span class="s2">"bibtex"</span><span class="p">,</span><span class="w">
</span><span class="nl">"command"</span><span class="p">:</span><span class="w"> </span><span class="s2">"bibtex"</span><span class="p">,</span><span class="w">
</span><span class="nl">"args"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
</span><span class="s2">"%DOCFILE%"</span><span class="w">
</span><span class="p">]</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="p">]</span><span class="err">,</span><span class="w">
</span><span class="nl">"latex-workshop.latex.recipes"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
</span><span class="p">{</span><span class="w">
</span><span class="nl">"name"</span><span class="p">:</span><span class="w"> </span><span class="s2">"xelatex"</span><span class="p">,</span><span class="w">
</span><span class="nl">"tools"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
</span><span class="s2">"xelatex"</span><span class="w">
</span><span class="p">]</span><span class="w">
</span><span class="p">},</span><span class="w">
</span><span class="p">{</span><span class="w">
</span><span class="nl">"name"</span><span class="p">:</span><span class="w"> </span><span class="s2">"latexmk"</span><span class="p">,</span><span class="w">
</span><span class="nl">"tools"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
</span><span class="s2">"latexmk"</span><span class="w">
</span><span class="p">]</span><span class="w">
</span><span class="p">},</span><span class="w">
</span><span class="p">{</span><span class="w">
</span><span class="nl">"name"</span><span class="p">:</span><span class="w"> </span><span class="s2">"pdflatex -> bibtex -> pdflatex*2"</span><span class="p">,</span><span class="w">
</span><span class="nl">"tools"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
</span><span class="s2">"pdflatex"</span><span class="p">,</span><span class="w">
</span><span class="s2">"bibtex"</span><span class="p">,</span><span class="w">
</span><span class="s2">"pdflatex"</span><span class="p">,</span><span class="w">
</span><span class="s2">"pdflatex"</span><span class="w">
</span><span class="p">]</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="p">]</span><span class="err">,</span><span class="w">
</span><span class="nl">"latex-workshop.view.pdf.viewer"</span><span class="p">:</span><span class="w"> </span><span class="s2">"tab"</span><span class="err">,</span><span class="w">
</span><span class="nl">"latex-workshop.latex.clean.fileTypes"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
</span><span class="s2">"*.aux"</span><span class="p">,</span><span class="w">
</span><span class="s2">"*.bbl"</span><span class="p">,</span><span class="w">
</span><span class="s2">"*.blg"</span><span class="p">,</span><span class="w">
</span><span class="s2">"*.idx"</span><span class="p">,</span><span class="w">
</span><span class="s2">"*.ind"</span><span class="p">,</span><span class="w">
</span><span class="s2">"*.lof"</span><span class="p">,</span><span class="w">
</span><span class="s2">"*.lot"</span><span class="p">,</span><span class="w">
</span><span class="s2">"*.out"</span><span class="p">,</span><span class="w">
</span><span class="s2">"*.toc"</span><span class="p">,</span><span class="w">
</span><span class="s2">"*.acn"</span><span class="p">,</span><span class="w">
</span><span class="s2">"*.acr"</span><span class="p">,</span><span class="w">
</span><span class="s2">"*.alg"</span><span class="p">,</span><span class="w">
</span><span class="s2">"*.glg"</span><span class="p">,</span><span class="w">
</span><span class="s2">"*.glo"</span><span class="p">,</span><span class="w">
</span><span class="s2">"*.gls"</span><span class="p">,</span><span class="w">
</span><span class="s2">"*.ist"</span><span class="p">,</span><span class="w">
</span><span class="s2">"*.fls"</span><span class="p">,</span><span class="w">
</span><span class="s2">"*.log"</span><span class="p">,</span><span class="w">
</span><span class="s2">"*.fdb_latexmk"</span><span class="w">
</span><span class="p">]</span><span class="w">
</span></code></pre></div></div>
<p>试试通过<code class="language-plaintext highlighter-rouge">VS Code</code>连接到容器里码<code class="language-plaintext highlighter-rouge">LaTeX</code>的快乐吧。每次保存<code class="language-plaintext highlighter-rouge">tex</code>文件后,自动编译成<code class="language-plaintext highlighter-rouge">pdf</code>,快捷键<code class="language-plaintext highlighter-rouge">option+command+B</code>编译,<code class="language-plaintext highlighter-rouge">option+command+V</code>预览pdf文件。</p>
<p>碰到一个代码高亮的问题,<code class="language-plaintext highlighter-rouge">command+K,T</code>调出选择颜色主题框,把<code class="language-plaintext highlighter-rouge">Dark(Visual studio)</code>改成<code class="language-plaintext highlighter-rouge">Dark+</code>,高亮语法出现。</p>Linbo Liao距离上次写LaTeX,已经过去整整6年了Goodbye 20192020-01-01T00:00:00+00:002020-01-01T00:00:00+00:00http://linbo.github.io/2020/01/01/goodbye-2019<blockquote>
<p><em>人有悲欢离合,月有阴晴圆缺</em><br />
<em>此事古难全</em></p>
</blockquote>
<p>2019年就这样悄无声息的过去了,坐在电脑旁,想着过去的一年,好像发生过很多事情,又好像什么都没有发生。</p>
<p>这一年,小伙伴们跑了两位,去了更头部的公司,也来了更年轻的新人。想想工作都已经10年,经历很多次的人来人往,自己也从新人变成老人。以前不懂,不了解的很多事情,也在自己身上发生,懂得中年人的焦虑,体会到中年人的危机。在现代社会,唯一能做的,就是不断学习,提高自己,迎头而上,直面危机,否则迟早会被危机吞没。</p>
<p>工作上也没啥可说的,还是在运维工具建设上兜兜转转。技术上今年估计是退步好多,很多新技术,新概念感觉已经变得很复杂,不是那么容易理解。不知道是离开技术久了,还是年纪大了,总感觉有点跟不上技术的快速发展。在其它方面倒是得到不少锻炼,主要是文档能力,工作总结,项目管理方面。能学习到一些常用技能,总是好的。</p>
<p>今年也读了一些理财方面的书籍,以前可能一直入不敷出,手上没有零钱,也没太关注这些。虽然理财很难让自己财务有太大的变化,不过里面一些观念,一些思想,对自己触动还是很大的。以后要多多思考并理解实践这些有益的观念思想,希望对以后的路有所帮助。</p>
<p>五月份去了一次西安,对吃没什么感觉,古城的历史厚重感也很少。可能古城墙已经太干净太现代,或者是博物馆的文物太多太杂,兵马俑的坑太小太少,总之“古代”似乎并没有深深的烙印在这些东西上,古代仍然只是脑海深处那个古代,再也见不到了。也去了一次华山,坐着缆车,心都要跳出来了,真是非常的紧张。好在天气晴朗,阳光充沛,微风些许,走马观花的把大部分山峰都走了遍。那些很险的地方人太多,而且似乎勇气不是那么足,也就没有尝试。看着崇山峻岭,壁立千仞,感叹大自然的鬼斧神工,瑰丽奇伟,虽不能说洗涤心灵,但也能小小的陶冶情操,修身养性。</p>
<p>7月底,妈妈因为高血压导致脑溢血,因为是晚上发作,早上哥哥发现送往医院,时间已经比较久了,手术后一直昏迷至今,苏醒的可能性不会太大。妈妈一辈子辛苦操劳,基本没过几天舒心日子,作为儿女,心里十分愧疚,却可能再也没有机会补偿了。很多事情,一旦发生就再也不能改变,能做的就是珍惜当下的生活,珍惜身边的人,珍惜过着的每一分每一秒。虽然机会渺茫,还是不会放弃,希望奇迹能够发生,妈妈能醒过来看看大家。</p>
<p>2019年是过去十年中最差的一年,虽然很艰辛,但是微弱的希望灯光仍然照耀着前路,只能努力前行,尽人事,不留遗憾,相信未来十年还是有别样的风景。</p>Linbo Liao人有悲欢离合,月有阴晴圆缺 此事古难全epoll的那些坑2019-04-14T00:00:00+00:002019-04-14T00:00:00+00:00http://linbo.github.io/2019/04/14/epoll-pitfall<p>可能当初 epoll 设计没有考虑太多并发的情况,单进程单线程下 epoll 工作良好,但是多进程或者多线程下就有一些坑。</p>
<h1 id="文件对象">文件对象</h1>
<p>在讲这个之前,需要了解和文件有关的三个数据结构,具体可以参考《Unix环境高级编程》3.10节,或者<a href="https://medium.com/@copyconstruct/the-method-to-epolls-madness-d9d2d6378642">The method to epoll’s madness</a>的内容。文件涉及三个数据结构:</p>
<ul>
<li>进程维护 file descriptor 表,每个 fd 包含
<ul>
<li>fd 标志</li>
<li>指向内核 file description 表项的指针</li>
</ul>
</li>
<li>内核维护所有打开文件的 file description 表,每个 file description 包含
<ul>
<li>当前文件的 offset</li>
<li>文件状态标志(读、写、阻塞、非阻塞等)</li>
<li>指向该文件 v 节点表项的指针</li>
</ul>
</li>
<li>每个打开文件/设备都有一个 v 节点结构,包含 inode 信息等</li>
</ul>
<h1 id="epoll-相关的数据结构">epoll 相关的数据结构</h1>
<p><img src="https://cdn-images-1.medium.com/max/1600/1*ObWegZ_IDTqGVH2KLYxPSA.png" alt="epoll_fd" /></p>
<p>调用 epoll_create 成功,返回 epoll instance fd,进程通过 efd 对 epoll 进行操作。同时在内核也会创建 epoll instance 对应的 file description,在文件系统也会创建 inode 信息。</p>
<p>例如图中,fd9 和相关联的其它数据结构,就是 epoll instance 相关的数据结构(淡黄色部分)。</p>
<p>同时注册 fd 到 epoll instance,epoll set 其实监听的是 file description,而不是 file descriptor(fd)。所以 epoll set 里面的 fd0 其实是 fd0 指向的 file description(橙色部分)。</p>
<h1 id="复制-epoll-fd-问题">复制 epoll fd 问题</h1>
<p><img src="https://cdn-images-1.medium.com/max/1600/1*oYdvrj-gPPkycZdTFqb3fA.png" alt="epoll_copy" /></p>
<p>假设 Process A 创建了 epoll,并将 fd0 注册到 epoll 中。Process A fork 子进程 B,此时 B 也拥有和 A 同样的 fd table,B 的 epoll file description 和 A 是同一个。</p>
<p>现在 A 创建了一个新的 fd8 并注册到 epoll 中,如果 fd8 有事件,不仅仅 A 能接收到这个事件,B 也能(虽然 B 都不知道有这个 fd8)。</p>
<h1 id="复制-fd-问题">复制 fd 问题</h1>
<p>继续上面的例子,假设 A close(fd0),A 以为自己已经关闭了 fd0,不会收到 fd0 任何事件了。但是由于如下原因</p>
<ul>
<li>epoll 监听的是 file description</li>
<li>只有指向 file description 的 file descriptor 都关闭,file description 才会删除</li>
<li>虽然 A 关闭 fd0,但是file description 还有 B 的 fd0 指着,所以不会删除</li>
</ul>
<p>所以 A 还是会继续收到 fd0 的事件。由此可以看出,epoll 注册对象的生命周期和对象对应的 fd 生命周期不完全一致。</p>
<p>再比如,epoll 监听了 fd,程序执行 fd2 = dup(fd),然后调用 close(fd),会出现如下问题</p>
<ul>
<li>程序还是能接收到 fd 的事件</li>
<li>fd 不能从 epoll 里面删除,即使做如下 epoll_ctl 操作也不行</li>
</ul>
<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">epoll_ctl</span><span class="p">(</span><span class="n">efpd</span><span class="p">,</span> <span class="n">EPOLL_CTL_DEL</span><span class="p">,</span> <span class="n">rfd</span><span class="p">)</span>
<span class="n">epoll_ctl</span><span class="p">(</span><span class="n">efpd</span><span class="p">,</span> <span class="n">EPOLL_CTL_DEL</span><span class="p">,</span> <span class="n">rfd2</span><span class="p">)</span>
</code></pre></div></div>
<p><strong>所以在 close 之前,一定要记得先把 fd 从 epoll set 里面删除。</strong></p>
<p>上面两个问题,具体可以参考 <a href="https://medium.com/@copyconstruct/the-method-to-epolls-madness-d9d2d6378642">The method to epoll’s madness</a></p>
<h1 id="load-balance问题">load balance问题</h1>
<p><a href="https://idea.popcount.org/2017-03-20-epoll-is-fundamentally-broken-12/">Epoll is fundamentally broken 1/2</a>讨论了负载均衡的问题</p>
<h2 id="scale-out-accept">scale out accept()</h2>
<p>如果多个worker thread共享epoll fd,将会存在各种问题</p>
<h3 id="lt模式">LT模式</h3>
<p>LT将会发生惊群效应,示例如下</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>1. Kernel:接收新连接
2. Kernel:通知A线程处理,假如处理不及时,因为连接还在内核缓冲区中,所以也会通知B线程处理
3. Thread A:完成 epoll_wait()
4. Thread B:完成 epoll_wait()
5. Thread A:accept() 返回成功
6. Thread B:accept() 返回 EAGAIN
</code></pre></div></div>
<p>线程B在这种场景下根本没有必要被唤醒</p>
<h3 id="et模式">ET模式</h3>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>1. Kernel: 接收第一个连接,两个线程在等待,此时只有一个被唤醒,例如thread A.
2. Thread A: 完成 epoll_wait().
3. Thread A: accept()返回成功.
4. Kernel: 因为内核缓冲区为空,ET下新事件产生.
5. Kernel: 接收第二个连接.
6. Kernel: 当前只有线程B在等待,线程B被唤醒.
7. Thread A: 循环调用 accept()直到返回 EAGAIN, 结果却返回新的socket.
8. Thread B: 调用 accept(),期待是新socket,结果返回 EAGAIN.
9. Thread A: 循环调用 accept()直到返回EAGAIN
</code></pre></div></div>
<p>这种情况下,线程B没必要被唤醒。</p>
<p>下面例子中,线程可能会被饿死</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>1. Kernel: 同时接收两个连接,当前线程A and B在等待,ET下只有A被唤醒
2. Thread A: 完成 epoll_wait().
3. Thread A: 调用 accept()成功
4. Kernel: 接收第三个连接,因为内核缓冲区一直有连接,没有新事件产生
5. Thread A: 循环调用accept(),没有返回 EAGAIN
6. Kernel: 接收第四个连接
7. Thread A: 循环调用 accept(),没有返回EAGAIN
</code></pre></div></div>
<p>永远只有A在工作,负载均衡没有生效</p>
<h3 id="解决">解决</h3>
<p>LT模式,Kernel 4.5+ 用 EPOLLEXCLUSIVE flag 可以保证一个事件只有一个线程被唤醒。</p>
<p>ET模式下,可以使用EPOLLONESHOT flag,但是每个事件要多调用一次epoll_ctl。这种方式下负载可以分到不同CPU中,但是同一时刻最多只能有一个线程在accept,限制了吞吐。</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>1. Kernel: 接收两个连接,ET下两个等待线程只有A被唤醒
2. Thread A: 完成 epoll_wait()
3. Thread A: 调用 accept() 成功
4. Thread A: 调用 epoll_ctl(EPOLL_CTL_MOD), 将会重置 EPOLLONESHOT,re-arm the socket
</code></pre></div></div>
<h2 id="scale-out-read">scale out read()</h2>
<h3 id="lt">LT</h3>
<p>防止惊群效应,采用EPOLLEXCLUSIVE flag</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>1. Kernel: 接收2047字节数据
2. Kernel: 两个等待的线程,因为采用EPOLLEXCLUSIVE,只唤醒A.
3. Thread A: 完成 epoll_wait()
4. Kernel: 接收2字节数据
5. Kernel: 只有一个线程B等待,唤醒B
6. Thread A: 调用 read(2048),读取缓冲区2048字节数据
7. Thread B: 调用 read(2048),读取1字节数据
</code></pre></div></div>
<p>数据分散到两个线程中,而且没有加锁保护,数据可能乱序</p>
<h3 id="et">ET</h3>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>1. Kernel: 接收2048字节数据
2. Kernel: ET下两个等待线程,只有A被唤醒
3. Thread A: 完成 epoll_wait()
4. Thread A: 调用read(2048),接收2048字节数据
5. Kernel: 内核缓冲区为空,事件触发
6. Kernel: 接收1字节
7. Kernel: 只有1个线程在等待,B被唤醒
8. Thread B: 完成 epoll_wait()
9. Thread B: 调用 read(2048),接收1字节
10. Thread A: 重新调用 read(2048), 返回 EAGAIN
</code></pre></div></div>
<h3 id="解决-1">解决</h3>
<p>解决方法是设置 EPOLLONESHOT flag,每次读完后调用epoll_ctl(EPOLL_CTL_MOD),重新触发事件</p>
<h1 id="reference">reference</h1>
<ul>
<li><a href="https://medium.com/@copyconstruct/the-method-to-epolls-madness-d9d2d6378642">The method to epoll’s madness</a></li>
<li><a href="https://idea.popcount.org/2017-03-20-epoll-is-fundamentally-broken-12/">Epoll is fundamentally broken 1/2</a></li>
</ul>Linbo Liao可能当初 epoll 设计没有考虑太多并发的情况,单进程单线程下 epoll 工作良好,但是多进程或者多线程下就有一些坑。epoll的那些事2019-03-01T00:00:00+00:002019-03-01T00:00:00+00:00http://linbo.github.io/2019/03/01/epoll-fundamental<p>一直没搞明白 epoll 的机制,以前看不明白 epoll 资料就放弃了。最近重新看这些资料,感觉看明白了大部分。记一下,省的以后又糊涂了。以下内容都是各种资料的小结,以后翻阅省事一点。</p>
<h1 id="io-模型与-epoll">I/O 模型与 epoll</h1>
<h2 id="io-流">I/O 流</h2>
<p>阻塞模式下,一个线程很难处理多个 I/O 流。比如一个线程要读两个 I/O 事件流,可能 read 第一个I/O 时,因为数据没有就绪,所以整个线程都阻塞了,而第二个 I/O 数据虽然已经就绪,却得不到处理。具体原因个人理解是,线程不知道哪个 I/O 事件已经就绪,只能一个个试。第二个原因是阻塞模式下,如果事件没有就绪,系统调用会阻塞,导致整个线程都阻塞。</p>
<p>非阻塞模式,线程可以通过忙轮询处理多个 I/O 流,同样因为无法知道哪个 I/O 流是否已经就绪,导致很多系统调用都是无效的,效率非常低下。</p>
<h2 id="io-多路复用">I/O 多路复用</h2>
<p>如果有一个代理,帮助管理多个 I/O 流,当没有可用的 I/O,线程继续阻塞,I/O 就绪时,唤醒线程处理 I/O,效率会大大提高。在 Linux 平台上,select,poll,epoll 就是这个代理。它们之间具体的优缺点就不讲了,这里只讲 epoll 的机制。</p>
<p>I/O 相关的机制,可以参考知乎上面的讨论 <a href="https://www.zhihu.com/question/20122137/answer/14049112">I/O与epoll</a></p>
<h1 id="epoll-基础">epoll 基础</h1>
<p>以下内容基本上来自<a href="https://medium.com/@copyconstruct/the-method-to-epolls-madness-d9d2d6378642">The method to epoll’s madness</a> 和 manpage。</p>
<p><img src="https://cdn-images-1.medium.com/max/1600/1*KDk1AVzQJegkcWKJQURYfw.jpeg" alt="epoll" /></p>
<p>内核内部用数据结构来维护 epoll 的相关信息,epoll 的三个 API 分别操作这些数据结构。</p>
<h2 id="epoll_create">epoll_create</h2>
<p>epoll_create 在内核创建 epoll instance(图中下方褐色方块),返回指向这个 epoll instance 的 file descriptor。</p>
<h2 id="epoll_ctl">epoll_ctl</h2>
<p>epoll_ctl 可以让 file descriptor 注册到 epoll instance 中,这些 file descriptor 称为 epoll set(图中 INTEREST LIST)。当 epoll set 里面的 file descriptor 有 I/O 就绪情况下,这些 file descriptor 会放到 READY LIST 里面(图中蓝色部分)。</p>
<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cp">#include <sys/epoll.h>
</span><span class="kt">int</span> <span class="nf">epoll_ctl</span><span class="p">(</span><span class="kt">int</span> <span class="n">epfd</span><span class="p">,</span> <span class="kt">int</span> <span class="n">op</span><span class="p">,</span> <span class="kt">int</span> <span class="n">fd</span><span class="p">,</span> <span class="k">struct</span> <span class="n">epoll_event</span> <span class="o">*</span><span class="n">event</span><span class="p">);</span>
</code></pre></div></div>
<ul>
<li>epfd - epoll_create 返回的 epoll file descriptor</li>
<li>fd - epoll instance 要监听的 file descriptor</li>
<li>op - 对 file descriptor 的操作
<ul>
<li>EPOLL_CTL_ADD - 注册 fd 到 epoll instance,fd 成为 epoll set 一员</li>
<li>EPOLL_CTL_DEL - 把 fd 从 epoll set 删除,删除后进程无法得到 fd 任何事件的通知。如果 fd 注册到多个epoll instance 中,fd 关闭将导致 fd 从所有 epoll set 中删除</li>
<li>EPOLL_CTL_MOD - 修改监听 fd 的事件</li>
</ul>
</li>
<li>event - 事件信息,具体如下所示</li>
</ul>
<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">typedef</span> <span class="k">union</span> <span class="n">epoll_data</span> <span class="p">{</span>
<span class="kt">void</span> <span class="o">*</span><span class="n">ptr</span><span class="p">;</span>
<span class="kt">int</span> <span class="n">fd</span><span class="p">;</span>
<span class="kt">uint32_t</span> <span class="n">u32</span><span class="p">;</span>
<span class="kt">uint64_t</span> <span class="n">u64</span><span class="p">;</span>
<span class="p">}</span> <span class="n">epoll_data_t</span><span class="p">;</span>
<span class="k">struct</span> <span class="n">epoll_event</span> <span class="p">{</span>
<span class="kt">uint32_t</span> <span class="n">events</span><span class="p">;</span> <span class="cm">/* Epoll events */</span>
<span class="n">epoll_data_t</span> <span class="n">data</span><span class="p">;</span> <span class="cm">/* User data variable */</span>
<span class="p">};</span>
</code></pre></div></div>
<p>其中事件类型通过 uint32_t events 的 bit 表示,epoll_data 一般存放发生事件的 fd。</p>
<h2 id="epoll_wait">epoll_wait</h2>
<p>线程调用 epoll_wait 会一直阻塞,直到 epoll set 里面有 fd 的 I/O 就绪。当 epoll_wait 返回后,线程遍历 evlist,处理 READY LIST 里面的就绪的 I/O 事件。</p>
<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cp">#include <sys/epoll.h>
</span><span class="kt">int</span> <span class="nf">epoll_wait</span><span class="p">(</span><span class="kt">int</span> <span class="n">epfd</span><span class="p">,</span> <span class="k">struct</span> <span class="n">epoll_event</span> <span class="o">*</span><span class="n">evlist</span><span class="p">,</span> <span class="kt">int</span> <span class="n">maxevents</span><span class="p">,</span> <span class="kt">int</span> <span class="n">timeout</span><span class="p">);</span>
</code></pre></div></div>
<h2 id="lt--et">LT & ET</h2>
<p>对于 write 来说,当内核缓冲区非满(包括空和有部分数据数据),LT 模式下 EPOLLOUT 会一直触发,当缓冲区从满到非满,ET 模式下 EPOLLOUT 才会触发。对于 read 来说,当缓冲区非空(包括满和有部分数据),LT 模式下 EPOLLIN 会一直触发,当缓冲区从空到非空,ET 模式下 EPOLLIN 才会触发。默认触发方式是 LT,如果是 ET,在 epoll_ctl 函数里面设置参数 event.events | EPOLLET 。</p>
<p>因为 LT & ET 触发方式不同,处理事件的逻辑也不同。先看 manpage 里面的一个例子</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>1. The file descriptor that represents the read side of a pipe (rfd) is registered on the epoll instance.
2. A pipe writer writes 2 kB of data on the write side of the pipe.
3. A call to epoll_wait(2) is done that will return rfd as a ready file descriptor.
4. The pipe reader reads 1 kB of data from rfd.
5. A call to epoll_wait(2) is done.
</code></pre></div></div>
<p>在 ET 模式下,这种情况可能导致进程一直阻塞。</p>
<ul>
<li>假设 pipe 刚开始是空的,A端发送 2KB,然后等待B端的响应。</li>
<li>步骤2完成后,缓冲区从空变成非空,ET 会触发 EPOLLIN 事件</li>
<li>步骤3 epoll_wait 正常返回</li>
<li>B开始读操作,但是只从管道读 1KB 数据</li>
<li>步骤5调用 epoll_wait 将一直阻塞。因为 ET 下,缓冲区从空变成非空,才会触发 EPOLLIN 事件,缓冲区从满变成非满,才会触发 EPOLLOUT 事件。而当前情况不满足任何触发条件,所以 epoll_wait 会一直阻塞。</li>
</ul>
<p>如何解决呢,一个办法就是步骤4一直读,直到数据全部读完,但是在 blocking IO 下会出现另外一个问题,如果某次读完内核缓冲区后,再次调用 read 时,线程将会阻塞。所以需要设置 fd 是非阻塞的,当调用 read 或者 write 时,当返回 EAGIN/EWOULDBLOCK 后才去调用 epoll_wait。</p>
<p>在LT模式下,步骤4结束后,缓冲区还有数据,所以步骤5的 epoll_wait 不会阻塞,因为 EPOLLIN 事件不会丢失,会一直触发。但是也有一个问题,如果一次读的数据太少,将导致多次调用 epoll_wait,所以效率会有所下降。</p>
<p>为了减少 epoll_wait 调用次数,也可以采用ET的模式,使用非阻塞 IO,然后读写直到返回 EAGIN/EWOULDBLOCK。</p>
<p>LT/ET 在非阻塞处理有一点点不同,具体参考网络大神的总结 <a href="https://zhuanlan.zhihu.com/p/21374980">epoll LT/ET 深度剖析</a></p>
<h1 id="reference">reference</h1>
<ul>
<li>《Unix环境高级编程》</li>
<li><a href="https://www.zhihu.com/question/20122137/answer/14049112">I/O与epoll</a></li>
<li><a href="https://zhuanlan.zhihu.com/p/21374980">epoll LT/ET 深度剖析</a></li>
<li><a href="https://medium.com/@copyconstruct/the-method-to-epolls-madness-d9d2d6378642">The method to epoll’s madness</a></li>
</ul>Linbo Liao一直没搞明白 epoll 的机制,以前看不明白 epoll 资料就放弃了。最近重新看这些资料,感觉看明白了大部分。记一下,省的以后又糊涂了。以下内容都是各种资料的小结,以后翻阅省事一点。《孤筏重洋》2019-01-18T00:00:00+00:002019-01-18T00:00:00+00:00http://linbo.github.io/2019/01/18/kon-tiki-expedition<blockquote>
<p>不相信我的理论?老子证明给你看</p>
</blockquote>
<h1 id="缘起">缘起</h1>
<p><a href="https://book.douban.com/subject/1013208/">《如何阅读一本书》</a>说阅读有四个层次,其中第三个层次分析阅读是作者重点讲解的部分。分析阅读的第一个阶段有四个规则,分别是:</p>
<ol>
<li>尽早知道所读书的分类</li>
<li>最简短的句子描述书的内容</li>
<li>按照顺序和关系,列出全书最重要的部分,给全书拟纲要</li>
<li>找出作者问的问题或者作者想解决的问题</li>
</ol>
<p>觉得还是有必要训练一下,以后有空便多做锻炼,勤记录,不然看书感觉效率低下,时间有些浪费。</p>
<h1 id="内容">内容</h1>
<p><a href="https://book.douban.com/subject/25720197/">《孤筏重洋》</a>讲述作者托尔·海尔达尔根据自己在太平洋波利尼西亚海岛的调查研究,认为群岛的第一批居民是在公元五世纪从南美洲漂洋过海而来的。但是没有人相信他的理论,于是他便约了五个伙伴,按照古代印第安人木筏的样式,造了一只木筏,从秘鲁漂了四千多海里,漂了三个多月,漂到了波利尼西亚群岛。</p>
<h1 id="简要大纲">简要大纲</h1>
<p>全书分为四个部分</p>
<ul>
<li>理论受挫,于是作者动了造木筏的心思,证明单单靠木筏,是可以横渡太平洋的</li>
<li>一不做二不休,行动起来</li>
<li>横渡太平洋</li>
<li>顺利到达南海群岛</li>
</ul>
<h1 id="最重要的部分">最重要的部分</h1>
<p>虽然横渡太平洋所占篇幅最大,除了几次和风浪搏斗,最后登陆的惊心动魄,其它时候看起来似乎是作者和小伙伴们三个多月惬意的海上之行。个人认为最精彩的部分是第二部分,主要是有点点天方夜谭。</p>
<p>老外的冒险精神也不知道是不是与生俱来的,自己的理论不受待见,便撸起袖子,老子证明给你看。要是换了我,连这念头都不可能有的。</p>
<p>只有坚定的信念是远远不够的,还需要执行力。要造一个几世纪之前的木筏,看看作者的神操作吧。</p>
<p>“21世纪,什么最贵?人才!”</p>
<p>作者在很早之前就做了很多准备,查阅了很多资料,但是当这个念头要真正实施的时候,第一步就是要找人,而且要找对人。这里的人不但包括要一起横渡太平洋的五个小伙伴,还包括造木筏需要的形形色色的人才!</p>
<p>不得不佩服作者的人脉,在造木筏过程中,不同的人 - 包括美国国防部官员,海军将军,英国军医,联合国秘书长,助理秘书长,挪威大使,秘鲁筏木王,秘鲁海军部长,秘鲁总统 - 都对作者的壮举提供了很大的帮助。印象最深的是两个:</p>
<ol>
<li>美国国防部首长问作者,如果提供帮助能得到什么回报,作者的回答无法令他满意,不过最后还是同意援助物资,因为他觉得,勇气和胆量也是报答。不愧为军人!</li>
<li>作者找不到一个宽阔的地方造木筏,于是求助秘鲁总统(嗯,看看人家都可以见什么人)。好玩的是觐见总统,作者见好几个穿戴的富丽堂皇的人,都以为是总统,结果都不是。最后见了一个像仆人的人,结果是总统,😄。最后总统同意在海军军港造木筏!</li>
</ol>
<p>人脉固然重要,但是碰到问题,能百折不挠,果敢前进,不达目的不罢休的人,才是真的猛士,作者和同伴便是如此。雨季很难进森林找筏木,作者硬是克服种种困难,找到造木筏所需的各种木料,最后在海军军港,造成了横渡太平洋的木筏。</p>
<h1 id="感想">感想</h1>
<p>也许刚经历过二战,那个时代的人什么都见识过了,便不觉得什么是不可能的。现代的人估计懦弱的多了,这种刚猛,富于冒险的事情几乎看不到了。也许下一次人类再现这种与生俱来的精神,可能是浩瀚无垠的太空了。</p>Linbo Liao不相信我的理论?老子证明给你看Goodbye 20182018-12-31T00:00:00+00:002018-12-31T00:00:00+00:00http://linbo.github.io/2018/12/31/goodbye-2018<blockquote>
<p><em>这才知道我的全部努力</em><br />
<em>不过完成了普通的生活</em><br />
- 穆旦《冥想》</p>
</blockquote>
<p>以前每到年末,各种年度总结、年末报告、跨年演讲雪花似的漫天飞舞。也许今年经济不好,大家忙着过冬,也没什么好炫耀的,也就把这档子事给忘了。</p>
<p>2018年终于快熬完了,一些事情让自己褪去青春的激情,慢慢的走向平淡,也开始对一些事情有了不一样的想法,可能这就是人生吧。</p>
<p>去年公司体检,发现身体一些不太好的症状,也没太在意,年初去医院复查才发现比较严重。不幸中的万幸是,还是比较早期的问题,手术基本上能根治,只不过术后需要一直服药也要定期复查。春节后在广州进行了手术,虽然手术过程有一点点不顺利,不过结果还不错,到现在几次术后复查也没什么异常。只不过这事对自己身心冲击不小,恢复也是一个长期过程。</p>
<p>以前也没太把身体当回事,虽没天天熬夜,但都比较晚休息。由于各种原因,很多时候压力都比较大,久而久之,不但影响了身体,也造成精神的一些问题,比如堆积压力,调节释放不够等等。身体问题至少可以通过饮食,锻炼,休息去调整,但是精神方面就比较麻烦一点,怀疑这些是自己得病的部分原因。不过早认识问题总比没认识或者晚认识强,至少还有与之斗争的时间和勇气。</p>
<p>手术后在家休息了一个月,才去公司上班。现在的工作已不是那么的纯粹,或者不那么的深耕一方,更需要从整体角度,看待一个项目或者一个产品。面广,人杂,事多,纷繁复杂,千头万绪,任何环节都有风险,有失控的可能性。现在工作基本上不太关注技术,很多时间精力花在方案设计,项目管理,产品提炼等方面,也算是用另外一只眼睛看产品,用另外一只手去做产品。</p>
<p>现在技术的发展趋势,便是一体化了。运维体系的一体化,开发测试运维的一体化,以后的IT高精端人士,便如同工业革命中的工人一样,都是流水线里面可以随意替代的螺丝钉。比如业务开发真的是傻瓜式的工人了,只需要写业务逻辑,不用关心底层设施,测试,运维等等产品研发的各种环节,代码结束往CI/CD流水线一扔完事。IT民工的时代,也许真的大踏步的向我们走来。</p>
<p>毕业之后再也没有回过上海了,想想离开上海都快十年了,时间过的真快,第一次坐火车去上海,沿途的风景还历历在目。九月中旬去了上海一趟,本来还想好好玩玩,去母校怀念一下曾经的青春岁月,结果碰到深圳的台风,会还没有参加完就急急忙忙赶回来了。几天的上海时光,要么在同行业公司的高楼大厦里,要么参加会议,连上海变成什么样子都无法知道。还好和快十年没见面的室友吃了一顿晚饭,和当年一起进Oracle的同事吃了一顿中饭,也算是不虚此行。</p>
<p>十一跑去马来西亚的兰卡威玩了一下,去马来和在国内真的没太大区别,一下机场全是中文,甚至连入境卡都不需要填写,一到商场,Alipay,Webchat Pay满天飞。说英文太麻烦,直接切换到中文,除了吃有点点不一样,其它真的没什么感觉。而且东南亚旅游路线,旅游方式,东南亚的海,好像真的差别不大,不过去异国他乡放松身心,也还是不错的。</p>
<p>今年也没看什么书,倒是啃完了一本英文的技术书<a href="https://book.douban.com/subject/26197294/">《Designing Data-Intensive Applications》</a>,长这么大第一次完完整整啃完的英文书,也是近年来最好的一本技术书。如果经常和数据打交道的同学,建议翻翻,基本上把数据处理的方方面面都讲了。应该算是一本入门书,很多主题只是点到为止,没有做很深入的讨论,如果真想把书中的内容全部吸收,估计得花好长好长的时间了。全书最后一节讨论数据的道德问题,把整本书的逼格提高了好多个档次,妥妥的甩了其它技术书籍好几条街了。谁说技术只是冷冰冰的东西,也有悲天悯人的人文关怀呢。</p>
<p>第二本是武汉大学哲学系教授赵林的<a href="https://book.douban.com/subject/4246032/">《西方哲学史讲演录》</a>。以前学马哲真是痛苦,也不知道考试是怎么过的,哲学对自己都快有阴影,对哲学总是敬而远之,不敢再往前踏一步。幸好赵老师通俗的讲解了西方哲学从泰勒斯到黑格尔的发展历程,总算对哲学有个大致的了解。感觉细细品尝哲学的一些东西,还是蛮有意思的,以后有空去翻翻那些哲学大家的作品,但愿不会打瞌睡。</p>
<p>上半年也看了网易的公开课<a href="http://open.163.com/special/positivepsychology/">《哈佛幸福课》</a>,不过半途而废的毛病又犯了,到现在还没有看完。内容真的不错,很多知识对平常生活都很有指导作用,可惜都没有知行合一。明年要多看几遍,然后生活上多实践,相信一定会让自己的生活更平和、更积极、更幸福。身体和心理两方面都要不断探索,不断学习,不断修炼,这样才能在现今的社会里,更好的活着。</p>
<p>去年的计划一个都没有完成,真是言而无信。xv6看到进程那块,Homework一直搞不定,就一直耽搁着,还有剩下的磁盘部分,今年一定要完成,不然说到OS,怎么和人家谈笑风生呢。原来以为今年研发会上Docker,结果同事调研下来,发现对业务的作用没有那么大,经济不好嘛,这事就暂时停滞。不过也看了Docker和K8S的资料,也了解部分的基础概念,总算抓住现在技术潮流的尾巴,不至于掉队太多,不会望尘莫及。未来有机会如果能玩玩微服务和Docker那一套,也是不错。Raft协议,Rust都泡汤了,今年应该用做项目的方法,定一些切实可行的计划,能在各个方面继续努力。</p>
<p>写完这些文字,突然感觉未来充满希望。Goodbye 2018, Hello 2019!</p>Linbo Liao这才知道我的全部努力 不过完成了普通的生活 - 穆旦《冥想》XV6 - First Process(1)2018-04-14T00:00:00+00:002018-04-14T00:00:00+00:00http://linbo.github.io/2018/04/14/xv6-first_process_1<h1 id="进程信息">进程信息</h1>
<h2 id="地址空间">地址空间</h2>
<p>进程地址空间分成三部分:</p>
<ul>
<li>用户内存<br />
用户内存位于 <code class="language-plaintext highlighter-rouge">0 ~ 0x7FFFFFFF</code>,从底向上依次是
<ol>
<li>进程的指令和数据</li>
<li>进程的栈</li>
<li>进程的堆</li>
</ol>
</li>
<li>
<p>BIOS<br />
BIOS 被映射到 <code class="language-plaintext highlighter-rouge">0x80000000 ~ 0x800FFFFF</code>处</p>
</li>
<li>Kernel 区域<br />
内核指令和数据映射到 <code class="language-plaintext highlighter-rouge">0x80100000 ~ 0xFFFFFFFF</code>。当进程使用系统调用时,系统调用会在进程地址空间的内核区域运行,这样可以使得系统调用代码直接指向用户内存。</li>
</ul>
<p><strong>发生系统调用时,所有代码(包括内核代码)都在同一个进程的地址空间中,所以不需要切换页表</strong></p>
<h2 id="进程状态">进程状态</h2>
<p><code class="language-plaintext highlighter-rouge">proc.h</code>的结构体维护了进程的状态</p>
<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// Per-process state</span>
<span class="k">struct</span> <span class="n">proc</span> <span class="p">{</span>
<span class="n">uint</span> <span class="n">sz</span><span class="p">;</span> <span class="c1">// Size of process memory (bytes)</span>
<span class="n">pde_t</span><span class="o">*</span> <span class="n">pgdir</span><span class="p">;</span> <span class="c1">// Page table</span>
<span class="kt">char</span> <span class="o">*</span><span class="n">kstack</span><span class="p">;</span> <span class="c1">// Bottom of kernel stack for this process</span>
<span class="k">enum</span> <span class="n">procstate</span> <span class="n">state</span><span class="p">;</span> <span class="c1">// Process state</span>
<span class="kt">int</span> <span class="n">pid</span><span class="p">;</span> <span class="c1">// Process ID</span>
<span class="k">struct</span> <span class="n">proc</span> <span class="o">*</span><span class="n">parent</span><span class="p">;</span> <span class="c1">// Parent process</span>
<span class="k">struct</span> <span class="n">trapframe</span> <span class="o">*</span><span class="n">tf</span><span class="p">;</span> <span class="c1">// Trap frame for current syscall</span>
<span class="k">struct</span> <span class="n">context</span> <span class="o">*</span><span class="n">context</span><span class="p">;</span> <span class="c1">// swtch() here to run process</span>
<span class="kt">void</span> <span class="o">*</span><span class="n">chan</span><span class="p">;</span> <span class="c1">// If non-zero, sleeping on chan</span>
<span class="kt">int</span> <span class="n">killed</span><span class="p">;</span> <span class="c1">// If non-zero, have been killed</span>
<span class="k">struct</span> <span class="n">file</span> <span class="o">*</span><span class="n">ofile</span><span class="p">[</span><span class="n">NOFILE</span><span class="p">];</span> <span class="c1">// Open files</span>
<span class="k">struct</span> <span class="n">inode</span> <span class="o">*</span><span class="n">cwd</span><span class="p">;</span> <span class="c1">// Current directory</span>
<span class="kt">char</span> <span class="n">name</span><span class="p">[</span><span class="mi">16</span><span class="p">];</span> <span class="c1">// Process name (debugging)</span>
<span class="p">};</span>
</code></pre></div></div>
<p>每个进程都有一个线程用来执行进程的指令,系统在进程间切换,就是切换进程内的线程,线程的大部分状态保存在线程的栈上。</p>
<p>每个进程有用户栈和内核栈,当执行用户指令时,使用用户栈,当进程通过系统调用和中断进入内核时,切换用户栈到内核栈。</p>
<p>其它重要的状态有如下这些</p>
<ul>
<li>页表 <code class="language-plaintext highlighter-rouge">proc->pgdir</code></li>
<li>进程状态 <code class="language-plaintext highlighter-rouge">proc->state</code></li>
<li>进程上下文切换的 Trap frame <code class="language-plaintext highlighter-rouge">proc->tf</code> //syscall有具体说明</li>
<li>切换进程的上下文信息 <code class="language-plaintext highlighter-rouge">proc->context</code></li>
</ul>
<h1 id="创建第一个用户进程">创建第一个用户进程</h1>
<p><code class="language-plaintext highlighter-rouge">userinit</code> 用来创建第一个用户进程,主要工作就是分配内存,设置 <code class="language-plaintext highlighter-rouge">struct proc</code> 结构体相关的信息。</p>
<p><code class="language-plaintext highlighter-rouge">userinit</code> 调用 <code class="language-plaintext highlighter-rouge">allocproc</code> 分配 <code class="language-plaintext highlighter-rouge">struct proc</code>,并设置相关字段。xv6 维护一个 <code class="language-plaintext highlighter-rouge">struct proc</code> 数组,当创建新进程的时候,找到表中未用的元素,用来存放当前进程的 <code class="language-plaintext highlighter-rouge">struct proc</code>。如果没有找到,返回 NULL指针。如果表中有可用的元素,接下来就是设置 <code class="language-plaintext highlighter-rouge">struct proc</code>的相关字段,首先设置 pid 和 进程状态,然后分配内核堆栈内存,并初始化内核堆栈。</p>
<h3 id="初始化内核堆栈">初始化内核堆栈</h3>
<p>进程对应的内核堆栈结构如下</p>
<p><img src="http://linbo.github.io/assets/post/xv6/kstack.png" alt="func" /></p>
<p>内核堆栈从底向上分成三部分</p>
<ul>
<li><code class="language-plaintext highlighter-rouge">struct trapframe</code>: 系统调用或者中断发生时,需要保存的信息</li>
<li><code class="language-plaintext highlighter-rouge">trapret</code></li>
<li><code class="language-plaintext highlighter-rouge">struct context</code>: 进程切换需要保存的上下文</li>
</ul>
<p><code class="language-plaintext highlighter-rouge">allocproc</code>只是分配了内核堆栈,并在 <code class="language-plaintext highlighter-rouge">struct proc</code> 中设置 <code class="language-plaintext highlighter-rouge">trapframe</code> 和 <code class="language-plaintext highlighter-rouge">context</code> 初始位置。同时清零 <code class="language-plaintext highlighter-rouge">context</code>,设置 <code class="language-plaintext highlighter-rouge">context->eip</code> 为 <code class="language-plaintext highlighter-rouge">forkret</code>。</p>
<h3 id="初始化进程运行需要的信息">初始化进程运行需要的信息</h3>
<p>第一个进程为 <code class="language-plaintext highlighter-rouge">initcode.S</code>,链接器将这个代码嵌入到内核中,并定义两个特殊的符号:<code class="language-plaintext highlighter-rouge">_binary_initcode_start</code>(指令开始位置),<code class="language-plaintext highlighter-rouge">_binary_initcode_size</code>(进程大小)。</p>
<p><code class="language-plaintext highlighter-rouge">userinit</code> 调用 <code class="language-plaintext highlighter-rouge">setupkvm</code> 创建页表,映射内核代码到用户的地址空间。然后调用 <code class="language-plaintext highlighter-rouge">inituvm</code> 分配物理内存,将程序拷贝到物理内存,创建用户程序的页表映射。下面代码可以知道,程序被映射到虚拟地址 0 开始的位置,所以第一条指令的地址是 0。</p>
<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">mappages</span><span class="p">(</span><span class="n">pgdir</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="n">PGSIZE</span><span class="p">,</span> <span class="n">V2P</span><span class="p">(</span><span class="n">mem</span><span class="p">),</span> <span class="n">PTE_W</span><span class="o">|</span><span class="n">PTE_U</span><span class="p">);</span>
</code></pre></div></div>
<p>接下来初始化 <code class="language-plaintext highlighter-rouge">struct trapframe</code>,主要是段寄存器,用户栈相关的寄存器,状态寄存器和 eip。最后将进程状态设置为 <code class="language-plaintext highlighter-rouge">RUNNABLE</code>,等待内核调度线程调度,获取 CPU 运行。</p>
<h1 id="进程调度器">进程调度器</h1>
<p>到现在为止,CPU 运行的所有代码都是内核代码,包括前面的进程创建代码。接下来,每个 CPU 会起一个调度器,找到一个 <code class="language-plaintext highlighter-rouge">RUNNABLE</code> 进程,切换当前内核调度器到可运行的用户线程上,运行用户进程。</p>
<p><code class="language-plaintext highlighter-rouge">main.c:mpmain(void)</code> 会调用 <code class="language-plaintext highlighter-rouge">proc.c:scheduler(void)</code> ,运行调度器。调度器是一个死循环,它查找 <code class="language-plaintext highlighter-rouge">proc</code> 数组,找到可运行的进程,切换内核调度器到可运行的进程并运行,并设置进程状态为 <code class="language-plaintext highlighter-rouge">RUNNING</code>。</p>
<h2 id="设置-cpu">设置 CPU</h2>
<p>调度器首先设置当前 CPU 的一些信息,这部分在系统调用写过,重新贴一下。</p>
<p>xv6 会为每个 CPU 创建一些 CPU 的状态信息,具体为 <code class="language-plaintext highlighter-rouge">proc.h:struct cpu</code>。在每个 CPU 结构体中有两个字段,一个是 GDT,一个是 TSS。其中 TSS 描述符会安装到 GDT 中。</p>
<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">struct</span> <span class="n">cpu</span> <span class="p">{</span>
<span class="p">...</span>
<span class="k">struct</span> <span class="n">taskstate</span> <span class="n">ts</span><span class="p">;</span> <span class="c1">// Used by x86 to find stack for interrupt</span>
<span class="k">struct</span> <span class="n">segdesc</span> <span class="n">gdt</span><span class="p">[</span><span class="n">NSEGS</span><span class="p">];</span> <span class="c1">// x86 global descriptor table</span>
<span class="p">...</span>
<span class="p">}</span>
</code></pre></div></div>
<p>每个进程运行之前,内核会设置这些数据结构,具体代码在 <code class="language-plaintext highlighter-rouge">vm.c:switchuvm()</code>,主要是安装 TSS 描述符到 GDT中,然后设置 TSS 的内核栈和 iomb,其它3个特权级的栈信息和其它信息都没有用到。其中 TSS 的内核栈就是内核为进程创建的内核栈。最后装载 TSS 段,并切换页表。</p>
<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code> <span class="n">cpu</span><span class="o">-></span><span class="n">gdt</span><span class="p">[</span><span class="n">SEG_TSS</span><span class="p">]</span> <span class="o">=</span> <span class="n">SEG16</span><span class="p">(</span><span class="n">STS_T32A</span><span class="p">,</span> <span class="o">&</span><span class="n">cpu</span><span class="o">-></span><span class="n">ts</span><span class="p">,</span> <span class="k">sizeof</span><span class="p">(</span><span class="n">cpu</span><span class="o">-></span><span class="n">ts</span><span class="p">)</span><span class="o">-</span><span class="mi">1</span><span class="p">,</span> <span class="mi">0</span><span class="p">);</span>
<span class="n">cpu</span><span class="o">-></span><span class="n">gdt</span><span class="p">[</span><span class="n">SEG_TSS</span><span class="p">].</span><span class="n">s</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>
<span class="n">cpu</span><span class="o">-></span><span class="n">ts</span><span class="p">.</span><span class="n">ss0</span> <span class="o">=</span> <span class="n">SEG_KDATA</span> <span class="o"><<</span> <span class="mi">3</span><span class="p">;</span>
<span class="n">cpu</span><span class="o">-></span><span class="n">ts</span><span class="p">.</span><span class="n">esp0</span> <span class="o">=</span> <span class="p">(</span><span class="n">uint</span><span class="p">)</span><span class="n">p</span><span class="o">-></span><span class="n">kstack</span> <span class="o">+</span> <span class="n">KSTACKSIZE</span><span class="p">;</span>
<span class="c1">// setting IOPL=0 in eflags *and* iomb beyond the tss segment limit</span>
<span class="c1">// forbids I/O instructions (e.g., inb and outb) from user space</span>
<span class="n">cpu</span><span class="o">-></span><span class="n">ts</span><span class="p">.</span><span class="n">iomb</span> <span class="o">=</span> <span class="p">(</span><span class="n">ushort</span><span class="p">)</span> <span class="mh">0xFFFF</span><span class="p">;</span>
<span class="n">ltr</span><span class="p">(</span><span class="n">SEG_TSS</span> <span class="o"><<</span> <span class="mi">3</span><span class="p">);</span>
<span class="n">lcr3</span><span class="p">(</span><span class="n">V2P</span><span class="p">(</span><span class="n">p</span><span class="o">-></span><span class="n">pgdir</span><span class="p">));</span> <span class="c1">// switch to process's address space</span>
</code></pre></div></div>
<p>这里载入了进程的页表,为什么后面代码还能继续运行呢?因为对于内核物理内存,在内核的地址空间和进程地址空间,都被映射到相同的地方,即从 <code class="language-plaintext highlighter-rouge">0x08100000 ~ 0xFFFFFFFF</code></p>
<h3 id="内核初始页表">内核初始页表</h3>
<p>先看看内核初始页表内容</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="o">(</span>gdb<span class="o">)</span> b 22
Breakpoint 1 at 0x801038ab: file main.c, line 22.
<span class="o">(</span>gdb<span class="o">)</span> c
Continuing.
The target architecture is assumed to be i386
<span class="o">=></span> 0x801038ab <main+34>: call 0x80103cae <mpinit>
Breakpoint 1, main <span class="o">()</span> at main.c:22
22 mpinit<span class="o">()</span><span class="p">;</span> // detect other processors
</code></pre></div></div>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="o">(</span>qemu<span class="o">)</span> info pg
VPN range Entry Flags Physical page
<span class="o">[</span>80000-803ff] PDE[200] <span class="nt">----A--UWP</span>
<span class="o">[</span>80000-800ff] PTE[000-0ff] <span class="nt">--------WP</span> 00000-000ff
<span class="o">[</span>80100-80102] PTE[100-102] <span class="nt">---------P</span> 00100-00102
<span class="o">[</span>80103-80103] PTE[103] <span class="nt">----A----P</span> 00103
<span class="o">[</span>80104-80106] PTE[104-106] <span class="nt">---------P</span> 00104-00106
<span class="o">[</span>80107-80108] PTE[107-108] <span class="nt">----A----P</span> 00107-00108
<span class="o">[</span>80109-80109] PTE[109] <span class="nt">---------P</span> 00109
<span class="o">[</span>8010a-8010c] PTE[10a-10c] <span class="nt">--------WP</span> 0010a-0010c
<span class="o">[</span>8010d-8010d] PTE[10d] <span class="nt">----A---WP</span> 0010d
<span class="o">[</span>8010e-803ff] PTE[10e-3ff] <span class="nt">--------WP</span> 0010e-003ff
<span class="o">[</span>80400-8dfff] PDE[201-237] <span class="nt">-------UWP</span>
<span class="o">[</span>80400-8dfff] PTE[000-3ff] <span class="nt">--------WP</span> 00400-0dfff
<span class="o">[</span>fe000-fffff] PDE[3f8-3ff] <span class="nt">-------UWP</span>
<span class="o">[</span>fe000-fffff] PTE[000-3ff] <span class="nt">--------WP</span> fe000-fffff
<span class="o">(</span>qemu<span class="o">)</span> info registers
...
<span class="nv">CR0</span><span class="o">=</span>80010011 <span class="nv">CR2</span><span class="o">=</span>00000000 <span class="nv">CR3</span><span class="o">=</span>003ff000 <span class="nv">CR4</span><span class="o">=</span>00000010
...
</code></pre></div></div>
<h3 id="没有切换页表前">没有切换页表前</h3>
<p>在调用 <code class="language-plaintext highlighter-rouge">vm.c:switchuvm()</code> 前,可以看到页表很多熟悉已经被改变了,有些被访问过了(A被置位),有些页面已经被修改(D被置位)</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="o">(</span>qemu<span class="o">)</span> info registers
...
<span class="nv">CR0</span><span class="o">=</span>80010011 <span class="nv">CR2</span><span class="o">=</span>00000000 <span class="nv">CR3</span><span class="o">=</span>003ff000 <span class="nv">CR4</span><span class="o">=</span>00000010
...
<span class="o">(</span>qemu<span class="o">)</span> info pg
VPN range Entry Flags Physical page
<span class="o">[</span>80000-803ff] PDE[200] <span class="nt">----A--UWP</span>
<span class="o">[</span>80000-80000] PTE[000] <span class="nt">----A---WP</span> 00000
<span class="o">[</span>80001-80006] PTE[001-006] <span class="nt">--------WP</span> 00001-00006
<span class="o">[</span>80007-80007] PTE[007] <span class="nt">---DA---WP</span> 00007
<span class="o">[</span>80008-8009e] PTE[008-09e] <span class="nt">--------WP</span> 00008-0009e
<span class="o">[</span>8009f-8009f] PTE[09f] <span class="nt">----A---WP</span> 0009f
<span class="o">[</span>800a0-800b7] PTE[0a0-0b7] <span class="nt">--------WP</span> 000a0-000b7
<span class="o">[</span>800b8-800b8] PTE[0b8] <span class="nt">---DA---WP</span> 000b8
<span class="o">[</span>800b9-800ef] PTE[0b9-0ef] <span class="nt">--------WP</span> 000b9-000ef
<span class="o">[</span>800f0-800f1] PTE[0f0-0f1] <span class="nt">----A---WP</span> 000f0-000f1
<span class="o">[</span>800f2-800ff] PTE[0f2-0ff] <span class="nt">--------WP</span> 000f2-000ff
<span class="o">[</span>80100-80108] PTE[100-108] <span class="nt">----A----P</span> 00100-00108
<span class="o">[</span>80109-80109] PTE[109] <span class="nt">---------P</span> 00109
<span class="o">[</span>8010a-8010a] PTE[10a] <span class="nt">----A---WP</span> 0010a
<span class="o">[</span>8010b-8010b] PTE[10b] <span class="nt">--------WP</span> 0010b
<span class="o">[</span>8010c-80112] PTE[10c-112] <span class="nt">---DA---WP</span> 0010c-00112
<span class="o">[</span>80113-80113] PTE[113] <span class="nt">----A---WP</span> 00113
<span class="o">[</span>80114-80114] PTE[114] <span class="nt">---DA---WP</span> 00114
<span class="o">[</span>80115-80116] PTE[115-116] <span class="nt">----A---WP</span> 00115-00116
<span class="o">[</span>80117-80117] PTE[117] <span class="nt">---DA---WP</span> 00117
<span class="o">[</span>80118-803ff] PTE[118-3ff] <span class="nt">--------WP</span> 00118-003ff
<span class="o">[</span>80400-8dfff] PDE[201-237] <span class="nt">----A--UWP</span>
<span class="o">[</span>80400-8dfff] PTE[000-3ff] <span class="nt">---DA---WP</span> 00400-0dfff
<span class="o">[</span>fe000-febff] PDE[3f8-3fa] <span class="nt">-------UWP</span>
<span class="o">[</span>fe000-febff] PTE[000-3ff] <span class="nt">--------WP</span> fe000-febff
<span class="o">[</span>fec00-fefff] PDE[3fb] <span class="nt">----A--UWP</span>
<span class="o">[</span>fec00-fec00] PTE[000] <span class="nt">---DA---WP</span> fec00
<span class="o">[</span>fec01-fedff] PTE[001-1ff] <span class="nt">--------WP</span> fec01-fedff
<span class="o">[</span>fee00-fee00] PTE[200] <span class="nt">---DA---WP</span> fee00
<span class="o">[</span>fee01-fefff] PTE[201-3ff] <span class="nt">--------WP</span> fee01-fefff
<span class="o">[</span>ff000-fffff] PDE[3fc-3ff] <span class="nt">-------UWP</span>
<span class="o">[</span>ff000-fffff] PTE[000-3ff] <span class="nt">--------WP</span> ff000-fffff
</code></pre></div></div>
<p>###切换页表后
<code class="language-plaintext highlighter-rouge">CR3</code> 变化了,但是页表映射内核的部分没有变化,而且页表属性和内核初始化页表差不多。</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="o">(</span>qemu<span class="o">)</span> info registers
...
<span class="nv">CR0</span><span class="o">=</span>80010011 <span class="nv">CR2</span><span class="o">=</span>00000000 <span class="nv">CR3</span><span class="o">=</span>0dffe000 <span class="nv">CR4</span><span class="o">=</span>00000010
...
<span class="o">(</span>qemu<span class="o">)</span> info pg
VPN range Entry Flags Physical page
<span class="o">[</span>00000-003ff] PDE[000] <span class="nt">-------UWP</span>
<span class="o">[</span>00000-00000] PTE[000] <span class="nt">-------UWP</span> 0dfbd
<span class="o">[</span>80000-803ff] PDE[200] <span class="nt">----A--UWP</span>
<span class="o">[</span>80000-800ff] PTE[000-0ff] <span class="nt">--------WP</span> 00000-000ff
<span class="o">[</span>80100-80103] PTE[100-103] <span class="nt">---------P</span> 00100-00103
<span class="o">[</span>80104-80105] PTE[104-105] <span class="nt">----A----P</span> 00104-00105
<span class="o">[</span>80106-80106] PTE[106] <span class="nt">---------P</span> 00106
<span class="o">[</span>80107-80108] PTE[107-108] <span class="nt">----A----P</span> 00107-00108
<span class="o">[</span>80109-80109] PTE[109] <span class="nt">---------P</span> 00109
<span class="o">[</span>8010a-8010c] PTE[10a-10c] <span class="nt">--------WP</span> 0010a-0010c
<span class="o">[</span>8010d-8010d] PTE[10d] <span class="nt">---DA---WP</span> 0010d
<span class="o">[</span>8010e-80113] PTE[10e-113] <span class="nt">--------WP</span> 0010e-00113
<span class="o">[</span>80114-80114] PTE[114] <span class="nt">---DA---WP</span> 00114
<span class="o">[</span>80115-803ff] PTE[115-3ff] <span class="nt">--------WP</span> 00115-003ff
<span class="o">[</span>80400-8dfff] PDE[201-237] <span class="nt">-------UWP</span>
<span class="o">[</span>80400-8dfff] PTE[000-3ff] <span class="nt">--------WP</span> 00400-0dfff
<span class="o">[</span>fe000-fffff] PDE[3f8-3ff] <span class="nt">-------UWP</span>
<span class="o">[</span>fe000-fffff] PTE[000-3ff] <span class="nt">--------WP</span> fe000-fffff
</code></pre></div></div>
<h2 id="调度">调度</h2>
<p><code class="language-plaintext highlighter-rouge">swtch</code> 调用的是 <code class="language-plaintext highlighter-rouge">swtch.S</code> 的汇编代码,因为 <code class="language-plaintext highlighter-rouge">swtch</code> 是函数调用,所以内核堆栈会把参数和 <code class="language-plaintext highlighter-rouge">eip</code> 压栈,然后跳转到 <code class="language-plaintext highlighter-rouge">swtch.S</code> 代码处。</p>
<h3 id="调用-swtch">调用 swtch</h3>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="o">=></span> 0x80104ac3 <scheduler+73>: mov <span class="nt">-0xc</span><span class="o">(</span>%ebp<span class="o">)</span>,%eax
303 swtch<span class="o">(</span>&cpu->scheduler, p->context<span class="o">)</span><span class="p">;</span>
<span class="o">(</span>gdb<span class="o">)</span> si
<span class="o">=></span> 0x80104ac6 <scheduler+76>: mov 0x1c<span class="o">(</span>%eax<span class="o">)</span>,%eax
0x80104ac6 303 swtch<span class="o">(</span>&cpu->scheduler, p->context<span class="o">)</span><span class="p">;</span>
<span class="o">(</span>gdb<span class="o">)</span> si
<span class="o">=></span> 0x80104ac9 <scheduler+79>: mov %gs:0x0,%edx
0x80104ac9 303 swtch<span class="o">(</span>&cpu->scheduler, p->context<span class="o">)</span><span class="p">;</span>
<span class="o">(</span>gdb<span class="o">)</span> si
<span class="o">=></span> 0x80104ad0 <scheduler+86>: add <span class="nv">$0x4</span>,%edx
0x80104ad0 303 swtch<span class="o">(</span>&cpu->scheduler, p->context<span class="o">)</span><span class="p">;</span>
<span class="o">(</span>gdb<span class="o">)</span> si
<span class="o">=></span> 0x80104ad3 <scheduler+89>: mov %eax,0x4<span class="o">(</span>%esp<span class="o">)</span>
0x80104ad3 303 swtch<span class="o">(</span>&cpu->scheduler, p->context<span class="o">)</span><span class="p">;</span>
<span class="o">(</span>gdb<span class="o">)</span> si
<span class="o">=></span> 0x80104ad7 <scheduler+93>: mov %edx,<span class="o">(</span>%esp<span class="o">)</span>
0x80104ad7 303 swtch<span class="o">(</span>&cpu->scheduler, p->context<span class="o">)</span><span class="p">;</span>
<span class="o">(</span>gdb<span class="o">)</span> si
<span class="o">=></span> 0x80104ada <scheduler+96>: call 0x80105528
0x80104ada 303 swtch<span class="o">(</span>&cpu->scheduler, p->context<span class="o">)</span><span class="p">;</span>
<span class="o">(</span>gdb<span class="o">)</span> info reg
eax 0x8dffff9c <span class="nt">-1912602724</span>
ecx 0x40 64
edx 0x80114844 <span class="nt">-2146351036</span>
ebx 0x10074 65652
esp 0x8010d600 0x8010d600
ebp 0x8010d628 0x8010d628
esi 0x0 0
edi 0x1178c8 1145032
eip 0x80104ada 0x80104ada <scheduler+96>
eflags 0x86 <span class="o">[</span> PF SF <span class="o">]</span>
cs 0x8 8
ss 0x10 16
ds 0x10 16
es 0x10 16
fs 0x0 0
gs 0x18 24
<span class="o">(</span>gdb<span class="o">)</span> s
<span class="o">=></span> 0x80105528: mov 0x4<span class="o">(</span>%esp<span class="o">)</span>,%eax
?? <span class="o">()</span> at swtch.S:10
10 movl 4<span class="o">(</span>%esp<span class="o">)</span>, %eax
<span class="o">(</span>gdb<span class="o">)</span> info reg
eax 0x8dffff9c <span class="nt">-1912602724</span>
ecx 0x40 64
edx 0x80114844 <span class="nt">-2146351036</span>
ebx 0x10074 65652
esp 0x8010d5fc 0x8010d5fc
ebp 0x8010d628 0x8010d628
esi 0x0 0
edi 0x1178c8 1145032
eip 0x80105528 0x80105528
eflags 0x86 <span class="o">[</span> PF SF <span class="o">]</span>
cs 0x8 8
ss 0x10 16
ds 0x10 16
es 0x10 16
fs 0x0 0
gs 0x18 24
<span class="o">(</span>gdb<span class="o">)</span> x/4x 0x8010d5fc
0x8010d5fc: 0x80104adf 0x80114844 0x8dffff9c 0x801052d9
</code></pre></div></div>
<p>可以看到,压入 <code class="language-plaintext highlighter-rouge">swtch</code> 参数后,其中第一个参数是 <code class="language-plaintext highlighter-rouge">0x80114844</code>, 第二个参数是 <code class="language-plaintext highlighter-rouge">0x8dffff9c</code>。执行 <code class="language-plaintext highlighter-rouge">call</code> 指令后,<code class="language-plaintext highlighter-rouge">esp</code> 从原来的 <code class="language-plaintext highlighter-rouge">0x8010d600</code> 变成 <code class="language-plaintext highlighter-rouge">0x8010d5fc</code>,原因就是压入了 <code class="language-plaintext highlighter-rouge">eip:0x80104adf</code>,从 <code class="language-plaintext highlighter-rouge">kernel.asm</code> 可以知道,<code class="language-plaintext highlighter-rouge">80104adf</code> 就是 <code class="language-plaintext highlighter-rouge">switchkvm()</code> 函数。</p>
<h3 id="保存旧的-context">保存旧的 context</h3>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="o">(</span>gdb<span class="o">)</span> s
<span class="o">=></span> 0x8010552c: mov 0x8<span class="o">(</span>%esp<span class="o">)</span>,%edx
11 movl 8<span class="o">(</span>%esp<span class="o">)</span>, %edx
<span class="o">(</span>gdb<span class="o">)</span> s
<span class="o">=></span> 0x80105530: push %ebp
14 pushl %ebp
<span class="o">(</span>gdb<span class="o">)</span> s
<span class="o">=></span> 0x80105531: push %ebx
15 pushl %ebx
<span class="o">(</span>gdb<span class="o">)</span> s
<span class="o">=></span> 0x80105532: push %esi
16 pushl %esi
<span class="o">(</span>gdb<span class="o">)</span> s
<span class="o">=></span> 0x80105533: push %edi
17 pushl %edi
<span class="o">(</span>gdb<span class="o">)</span> s
<span class="o">=></span> 0x80105534: mov %esp,<span class="o">(</span>%eax<span class="o">)</span>
20 movl %esp, <span class="o">(</span>%eax<span class="o">)</span>
<span class="o">(</span>gdb<span class="o">)</span> info reg
eax 0x80114844 <span class="nt">-2146351036</span>
ecx 0x40 64
edx 0x8dffff9c <span class="nt">-1912602724</span>
ebx 0x10074 65652
esp 0x8010d5ec 0x8010d5ec
ebp 0x8010d628 0x8010d628
esi 0x0 0
edi 0x1178c8 1145032
eip 0x80105534 0x80105534
eflags 0x86 <span class="o">[</span> PF SF <span class="o">]</span>
cs 0x8 8
ss 0x10 16
ds 0x10 16
es 0x10 16
fs 0x0 0
gs 0x18 24
<span class="o">(</span>gdb<span class="o">)</span> x/5x 0x8010d5ec
0x8010d5ec: 0x001178c8 0x00000000 0x00010074 0x8010d628
0x8010d5fc: 0x80104adf
</code></pre></div></div>
<p><code class="language-plaintext highlighter-rouge">swtch.S</code> 首先获取两个参数,并将当前的 <code class="language-plaintext highlighter-rouge">context</code> 压栈,查看寄存器,可以看到,第一个参数值存放在 <code class="language-plaintext highlighter-rouge">eax</code>,第二个参数存放在 <code class="language-plaintext highlighter-rouge">edx</code>。这里将当前的 <code class="language-plaintext highlighter-rouge">context: %ebp,%ebx,%esi,%edi</code> 压入当前内核栈中。</p>
<p>但是这里只保存了四个寄存器,<code class="language-plaintext highlighter-rouge">context</code> 有5个寄存器。在前面讲过,执行 <code class="language-plaintext highlighter-rouge">call</code> 后,会把 <code class="language-plaintext highlighter-rouge">eip</code> 压栈,因为 <code class="language-plaintext highlighter-rouge">swtch.S</code> 是汇编代码,和 C 代码不一样,不会把 <code class="language-plaintext highlighter-rouge">ebp</code> 压栈。进入 <code class="language-plaintext highlighter-rouge">swtch.S</code> 后只 push 4个寄存器,当保存旧 <code class="language-plaintext highlighter-rouge">context</code> 后</p>
<div class="language-nasm highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nf">movl</span> <span class="o">%</span><span class="nb">esp</span><span class="p">,</span> <span class="p">(</span><span class="o">%</span><span class="nb">eax</span><span class="p">)</span> <span class="o">//</span> <span class="nv">cpu</span><span class="o">-></span><span class="nv">scheduler</span> <span class="err">=</span> <span class="err">当前</span><span class="nb">esp</span>
</code></pre></div></div>
<p><strong><code class="language-plaintext highlighter-rouge">call</code> 后的 <code class="language-plaintext highlighter-rouge">eip</code> 就是 <code class="language-plaintext highlighter-rouge">struct context</code> 的 <code class="language-plaintext highlighter-rouge">eip</code></strong>,也就是说 <code class="language-plaintext highlighter-rouge">cpu->scheduler->eip= 0x80104adf</code>。</p>
<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">struct</span> <span class="n">context</span> <span class="p">{</span>
<span class="n">uint</span> <span class="n">edi</span><span class="p">;</span>
<span class="n">uint</span> <span class="n">esi</span><span class="p">;</span>
<span class="n">uint</span> <span class="n">ebx</span><span class="p">;</span>
<span class="n">uint</span> <span class="n">ebp</span><span class="p">;</span>
<span class="n">uint</span> <span class="n">eip</span><span class="p">;</span>
<span class="p">};</span>
</code></pre></div></div>
<h3 id="载入新的-context">载入新的 context</h3>
<p>swtch 的原型是</p>
<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">void</span> <span class="nf">swtch</span><span class="p">(</span><span class="k">struct</span> <span class="n">context</span> <span class="o">**</span><span class="n">old</span><span class="p">,</span> <span class="k">struct</span> <span class="n">context</span> <span class="o">*</span><span class="n">new</span><span class="p">);</span>
</code></pre></div></div>
<p>其中 <code class="language-plaintext highlighter-rouge">**old</code> 的值是需要保存的,<code class="language-plaintext highlighter-rouge">*new</code> 是要新载入的 <code class="language-plaintext highlighter-rouge">context</code>,汇编代码如下</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Switch stacks</span>
movl %esp, <span class="o">(</span>%eax<span class="o">)</span> // 赋值 old
movl %edx, %es // 切换到 new 中
</code></pre></div></div>
<p>在创建进程中,会把进程的 <code class="language-plaintext highlighter-rouge">context</code> 内容清零,并且把 <code class="language-plaintext highlighter-rouge">context->eip=forkret</code>。下面调试信息显示新 <code class="language-plaintext highlighter-rouge">context</code> 内容:</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="o">(</span>gdb<span class="o">)</span> s
<span class="o">=></span> 0x80105536: mov %edx,%esp
21 movl %edx, %esp
<span class="o">(</span>gdb<span class="o">)</span> s
<span class="o">=></span> 0x80105538: pop %edi
24 popl %edi
<span class="o">(</span>gdb<span class="o">)</span> info reg
eax 0x80114844 <span class="nt">-2146351036</span>
ecx 0x40 64
edx 0x8dffff9c <span class="nt">-1912602724</span>
ebx 0x10074 65652
esp 0x8dffff9c 0x8dffff9c
ebp 0x8010d628 0x8010d628
esi 0x0 0
edi 0x1178c8 1145032
eip 0x80105538 0x80105538
eflags 0x86 <span class="o">[</span> PF SF <span class="o">]</span>
cs 0x8 8
ss 0x10 16
ds 0x10 16
es 0x10 16
fs 0x0 0
gs 0x18 24
<span class="o">(</span>gdb<span class="o">)</span> x/5x 0x8dffff9c
0x8dffff9c: 0x00000000 0x00000000 0x00000000 0x00000000
0x8dffffac: 0x80104bf7
<span class="o">(</span>gdb<span class="o">)</span> s
<span class="o">=></span> 0x80105539: pop %esi
25 popl %esi
<span class="o">(</span>gdb<span class="o">)</span> s
<span class="o">=></span> 0x8010553a: pop %ebx
26 popl %ebx
<span class="o">(</span>gdb<span class="o">)</span> s
<span class="o">=></span> 0x8010553b: pop %ebp
27 popl %ebp
<span class="o">(</span>gdb<span class="o">)</span> s
<span class="o">=></span> 0x8010553c: ret
?? <span class="o">()</span> at swtch.S:28
28 ret
<span class="o">(</span>gdb<span class="o">)</span> info reg
eax 0x80114844 <span class="nt">-2146351036</span>
ecx 0x40 64
edx 0x8dffff9c <span class="nt">-1912602724</span>
ebx 0x0 0
esp 0x8dffffac 0x8dffffac
ebp 0x0 0x0
esi 0x0 0
edi 0x0 0
eip 0x8010553c 0x8010553c
eflags 0x86 <span class="o">[</span> PF SF <span class="o">]</span>
cs 0x8 8
ss 0x10 16
ds 0x10 16
es 0x10 16
fs 0x0 0
gs 0x18 24
<span class="o">(</span>gdb<span class="o">)</span> s
<span class="o">=></span> 0x80104bf7 <forkret>: push %ebp
forkret <span class="o">()</span> at proc.c:354
354 <span class="o">{</span>
</code></pre></div></div>
<p>使用栈可以很灵活,任何一块内存都可以当作栈,只需要合理设置 <code class="language-plaintext highlighter-rouge">esp</code>即可。这里重新设置栈顶寄存器为 <code class="language-plaintext highlighter-rouge">p->context</code>,那么栈就从新位置开始了。而 <code class="language-plaintext highlighter-rouge">p->context</code> 的内容已经初始化过了,最重要的 <code class="language-plaintext highlighter-rouge">eip</code> 设置为 <code class="language-plaintext highlighter-rouge">forkret</code>,弹出4个寄存器后(全部为0),代码跳转到 <code class="language-plaintext highlighter-rouge">forkret</code> 中。</p>
<h3 id="运行用户代码">运行用户代码</h3>
<p>代码进入 <code class="language-plaintext highlighter-rouge">forkret</code> 不是通过函数调用,所以不会有什么参数,或者 <code class="language-plaintext highlighter-rouge">eip</code> 压栈。在前面的图中可以看到,<code class="language-plaintext highlighter-rouge">context->eip</code> 之前的 <code class="language-plaintext highlighter-rouge">trapret</code>,所以 <code class="language-plaintext highlighter-rouge">forkret</code> 结束之后,调用 <code class="language-plaintext highlighter-rouge">ret</code> 指令,代码将跳转到 <code class="language-plaintext highlighter-rouge">trapret</code> 中。</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="o">(</span>gdb<span class="o">)</span> info reg
eax 0x80114844 <span class="nt">-2146351036</span>
ecx 0x40 64
edx 0x8dffff9c <span class="nt">-1912602724</span>
ebx 0x0 0
esp 0x8dffffb0 0x8dffffb0
ebp 0x0 0x0
esi 0x0 0
edi 0x0 0
eip 0x80104bf7 0x80104bf7 <forkret>
eflags 0x86 <span class="o">[</span> PF SF <span class="o">]</span>
cs 0x8 8
ss 0x10 16
ds 0x10 16
es 0x10 16
fs 0x0 0
gs 0x18 24
<span class="o">(</span>gdb<span class="o">)</span> x/8x 0x8dffffb0
0x8dffffb0: 0x80106820 0x00000000 0x00000000 0x00000000
0x8dffffc0: 0x00000000 0x00000000 0x00000000 0x00000000
</code></pre></div></div>
<p>可以看到,<code class="language-plaintext highlighter-rouge">trapret</code> 的地址是 <code class="language-plaintext highlighter-rouge">0x80106820</code>。而 <code class="language-plaintext highlighter-rouge">trapasm.S:trapret</code> 的流程,就是系统调用后,从内核返回到用户进程的代码。</p>
<div class="language-nasm highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="err">#</span> <span class="nf">Return</span> <span class="nv">falls</span> <span class="nv">through</span> <span class="nv">to</span> <span class="nv">trapret...</span>
<span class="nf">.globl</span> <span class="nv">trapret</span>
<span class="nl">trapret:</span>
<span class="nf">popal</span>
<span class="nf">popl</span> <span class="o">%</span><span class="nb">gs</span>
<span class="nf">popl</span> <span class="o">%</span><span class="nb">fs</span>
<span class="nf">popl</span> <span class="o">%</span><span class="nb">es</span>
<span class="nf">popl</span> <span class="o">%</span><span class="nb">ds</span>
<span class="nf">addl</span> <span class="kc">$</span><span class="mh">0x8</span><span class="p">,</span> <span class="o">%</span><span class="nb">esp</span> <span class="err">#</span> <span class="nv">trapno</span> <span class="nv">and</span> <span class="nv">errcode</span>
<span class="nf">iret</span>
</code></pre></div></div>
<p>因为前面已经设置过 <code class="language-plaintext highlighter-rouge">trap frame</code>,所以调度器终于完成调度工作,从当前的内核调度器代码,切换到用户的代码当中,并从第一条指令 <code class="language-plaintext highlighter-rouge">eip:0</code> 开始运行。</p>
<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">//proc.c:userinit(void)</span>
<span class="n">memset</span><span class="p">(</span><span class="n">p</span><span class="o">-></span><span class="n">tf</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="k">sizeof</span><span class="p">(</span><span class="o">*</span><span class="n">p</span><span class="o">-></span><span class="n">tf</span><span class="p">));</span>
<span class="n">p</span><span class="o">-></span><span class="n">tf</span><span class="o">-></span><span class="n">cs</span> <span class="o">=</span> <span class="p">(</span><span class="n">SEG_UCODE</span> <span class="o"><<</span> <span class="mi">3</span><span class="p">)</span> <span class="o">|</span> <span class="n">DPL_USER</span><span class="p">;</span>
<span class="n">p</span><span class="o">-></span><span class="n">tf</span><span class="o">-></span><span class="n">ds</span> <span class="o">=</span> <span class="p">(</span><span class="n">SEG_UDATA</span> <span class="o"><<</span> <span class="mi">3</span><span class="p">)</span> <span class="o">|</span> <span class="n">DPL_USER</span><span class="p">;</span>
<span class="n">p</span><span class="o">-></span><span class="n">tf</span><span class="o">-></span><span class="n">es</span> <span class="o">=</span> <span class="n">p</span><span class="o">-></span><span class="n">tf</span><span class="o">-></span><span class="n">ds</span><span class="p">;</span>
<span class="n">p</span><span class="o">-></span><span class="n">tf</span><span class="o">-></span><span class="n">ss</span> <span class="o">=</span> <span class="n">p</span><span class="o">-></span><span class="n">tf</span><span class="o">-></span><span class="n">ds</span><span class="p">;</span>
<span class="n">p</span><span class="o">-></span><span class="n">tf</span><span class="o">-></span><span class="n">eflags</span> <span class="o">=</span> <span class="n">FL_IF</span><span class="p">;</span>
<span class="n">p</span><span class="o">-></span><span class="n">tf</span><span class="o">-></span><span class="n">esp</span> <span class="o">=</span> <span class="n">PGSIZE</span><span class="p">;</span>
<span class="n">p</span><span class="o">-></span><span class="n">tf</span><span class="o">-></span><span class="n">eip</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="c1">// beginning of initcode.S</span>
</code></pre></div></div>
<h1 id="总结">总结</h1>
<h2 id="进程初始化">进程初始化</h2>
<p>每个进程都有自己的用户栈和内核栈,进程状态保存在 <code class="language-plaintext highlighter-rouge">struct proc</code> 中。创建新进程,主要过程是:</p>
<ul>
<li>分配物理内存(存放程序,进程相关的页表)</li>
<li>
<p>设置用户进程对应的内核栈,主要是这两个结构体</p>
<ul>
<li>初始化 <code class="language-plaintext highlighter-rouge">struct context *context;</code>,最重要是设置 <code class="language-plaintext highlighter-rouge">context->eip</code>,进程刚开始运行的指令地址</li>
<li>初始化 <code class="language-plaintext highlighter-rouge">struct trapframe *tf;</code>,保存当前进程的状态。内核调度器切换到用户进程,需要进行特权转变,过程和系统调用返回到用户进程一样。</li>
</ul>
</li>
</ul>
<h2 id="调度器调度">调度器调度</h2>
<p>内核调度器不断循环,找到可以运行的进程,分配 <code class="language-plaintext highlighter-rouge">CPU</code> 资源,运行用户程序。主要过程如下:</p>
<ul>
<li>设置 <code class="language-plaintext highlighter-rouge">CPU</code> 信息,主要是设置 <code class="language-plaintext highlighter-rouge">TSS</code> 任务段,切换页表</li>
<li>通过 <code class="language-plaintext highlighter-rouge">swtch.S</code>,保存当前内核栈的信息到 <code class="language-plaintext highlighter-rouge">cpu->scheduler</code>,切换到进程的 <code class="language-plaintext highlighter-rouge">context</code> 中。通过结合 <code class="language-plaintext highlighter-rouge">ret</code> 和堆栈信息,CPU 跳转到进程的 <code class="language-plaintext highlighter-rouge">context->eip</code>,也就是 <code class="language-plaintext highlighter-rouge">forkret</code> 代码</li>
<li>调用 <code class="language-plaintext highlighter-rouge">forkret</code> 代码没有通过 <code class="language-plaintext highlighter-rouge">call</code> 指令,当 <code class="language-plaintext highlighter-rouge">forkret</code> 结束执行 <code class="language-plaintext highlighter-rouge">ret</code>,会将当前栈顶元素弹出,当作 <code class="language-plaintext highlighter-rouge">eip</code>,也就是创建进程设置的 <code class="language-plaintext highlighter-rouge">trapret</code></li>
<li>代码跳转到 <code class="language-plaintext highlighter-rouge">trapret</code>,逻辑就和系统调用返回到用户态一样了。</li>
</ul>
<h2 id="tips">Tips</h2>
<p>如果精心构造栈内容,结合 <code class="language-plaintext highlighter-rouge">ret</code> 或者 <code class="language-plaintext highlighter-rouge">iret(有特权级转换)</code>,可以让 <code class="language-plaintext highlighter-rouge">CPU</code> 跳转到预先准备好的指令上。这样就可以实现系统调用返回到用户进程,或者内核调度线程与用户线程的切换。</p>Linbo Liao进程信息 地址空间 进程地址空间分成三部分:XV6 Homework - Lock2018-04-08T00:00:00+00:002018-04-08T00:00:00+00:00http://linbo.github.io/2018/04/08/xv6-lock<p>当多个处理器上的指令对共享数据进行操作时,可能用到锁。多个处理器运行的可能是同一份代码,也可能是不同的代码。如果共享数据是常量,或者指令都是读数据,没有修改数据,应该不需要用到锁。</p>
<p>还有一种情况是,当中断发生时,中断 handler 也访问了共享数据,这时也需要对共享数据加锁。但是这种情况可能发生死锁,例如:</p>
<ul>
<li>线程 T 为了访问共享数据,获取锁</li>
<li>T 获取锁后,发生某一中断</li>
<li>中断回调函数要访问共享数据,需要获取同一个锁</li>
<li>因为线程 T 还没有释放锁,所以中断 handler 只能等待。</li>
<li>T 等待中断处理结束后,才能释放锁,但是中断处理函数又一直等待这个锁。这种情况下,系统就会死锁。</li>
</ul>
<p>xv6 持有锁时会关闭中断,防止上述情况发生。</p>
<h1 id="锁的实现">锁的实现</h1>
<p>一个简单的实现如下</p>
<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">void</span> <span class="nf">acquire</span> <span class="p">(</span><span class="k">struct</span> <span class="n">spinlock</span> <span class="o">*</span><span class="n">lk</span><span class="p">)</span> <span class="p">{</span>
<span class="k">while</span><span class="p">(</span><span class="n">lk</span><span class="o">-></span><span class="n">locked</span> <span class="o">==</span> <span class="mi">1</span><span class="p">)</span>
<span class="p">;</span>
<span class="n">lk</span><span class="o">-></span><span class="n">locked</span> <span class="o">=</span> <span class="mi">1</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>
<p>这种实现存在的问题是,判断语句和赋值语句是多个指令,不是原子指令。线程 T1 执行到while 循环,发现锁没有被占用,此时发生调度切换(T1 还没来得及设置 <code class="language-plaintext highlighter-rouge">lk->locked = 1</code>)线程 T2 也执行到 while 循环,也可以占用锁。此时,就有两个线程获取了同一个锁。</p>
<p>所以操作系统通过硬件提供一些底层原子操作来实现锁,xv6 就是通过 xchg 指令实现的。</p>
<p><code class="language-plaintext highlighter-rouge">xchg(old_mem, new_value)</code> 功能如下,只不过下面这些操作是原子的</p>
<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">old_value</span> <span class="o">=</span> <span class="o">*</span><span class="n">old_mem</span><span class="p">;</span>
<span class="o">*</span><span class="n">old_mem</span> <span class="o">=</span> <span class="n">new_value</span><span class="p">;</span>
<span class="k">return</span> <span class="n">old_value</span><span class="p">;</span>
</code></pre></div></div>
<h1 id="锁-api">锁 API</h1>
<p>xv6 提供两个 API,用来获取和释放锁,分别是 acquire/release,下面看一下具体实现。</p>
<h2 id="pushclipopcli-函数">pushcli/popcli 函数</h2>
<p>根据前面的介绍,在获取和释放锁时,需要关闭和开启中断,这里有两个对应的函数 pushcli/popcli 处理。</p>
<p>这里主要考虑,如果当前CPU需要获取多次锁时,如何管理中断。</p>
<h3 id="pushcli">pushcli</h3>
<p>CPU 在第一次调用 pushcli 时,获取当前的 flag 寄存器的 IF 位,保存在 cpu->intena中。每次调用 pushcli 时,关闭中断,记录关闭中断的次数。</p>
<h3 id="popcli">popcli</h3>
<p>什么时候才需要恢复中断呢?</p>
<ul>
<li>如果 CPU 第一次调用 pushcli 时,中断就是关闭的,那么不需要开启中断。通过 pushcli 保存的 cpu->intena 可以知道第一次 pushcli 中断是否是关闭的</li>
<li>如果调用了多次 pushcli, 那么只有最后一次调用 popcli 的时候,才需要开启中断</li>
</ul>
<h2 id="重复获取锁">重复获取锁</h2>
<p>占用锁后,CPU 再次运行 acquire 函数,会导致获取锁的指令死循环,当前系统会 hang 住</p>
<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">//重复调用,死循环</span>
<span class="k">while</span><span class="p">(</span><span class="n">xchg</span><span class="p">(</span><span class="o">&</span><span class="n">lk</span><span class="o">-></span><span class="n">locked</span><span class="p">,</span> <span class="mi">1</span><span class="p">)</span> <span class="o">!=</span> <span class="mi">0</span><span class="p">)</span>
<span class="p">;</span>
</code></pre></div></div>
<p>所以如果出现重复调用,系统应该 panic。</p>
<h2 id="内存乱序">内存乱序</h2>
<p>有时候编译器/处理器为了提高性能,可能会把指令顺序打乱。如果现在有个加锁操作,被编译器/处理器打乱了顺序,就会导致锁的功能失效</p>
<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">//正常逻辑 //乱序</span>
<span class="n">acquire</span><span class="p">()</span> <span class="n">acquire</span><span class="p">()</span>
<span class="n">i</span><span class="o">++</span> <span class="n">release</span><span class="p">()</span>
<span class="n">release</span><span class="p">()</span> <span class="n">i</span><span class="o">++</span>
</code></pre></div></div>
<p>所以需要 <code class="language-plaintext highlighter-rouge">__sync_synchronize()</code>,禁止编译器/处理器乱序内存操作。</p>
<p>具体怎么个乱序法,还要再研究一下。</p>
<h2 id="保存调用栈信息">保存调用栈信息</h2>
<p>acquire还会保存调用栈信息,用来做调试。 <code class="language-plaintext highlighter-rouge">getcallerpcs()</code> 通过参数地址,获取 ebp 地址,然后根据 ebp 信息,就可以找出整个函数的调用栈,这里只保存了最近10个函数的调用信息。</p>
<h1 id="homework">Homework</h1>
<h2 id="acquire两次">acquire两次</h2>
<p>这个前面已经分析过了,内核会 panic</p>
<h2 id="interrupts-in-idec">Interrupts in ide.c</h2>
<p>这个作业是获取 ide 锁后,打开中断,运行后可能出现如下结果</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>cpu0: starting
sb: size 1000 nblocks 941 ninodes 200 nlog 30 logstart 2 inodestart 32 bmap start 58
init: starting sh
cpu with apicid 1: panic: acquire
80105061 80102824 80106b9d 80106835 80102986 801001e7 80101ef3 80108492 80100cd0 8010648a
</code></pre></div></div>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>80105061: acquire<span class="o">()</span> -> panic<span class="o">()</span> //spinlock.c
80102824: ideintr<span class="o">()</span> -> acquire<span class="o">()</span> //ide.c
80106b9d: <span class="nb">trap</span><span class="o">()</span> -> ideintr<span class="o">()</span> // trap.c
80106835: <span class="nb">trap</span><span class="o">()</span> //vectors.S
80102986: iderw<span class="o">()</span> -> idestart<span class="o">()</span> // ide.c
801001e7: bread -> iderw<span class="o">()</span> // bio.c
80101ef3: readi -> bread<span class="o">()</span> // fs.c
80108492: loaduvm -> readi<span class="o">()</span> // vm.c
80100cd0: <span class="nb">exec</span> -> loaduvm<span class="o">()</span> // exec.c
8010648a: sys_exec -> <span class="nb">exec</span><span class="o">()</span> //sysfile.c
</code></pre></div></div>
<p>通过调用栈可以发现,fork一个进程并调用 exec syscall 后,需要读磁盘,在 idestart 后发生中断(IDE中断?),进入 IDE 中断处理函数,而 ideintr() 也需要获取 ide 锁,于是导致重复获取锁,kernel panic。</p>
<h2 id="interrupts-in-filec">Interrupts in file.c</h2>
<p>为什么获取 file_table_lock 锁后,打开中断,kernel 没有 panic 呢?</p>
<p>原因就是发生中断后,所有中断处理函数,都不需要再次获取 file_table_lock。意思是中断处理函数不会访问 file_table_lock 锁保护的共享数据,所以这种情况,不需要处理中断。</p>
<h2 id="xv6-lock-implementation">xv6 lock implementation</h2>
<p>如果释放锁后,再去清理 <code class="language-plaintext highlighter-rouge">lk->pcs[0], lk->cpu</code>,可能出现</p>
<ul>
<li>锁释放,但是还未清理 <code class="language-plaintext highlighter-rouge">lk->pcs[0], lk->cpu</code></li>
<li>另一个 CPU 尝试获取锁并成功,设置 <code class="language-plaintext highlighter-rouge">lk->pcs[0], lk->cpu</code></li>
<li>当前 CPU 继续执行,清理了 <code class="language-plaintext highlighter-rouge">lk->pcs[0], lk->cpu</code></li>
</ul>
<p>这就会导致锁相关的信息不正确,影响锁的功能,比如判断当前 CPU 是否占用该锁,需要用到 <code class="language-plaintext highlighter-rouge">lk->cpu</code>。</p>Linbo Liao当多个处理器上的指令对共享数据进行操作时,可能用到锁。多个处理器运行的可能是同一份代码,也可能是不同的代码。如果共享数据是常量,或者指令都是读数据,没有修改数据,应该不需要用到锁。XV6 Homework - CPU alarm2018-01-06T00:00:00+00:002018-01-06T00:00:00+00:00http://linbo.github.io/2018/01/06/xv6-cpu_alarm<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>这个 Homework 不知道做的对不对,但是跑起来感觉没什么问题
</code></pre></div></div>
<p><a href="https://pdos.csail.mit.edu/6.828/2017/homework/xv6-alarm.html">CPU alrm</a> 这个 Homework 还是和 interrupt/fault 有关,一个区别是 handler 是用户代码,而不是内核代码。interrupt/fault 发生时,要执行用户的一段代码。(interrupt/fault 处理流程都差不多,后面统一用中断替代)</p>
<p>中断发生时,如果 CPU 执行的是用户代码,是需要进行内核态/用户态切换的。而 handler 涉及用户代码,刚开始的思路是这样的:</p>
<ul>
<li>中断发生时,CPU 切换到内核中,保留上下文(如果发生用户态/内核态切换),判断中断类型,执行相应的 handler</li>
<li>如果 handler 是用户代码,那岂不是又要切换回用户态?</li>
<li>执行完 handler 中的用户代码,又切换回到内核,完成 handler剩余工作</li>
<li>最后切换回用户代码,继续执行用户代码的剩余部分</li>
</ul>
<p>但是再细想,感觉不太可行。上面步骤的1和4,前面分析 syscall 的时候,已经了解怎么实现。内核态和用户态相互切换,非常复杂,要保存各种上下文。而且只接触过用户态切换到内核态,然后通过 iret 从内核态恢复到用户态,没见过内核态直接切换到用户态的,想想各种上下文(寄存器,堆栈,页表等),感觉不大现实。</p>
<h1 id="进程执行">进程执行</h1>
<p>上面方向不行,换个思路,先理一下进程是怎么执行的。</p>
<p>进程运行的模型很简单,获得时间片后,恢复进程的上下文,循环两个操作,<strong>取指令,运行</strong>。如果时间片运行完后,暂停运行,保存上下文。</p>
<p>如果没有分支判断,函数调用等情况,那么取的指令就是下一条指令,这时进程一直在顺序执行指令。如果是分支,则会跳转到其它地方的指令,就不是下一条指令了。</p>
<p>函数调用前面已经讲过,会把函数参数压栈,把下一条指令 eip 压栈,然后跳转到函数代码处执行。</p>
<h1 id="homework">homework</h1>
<p>再分析一下 homework,当发生时钟中断的时候,在时钟中断的 handler 中判断当前进程是否消耗完 ticks 时间,如果消耗完时间,执行 alarm handler。</p>
<p>alarm handler 就一个简单的函数,也就是说,要调用这个 alarm handler 函数。那么在时钟中断的 handler 里面如果调用这个用户态的函数呢?</p>
<p>在时钟中断里面,我们是可以获取用户进程的上下文的,如果手动把这个 alarm handler 函数安装到进程的上下文中,让进程恢复运行的时候,就调用这个 handler,不就OK了吗?</p>
<p>我们已经知道汇编怎么实现函数调用,那么手动实现一个函数调用,也是很简单的事情。因为 alarm handler 没有参数,所以只需要把进程的 eip 压入进程栈,然后把进程的 eip 指向 alarm handler。那么下次进程运行的时候,就会直接执行 alarm handler。当 alarm handler 运行完,也会恢复 eip,继续执行后续的指令。这样我们就把一个函数手动注入到进程的上下文中,当函数恢复运行的时候,自动调用注入的函数。</p>
<h1 id="实现">实现</h1>
<h2 id="增加-alarmtestc">增加 alarmtest.c</h2>
<p>需要增加一个用户程序,测试这个功能。实现逻辑 homework 已经给出,怎么增加一个用户程序,前面的 homework 已经实现过了。</p>
<h2 id="增加-alarm-syscall">增加 alarm syscall</h2>
<p>首先需要增加一个 syscall</p>
<p><code class="language-plaintext highlighter-rouge">int alarm(int ticks, void (*handler)());</code></p>
<p>而且 homework 也给出了 alarm 需要实现的功能,其实就是初始化进程相关的字段,一个是需要把 alarm handler 的地址保存到进程的结构体中,一个是要初始化进程消耗的时间片。</p>
<p>当然,进程的结构体需要增加相应字段,不然 <code class="language-plaintext highlighter-rouge">alarm syscall</code> 没办法初始化这些字段。</p>
<h2 id="注入-alarm-handler">注入 alarm handler</h2>
<p>主要在时钟中断的 handler 里面实现。判断时钟中断的时候,是不是用户进程在执行(即是不是从用户进程切换到内核)。如果是,就要判断进程是否存活,而且进程有没有 <code class="language-plaintext highlighter-rouge">alarm handler</code>(没有就不用处理)。</p>
<p>然后还要判断,如果没有在进程上下文注入 <code class="language-plaintext highlighter-rouge">alarm handler</code>(防止 Homework 说的 Prevent re-entrant calls to the handler),而且时间片没有用完,就在进程上下文注入 <code class="language-plaintext highlighter-rouge">alarm handler</code>,大致逻辑如下。</p>
<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">//用的是2016的代码,和2017有点区别</span>
<span class="k">if</span><span class="p">(</span><span class="n">proc</span><span class="p">){</span>
<span class="k">if</span><span class="p">((</span><span class="n">tf</span><span class="o">-></span><span class="n">cs</span> <span class="o">&</span> <span class="mi">3</span><span class="p">)</span> <span class="o">==</span> <span class="mi">3</span> <span class="o">&&</span> <span class="n">proc</span><span class="o">-></span><span class="n">alarmhandler</span> <span class="o">&&</span> <span class="n">proc</span><span class="o">-></span><span class="n">killed</span> <span class="o">!=</span> <span class="mi">1</span><span class="p">){</span>
<span class="k">if</span><span class="p">((</span><span class="n">ticks</span> <span class="o">-</span> <span class="n">proc</span><span class="o">-></span><span class="n">ticks</span><span class="p">)</span> <span class="o">>=</span> <span class="n">proc</span><span class="o">-></span><span class="n">alarmticks</span> <span class="o">&&</span> <span class="n">tf</span><span class="o">-></span><span class="n">eip</span> <span class="o">!=</span> <span class="p">(</span><span class="n">uint</span><span class="p">)</span><span class="n">proc</span><span class="o">-></span><span class="n">alarmhandler</span><span class="p">){</span>
<span class="n">tf</span><span class="o">-></span><span class="n">esp</span> <span class="o">-=</span> <span class="mi">4</span><span class="p">;</span>
<span class="o">*</span><span class="p">(</span><span class="n">uint</span> <span class="o">*</span><span class="p">)(</span><span class="n">tf</span><span class="o">-></span><span class="n">esp</span><span class="p">)</span> <span class="o">=</span> <span class="n">tf</span><span class="o">-></span><span class="n">eip</span><span class="p">;</span>
<span class="n">tf</span><span class="o">-></span><span class="n">eip</span> <span class="o">=</span> <span class="p">(</span><span class="n">uint</span><span class="p">)</span><span class="n">proc</span><span class="o">-></span><span class="n">alarmhandler</span><span class="p">;</span>
<span class="n">proc</span><span class="o">-></span><span class="n">tf</span> <span class="o">=</span> <span class="n">tf</span><span class="p">;</span>
<span class="c1">//cprintf("ticks %d %d %d\n", ticks , proc->ticks, (uint)proc->alarmhandler);</span>
<span class="n">proc</span><span class="o">-></span><span class="n">ticks</span> <span class="o">=</span> <span class="n">ticks</span><span class="p">;</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>
<h1 id="总结">总结</h1>
<p>当中断发生时,如何在中断的 handler 里调用用户进程的一个函数,原理就是把这个用户函数注入到进程的上下文中。当进程恢复上下文,继续执行时,就可以先执行注入的函数,从而达到需要的效果。</p>
<p>很多漏洞应该也是用类似手段。比如函数返回时,会从栈中弹出以前压栈的 eip,继续执行。如果在栈中做了手脚,把栈内容改一下,让这个出栈的 eip 变成黑客的程序,不就在程序中植入病毒了吗?</p>Linbo Liao这个 Homework 不知道做的对不对,但是跑起来感觉没什么问题Goodbye 20172017-12-31T00:00:00+00:002017-12-31T00:00:00+00:00http://linbo.github.io/2017/12/31/goodbye-2017<p>都忘记2017年是怎么开始的,现在已经要结束了。</p>
<h1 id="工作">工作</h1>
<p>也算是杂家,测试起家,小公司打杂,号称全栈工程师,现在又做运维。技术的各个方向都玩了个遍。其实做什么都无所谓,基础扎实,方向再怎么变化,也无所畏惧。</p>
<p>今年主要任务是做开源中间件的运维,包括一些标准化制定,自动化和少量服务化的事情,比较纯技术。玩各种中间件过程中,也碰到一些问题,可惜只做了少量的积累,以后有机会多写一些实战经验。</p>
<p>以前在小公司,追求的是分布式架构,各种组件都要水平扩展,高可用,所以用的很多技术方案都是分布式的架构。现在要求没那么高,很多组件根本没有那么大的量,仅仅高可用就满足需求。所以现在很多组件就是主备模式,也会用一些常见的负载均衡软件,一些分布式架构的东西,就依赖Zookeeper或者ETCD来搭建了。</p>
<p>用的比较多的组件就是Keepalived,LVS,Nginx这些,可以前玩过的也就是Nginx了。现在开源的软件都比较友好,文档,社区都很齐全,但是以前一些软件,就比较随意。像LVS和Keepalived,连个像样的文档都没有,所以研究过程中也碰到各种各样的问题。幸好一直在学习《TCP/IP详解》卷一,所以很多文档缺失的问题,都可以通过抓包来研究解决。而且也是一直写代码,实在无法解决的,也可以翻翻源代码。可惜精力有限,翻代码的事情很少做。</p>
<p>IT日新月异,2000年开发技术发展迅猛,什么敏捷,TDD,XP,CI等等,把原来的开发模式搞的天翻地覆。说到底,还是因为互联网的普及,导致原来的开发效率无法满足需求的变化。但是开发效率提升,必然导致产业链后端的运维感到很大的压力。所以2010年开始,运维也出现各种各样的变革,什么Devops,SRE,CD等等。</p>
<p>我也算赶上这一潮流车,最近几个月被领导抓去做持续交付。以前都是写代码的,现在摇身一变,成了做项目的,真是一把辛酸泪。等明年搞完后,再写血泪史吧。</p>
<h1 id="读书学习">读书、学习</h1>
<p>现在上班基本上坐地铁,在地铁上大概有40多分钟时间,于是无聊,前大半年在地铁上翻了很多杂七杂八的书,大概有2,30本。小说多些,比如黑塞的,卡尔维诺的,马尔克斯的,毛姆的,推理小说。看完就忘,看的时候也看不大懂,也不知道是不是在浪费时间。</p>
<p>不过还是有几本书感觉写的好,推荐一下:</p>
<ul>
<li><a href="https://book.douban.com/subject/3448867/">《采桑子》</a> - 格格叶广芩的小说,感人,动人</li>
<li><a href="https://book.douban.com/subject/23780847/">《民企江湖》</a> - 阿耐,总有些人,明知难为而为之</li>
<li><a href="https://book.douban.com/subject/25891318/">《1453》</a>、<a href="https://book.douban.com/subject/25891321/">《海洋帝国》</a>、<a href="https://book.douban.com/subject/26296352/">《财富之城》</a>地中海三部曲 - 罗杰·克劳利,基督教和伊斯兰教的千年冲突,荡气回肠</li>
</ul>
<p>据说程序员的三大浪漫是图形学,编译原理,操作系统,据说身为一个合格程序员,要写过/看过一个OS,一门语言,一个DB。如果在学生时期,要完成这个目标还是有可能的,但是工作后,难度就很大了,因为没有那么多时间。</p>
<p>不过理想总是要有的,一直对OS很有兴趣,但是一直入不了门。今年终于有所突破了,首先前半年把<a href="https://book.douban.com/subject/20492528/">《x86汇编语言》</a>啃完了,这本书真不好啃,知识点都不错,但是里面的例子搞的太复杂了,很多OS已经不用的知识,也花了大量篇幅描述。如果里面的例子能好好设计精简一下,可以成为一本OS入门的经典书籍了。</p>
<p>很久就心仪MIT的<a href="https://pdos.csail.mit.edu/6.828/2017/schedule.html">xv6</a>操作系统,但是怎么看也看不大懂,后面发现基础知识不行。今年看完了<a href="https://book.douban.com/subject/25726019/">《汇编语言(第3版)》</a>和<a href="https://book.douban.com/subject/20492528/">《x86汇编语言》</a>,再看xv6的代码,轻松多了。断断续续看了xv6的bootstrap,栈的应用,虚拟内存,系统调用部分,也终于搞明白以前不懂的东西,也把自己的理解写到blog上,也算是一种鞭策吧。还剩下硬件的部分,进程调度,文件系统,锁的部分,希望明年可以一举拿下xv6,做一个略懂OS,半合格的程序员。</p>
<p>以前在小公司,一直醉心于分布式系统的构建,也听过鼎鼎大名的 paxos,raft等协议,但是只是看看皮毛,其实还是什么都不懂。</p>
<p>因为工作中用到了zookeeper和etcd,所以想深入研究一下raft协议。还是MIT好啊,<a href="https://pdos.csail.mit.edu/6.824/">6.824</a>的课程就是实现一个raft系统。有一段时间玩了一下,发现学术界和工程界简直就是隔行如隔山,raft协议论文就10多页,结果就是一个选举,实现太复杂了,各种边界,各种意外,各种条件,勉勉强强把选举的测试跑通,也不知道实现的正确不正确。可惜后面被抓去搞持续交付,没有那么多时间了,有空还是得继续研究。</p>
<p>不过今年没怎么偷懒,blog也有更新。</p>
<h1 id="生活娱乐">生活、娱乐</h1>
<p>基本上就是过年回家,其它时间都待在深圳。今年就去过一次汕尾,结果还碰到台风,啥也没玩成。明年要计划多看看祖国的大好河山,瞧瞧花花草草,观观日月星辰。</p>
<p>精力大不如从前了,锻炼才是王道。夏天一群小伙伴还每周跑到南山游泳,后面自己有空还跑跑步。现在天冷了就懒,跑步也没跑,还是要坚持锻炼。</p>
<p>偶尔也追追美剧,比如《权力的游戏》、《毒枭3》,电影什么的看的少,有空也休闲一下。</p>
<h1 id="展望">展望</h1>
<p>明年估计要玩玩docker生态,也看看rust这门语言。</p>
<p>明年blog,能写的东西更多吧。</p>Linbo Liao都忘记2017年是怎么开始的,现在已经要结束了。