<?xml version="1.0" encoding="UTF-8"?><rss version="2.0"
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
	xmlns:wfw="http://wellformedweb.org/CommentAPI/"
	xmlns:dc="http://purl.org/dc/elements/1.1/"
	xmlns:atom="http://www.w3.org/2005/Atom"
	xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
	xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
	>

<channel>
	<title>空雪小屋 &#187; 程序设计</title>
	<atom:link href="http://blog.sorayuki.net/?cat=6&#038;feed=rss2" rel="self" type="application/rss+xml" />
	<link>http://blog.sorayuki.net</link>
	<description>现在域名是 blog.sorayuki.net ~</description>
	<lastBuildDate>Tue, 29 Nov 2022 08:27:41 +0000</lastBuildDate>
	<language>zh-CN</language>
		<sy:updatePeriod>hourly</sy:updatePeriod>
		<sy:updateFrequency>1</sy:updateFrequency>
	<generator>http://wordpress.org/?v=4.0</generator>
	<item>
		<title>使用VS2022编译7zip的控制台程序</title>
		<link>http://blog.sorayuki.net/?p=651</link>
		<comments>http://blog.sorayuki.net/?p=651#comments</comments>
		<pubDate>Tue, 29 Nov 2022 07:58:52 +0000</pubDate>
		<dc:creator><![CDATA[空雪梦见]]></dc:creator>
				<category><![CDATA[VC]]></category>
		<category><![CDATA[Windows编程]]></category>
		<category><![CDATA[问题解决]]></category>

		<guid isPermaLink="false">http://blog.sorayuki.net/?p=651</guid>
		<description><![CDATA[7zip除了官方下载安装的图形界面工具之外，是有独立控制台程序的，在CPP\7zip\Bundles里面。这个 &#8230; <a href="http://blog.sorayuki.net/?p=651" class="more-link">继续阅读<span class="screen-reader-text">使用VS2022编译7zip的控制台程序</span> <span class="meta-nav">&#8594;</span></a>]]></description>
				<content:encoded><![CDATA[<p>7zip除了官方下载安装的图形界面工具之外，是有独立控制台程序的，在CPP\7zip\Bundles里面。这个控制台程序还分为全功能版（7zz）的和精简的（7zr），但官方那边能下载到的只有精简的，全功能的那个要自己编译。</p>
<p>在用VS2022编译全功能版的时候我遇到了问题。虽然官方提供了nmake用的编译脚本，但是它跑起来总是报错，而且报错的地方在Windows SDK里面、而不是7z自己的代码。报的错误是C2279，出现在mapi.h头文件里、诸如“这个typedef地方不能写throw标识符”这样的东西。</p>
<p>网上搜索之后能找到的相关的东西不太多，有一个说法是C++标准的问题，现在的Windows SDK需要配合新版本的C++标准（C++17之类）才能正常使用。看了一下7zip源码里nmake脚本加的参数，也确实没有指定标准。</p>
<p>本来想试试换个旧版的Windows SDK行不行，一看要下载那么多东西……烦了，就没试。最后我的做法是，<span style="color: #ff0000;">打开CPP下的Build.mk，找到定义 CFLAGS 的那一行，加个 /std:c++17</span>。文件是只读的，要去掉只读属性才能保存。再编译会变成另外的错误，报异常规范不匹配。最简单的<span style="color: #ff0000;">去掉警告视为错误（搜索，去掉-WX）</span>可以编译通过。</p>
<p>另外我发现它没加链接时代码生成这样的优化，暂时不知道为什么。试着开了一下，用7zz b看跑分，和不开看不出区别。</p>
]]></content:encoded>
			<wfw:commentRss>http://blog.sorayuki.net/?feed=rss2&#038;p=651</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>使用VS2022编译7zip的控制台程序</title>
		<link>http://blog.sorayuki.net/?p=651</link>
		<comments>http://blog.sorayuki.net/?p=651#comments</comments>
		<pubDate>Tue, 29 Nov 2022 07:58:52 +0000</pubDate>
		<dc:creator><![CDATA[空雪梦见]]></dc:creator>
				<category><![CDATA[VC]]></category>
		<category><![CDATA[Windows编程]]></category>
		<category><![CDATA[问题解决]]></category>

		<guid isPermaLink="false">http://blog.sorayuki.net/?p=651</guid>
		<description><![CDATA[7zip除了官方下载安装的图形界面工具之外，是有独立控制台程序的，在CPP\7zip\Bundles里面。这个 &#8230; <a href="http://blog.sorayuki.net/?p=651" class="more-link">继续阅读<span class="screen-reader-text">使用VS2022编译7zip的控制台程序</span> <span class="meta-nav">&#8594;</span></a>]]></description>
				<content:encoded><![CDATA[<p>7zip除了官方下载安装的图形界面工具之外，是有独立控制台程序的，在CPP\7zip\Bundles里面。这个控制台程序还分为全功能版（7zz）的和精简的（7zr），但官方那边能下载到的只有精简的，全功能的那个要自己编译。</p>
<p>在用VS2022编译全功能版的时候我遇到了问题。虽然官方提供了nmake用的编译脚本，但是它跑起来总是报错，而且报错的地方在Windows SDK里面、而不是7z自己的代码。报的错误是C2279，出现在mapi.h头文件里、诸如“这个typedef地方不能写throw标识符”这样的东西。</p>
<p>网上搜索之后能找到的相关的东西不太多，有一个说法是C++标准的问题，现在的Windows SDK需要配合新版本的C++标准（C++17之类）才能正常使用。看了一下7zip源码里nmake脚本加的参数，也确实没有指定标准。</p>
<p>本来想试试换个旧版的Windows SDK行不行，一看要下载那么多东西……烦了，就没试。最后我的做法是，<span style="color: #ff0000;">打开CPP下的Build.mk，找到定义 CFLAGS 的那一行，加个 /std:c++17</span>。文件是只读的，要去掉只读属性才能保存。再编译会变成另外的错误，报异常规范不匹配。最简单的<span style="color: #ff0000;">去掉警告视为错误（搜索，去掉-WX）</span>可以编译通过。</p>
<p>另外我发现它没加链接时代码生成这样的优化，暂时不知道为什么。试着开了一下，用7zz b看跑分，和不开看不出区别。</p>
]]></content:encoded>
			<wfw:commentRss>http://blog.sorayuki.net/?feed=rss2&#038;p=651</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>线程调度和IO多路复用杂谈</title>
		<link>http://blog.sorayuki.net/?p=640</link>
		<comments>http://blog.sorayuki.net/?p=640#comments</comments>
		<pubDate>Wed, 14 Mar 2018 01:44:40 +0000</pubDate>
		<dc:creator><![CDATA[空雪梦见]]></dc:creator>
				<category><![CDATA[多线程]]></category>
		<category><![CDATA[操作系统]]></category>

		<guid isPermaLink="false">http://blog.sorayuki.net/?p=640</guid>
		<description><![CDATA[做网络编程的话，经常会听到什么epoll（linux下）、kqueue（bsd和苹果下）、io完成端口（win &#8230; <a href="http://blog.sorayuki.net/?p=640" class="more-link">继续阅读<span class="screen-reader-text">线程调度和IO多路复用杂谈</span> <span class="meta-nav">&#8594;</span></a>]]></description>
				<content:encoded><![CDATA[<p>做网络编程的话，经常会听到什么epoll（linux下）、kqueue（bsd和苹果下）、io完成端口（windows下）这样的名词，也许看了网上的资料也知道用法是什么样的，但是却对为什么这样做没有很感性的认识。我在尝试设计一个实验能够从线程调度的角度来对这个事情有直观认识，现在勉强也能算是搞出来了，所以写一篇讲讲是做了什么。</p>
<p><span id="more-640"></span></p>
<h1>1. 线程</h1>
<p>线程的事情要从CPU开始说起。CPU可以执行程序代码，我拿C语言写的计算两个数的和的程序举例子：</p>
<p>int a, b;<br />
scanf("%d%d", &amp;a, &amp;b);<br />
printf("%d", a + b);</p>
<p>这个程序学过C语言的人大体上是都写过的。不说太复杂的，我省略掉细节说，那么就是CPU会一行一行代码执行下来：首先给变量 a 和 b 分配空间，然后调用 scanf 从键盘获取输入的数字，然后计算两个数字的和，并且显示出来。</p>
<p>但是这么一台电脑，只拿来干这种事情，实在是很不合算。CPU那么快，分配完变量 a 和 b 的空间以后就坐那边等，等你输入完数字再瞬间算完显示出来，非常非常大量的时间都花在“等”上面了。这么用电脑，实在是很浪费。不应该不应该。</p>
<p>然后现在要说到多线程。书本上说到多线程的时候，时常爱拿出来说的一个例子是你可以拿电脑一边听歌一边写报告。现在还是拿个简化的模型来考虑，考虑听歌的时候电脑在干什么，考虑写报告的时候电脑在干什么。<br />
对于听歌来说，电脑做的事情其实很简单：从硬盘中把MP3读出来，解码成声卡能识别的格式，把数据传到声卡上；这一小段放完以后，重复执行这个步骤，传下一个小段。因为电脑读MP3并且解码的速度是远远大于声卡播放声音的速度的（播放快了还能听吗？），所以电脑大部分时间还是空闲的。<br />
再说写报告，如果你没有在往里面打字的话，电脑在干啥？电脑在算时间，时间差不多到了把输入光标隐藏起来，等时间差不多到了再把输入光标显示出来，然后你就看到光标在闪：显示和隐藏光标其实根本花不了多少时间，电脑大部分时间花在“等”上面。那么你在打字的时候呢？就算一分钟能敲300个按键，每个按键平均也就20毫秒，输入法对一个按键的反应时间不要那么久，结果大部分时间还是在等你按键。<br />
现在把三件事情合起来，电脑需要等声卡播放完、需要等时间到了去闪光标、需要等你按键输入，全都在等。所以它在等声卡播放完之前可以处理你按键的事情，可以处理光标闪的事情，电脑可以同时干很多事。</p>
<h1>2. 线程调度</h1>
<p>电脑虽然能像上面说的那样同时干很多事，但是电脑系统并不能完全按照这样做。一个写报告的软件，在你也不输入光标也不闪的时候可以告诉系统“我现在没有要做的事情了”，但是如果开发软件的人做得不好，忘记告诉了或者怎么的，这电脑可能就卡死在这里面没有机会干其他事了。所以系统不能这样干，比如你现在要在CPU上执行程序，系统会分配给你一小段时间，你可以用CPU这么一段时间，如果时间到了你还没告诉系统“我现在暂时干完事了”，系统也会强行不让你继续运行，它会把你从CPU里拽出来，然后放另一个要用CPU的进去；只有说找不到什么程序要用CPU了，才会让你继续用。</p>
<p>这种会把你拽出来的方式叫做“抢占式多线程”；而这种你可以抱着CPU一直用到你觉得暂时用完了，比如上面算加法的C程序，执行到scanf的时候用户没输入，那也就是“暂时用完了”：要等用户输入了才能继续用CPU，这样的使用方式，叫做非抢占式多线程。Windows、Linux或者苹果这样的系统，都用的抢占式多线程的方式，这样才不会因为有一个程序写得不太好，抱着CPU不放，导致整个系统都没法用。</p>
<h1>3. 线程切换</h1>
<p>系统要强行把一个正在执行的程序从CPU里拖出来，就也要保证下次把这个程序再放CPU里去的时候它能继续这么执行。系统在把程序从CPU里拖出来的时候会保存程序的一系列信息，比如执行到哪里了、是通过调用了多少函数执行到这个位置的、执行到这边的时候各种变量的值是多少、算一半的那些东西放在寄存器里的值是多少，这类。这些东西都保存好之后，就可以把另一个程序的这些东西进CPU，这样就做到了“继续执行另一个程序”。然后等到了时间或者程序主动表示“这一段执行完了现在没事干了”，再把程序从CPU里弄出来。<br />
在系统寻找下一个要放到CPU上执行的程序的时候，这种表示“没事干”的程序是不会被考虑的：都没事干了放进去干嘛。当然这种没事干的程序也能变成有事干，一个输入法你不按键盘的时候它没事干，你按键盘了它就有事干了；一个写文章的软件，光标显示着的时候和光标隐藏着的时候都是没事干，要从隐藏变成显示、显示变成隐藏的时候软件就有事干；一个播放音乐的软件，声卡在把程序送入的声音放完之前，播放器没事干，这段播完了的时候程序就有事干了：要送下一段进去。一个程序的状态会在这种有事干没事干之间变化。</p>
<p>这里上下文里提到的“程序”指的是线程。线条一样执行下来的程序。</p>
<p>线程的状态分为 新建、就绪、阻塞、运行、死亡 这么几个状态。重点关注就绪阻塞和运行：就绪，代表这个线程需要放到CPU上运行，比如事情干一半被操作系统强行从CPU拖出来的，就会是这种状态：它下次还要继续运行才行；阻塞，就像刚才说的，一个播放器，已经把要播放的一小段送到声卡那边去了，在声卡播放完这段之前没事干，没事干就是阻塞状态，等声卡播完这一段了它又会变成就绪状态，否则你听完这一小段音乐就听不到下一段了。运行，就是线程塞到CPU里运行了。</p>
<h1>4. 模拟多线程网络服务器的线程切换开销</h1>
<p>前面的基础概念说完了，现在要讲讲网络服务器这边是一种什么样的情况。这里以最简单的能架静态网站的HTTP服务器为例子了。</p>
<p>这样的HTTP服务器要做的事情，其实很简单地就只包含这么几个：接受用户的链接，接收用户发来的请求，发送用户请求的文件，关闭连接。但是由于网络传输是很慢的，服务器并不能处理完一个人再处理下一个人：万一遇上一个网络状态不太好的人，光是收请求和发文件就花了特别多的时间，甚至突然对方网络就断了，只能等超时，就会变成后面排了一堆要上你这个网站的人卡在这里。</p>
<p>然后就有了这样一种简单的做法，来一个用户连上来就开一个新线程，专门接收这个人发来的请求，专门给这个人发他要的文件。这样的话，连的人多了，系统里就会有一堆这样的线程，这样的线程执行的套路大概就是执行了一段代码，比如要分析用户的请求数据了，这个时候就会调用例如 recv 这样的函数获取用户发来的数据，但是实际上此时用户没有数据发来，那么这个线程就变成了“没事干”的状态，系统就把这个线程从CPU拖出来、塞别的进去运行了，直到通过网络收到了用户发来的请求，才会变回就绪状态。</p>
<p>现在模拟一下线程这样的行为，为了方便计时，我忽略了网络传输所需的时间，就只模拟这个“被从CPU里拖出来”的动作。然后一个线程干的事情其实就两个：一个是执行代码，这里用求三个数的平均数模拟；另一个就是“被从CPU里拖出来”。拖出来用的是 std::this_thread::yield(); 这种方法。我要等所有线程都创建完了才开始执行和计时，所以用了Barrier：这是一个能让指定数量的线程同时到达这个“栅栏”之后才能继续执行的机制</p>
<pre>SYNCHRONIZATION_BARRIER barrier;
void ThreadFunc()
{
    EnterSynchronizationBarrier(&amp;barrier, 0);
    std::vector&lt;int&gt; input(256 * 1024);
    std::vector&lt;int&gt; output(256 * 1024);

    for (int rep = 0; rep &lt; 1000; ++rep)
    {
        for (size_t i = 0; i &lt; input.size() - 2; ++i)
            output[i] = (input[i] + input[i + 1] + input[i + 2]) / 3;
        std::this_thread::yield();
    }
}
</pre>
<p>然后我现在开200个这样的线程让它跑：</p>
<pre>int main()
{
    InitializeSynchronizationBarrier(&amp;barrier, 201, -1);
    std::vector&lt;std::thread&gt; threads(200);

    for (size_t i = 0; i &lt; threads.size(); ++i)
        threads[i] = std::thread(ThreadFunc);
    EnterSynchronizationBarrier(&amp;barrier, 0);
    DeleteSynchronizationBarrier(&amp;barrier);
    time_t beginTime = time(NULL);
    for (size_t i = 0; i &lt; threads.size(); ++i)
        threads[i].join();
    std::cout &lt;&lt; (time(NULL) - beginTime) &lt;&lt; std::endl;
}
</pre>
<p>加上所需的头文件，整个程序是这样。这个程序主要是演示线程切换会花多少时间。使用后面提到的IO多路复用技术并不完全是因为节省线程切换的开销的，这里先不谈其他的影响因素，一个个理解会简单一些。可以直接复制去编译自己运行试试</p>
<pre>#include &lt;Windows.h&gt;
#include &lt;thread&gt;
#include &lt;vector&gt;
#include &lt;iostream&gt;
#include &lt;time.h&gt;

SYNCHRONIZATION_BARRIER barrier;
void ThreadFunc()
{
    std::vector&lt;int&gt; input(256 * 1024);
    std::vector&lt;int&gt; output(256 * 1024);
    EnterSynchronizationBarrier(&amp;barrier, 0);
    for (int rep = 0; rep &lt; 1000; ++rep)
    {
        for (size_t i = 0; i &lt; input.size() - 2; ++i)
            output[i] = (input[i] + input[i + 1] + input[i + 2]) / 3;
        std::this_thread::yield();
    }
}

int main()
{
    InitializeSynchronizationBarrier(&amp;barrier, 201, -1);
    std::vector&lt;std::thread&gt; threads(200);

    for (size_t i = 0; i &lt; threads.size(); ++i)
        threads[i] = std::thread(ThreadFunc);
    EnterSynchronizationBarrier(&amp;barrier, 0);
    DeleteSynchronizationBarrier(&amp;barrier);
    time_t beginTime = time(NULL);
    for (size_t i = 0; i &lt; threads.size(); ++i)
        threads[i].join();
    std::cout &lt;&lt; (time(NULL) - beginTime) &lt;&lt; std::endl;
    return 0;
}
</pre>
<p>它能给出花的总时间。我这边运行以后，花费是 31。</p>
<h1>5. 减少线程开销</h1>
<p>以上程序代码用 std::this_thread::yield() 来模拟了“要从用户接收数据了，但是数据没来，所以让操作系统把当前线程从CPU抠出来，把CPU让给别人”这样的操作。这里有200个线程在执行，所以系统一直在这么些线程之间切换。我现在换一种方式，让同时运行的线程数正好一个CPU核心一个，这样在调用 yield 的时候，就会发生“系统发现没有其他线程要运行，所以继续让你用CPU”的情况，以此来模拟线程切换的开销小了的时候是什么样的。这里用一个“信号量”来控制同时运行的线程的数量，实际运行效果将是一个CPU核心上一个线程在跑，每次到 yield 的时候系统会看一圈有没有线程要用，没有要用所以就不切换。我直接给出整个程序代码了，和上面相比可以发现多了个 Semaphore（信号量） 而已。信号量是这样一种机制，你可以声明手头有多少个“资源”，这个资源拿走一个少一个，拿不到了就会在那边等；拿到的“人”用完了之后可以还回去，还回去直接就拿去给在那边等的“人”了。这样受到信号量的控制，比如我是4核CPU，那么200个线程里就只会有4个线程能进去执行算平均数和yield（模拟网络收发）这两个操作。那么yield的时候系统就会让你继续执行而没有切换，切换的开销大大减小了。</p>
<pre>#include &lt;Windows.h&gt;
#include &lt;thread&gt;
#include &lt;vector&gt;
#include &lt;iostream&gt;
#include &lt;time.h&gt;

SYNCHRONIZATION_BARRIER barrier;
HANDLE semaphore;
void ThreadFunc()
{
    std::vector&lt;int&gt; input(256 * 1024);
    std::vector&lt;int&gt; output(256 * 1024);
    EnterSynchronizationBarrier(&amp;barrier, 0);
    WaitForSingleObject(semaphore, INFINITE);
    for (int rep = 0; rep &lt; 1000; ++rep)
    {
        for (size_t i = 0; i &lt; input.size() - 2; ++i)
            output[i] = (input[i] + input[i + 1] + input[i + 2]) / 3;
        std::this_thread::yield();
    }
    ReleaseSemaphore(semaphore, 1, NULL);
}

int main()
{
    InitializeSynchronizationBarrier(&amp;barrier, 201, -1);
    semaphore = CreateSemaphore(0, std::thread::hardware_concurrency(), std::thread::hardware_concurrency(), 0);
    std::vector&lt;std::thread&gt; threads(200);

    for (size_t i = 0; i &lt; threads.size(); ++i)
        threads[i] = std::thread(ThreadFunc);
    EnterSynchronizationBarrier(&amp;barrier, 0);
    DeleteSynchronizationBarrier(&amp;barrier);
    time_t beginTime = time(NULL);
    for (size_t i = 0; i &lt; threads.size(); ++i)
        threads[i].join();
    std::cout &lt;&lt; (time(NULL) - beginTime) &lt;&lt; std::endl;
    CloseHandle(semaphore);
    return 0;
}
</pre>
<p>这个程序在我电脑上运行，最终输出是 21。</p>
<h1>6. IO多路复用</h1>
<p>从上面程序的对比，可以看出线程切换的开销小了之后，执行同样多的任务，性能要好不少，这个例子在我电脑上是差了差不多10秒钟。</p>
<p>那么回到刚才HTTP服务器的话题，HTTP服务器给一个连上来的用户开一个线程，网络又是一种很慢的输入输出（IO）方式，在调用 send 发送数据和调用 recv 接收数据的时候，会遇到发送缓冲区满了只能等、接收缓冲区空了只能等 这样的情况。因为你要等了，系统就会做线程切换，换到能运行的线程上。等你等到你要的东西了，再让你继续执行。效果就是和上面例子里前一个程序那样，切换线程花了很多时间。</p>
<p>那么 epoll、kqueue 是怎么一回事呢，你把网络通信那个“文件”设置成“非阻塞”模式之后，如果 send 遇到发送缓冲区满了、 recv 遇到接收缓冲区空了，它不会“等”，而是直接告诉你满了空了，这个时候你要做的事情是通过 epoll 或者 kqueue 机制，告诉系统“能发送数据、能接收数据了叫我”，因为线程没有在等，所以你可以接着干其他事，这里指的是处理其他用户的连接。等一波任务处理完之后，就回到“等 epoll / kqueue 叫”这样的阶段，epoll 和 kqueue 发现有用户可以发数据可以收数据了，就叫你，告诉你有多少用户可以收发了，然后你就接着处理这些用户——一切都是在同一个线程内执行，中间不进行切换。这样线程切换的开销就省掉了，就像上面第二个例子那样：yield 的时候不切换、继续让你执行。</p>
<p>还有一个“io完成端口”没说，其实它原理和 epoll、kqueue 差不多，只是它把顺序变了一下：不是可以 send 可以 recv 的时候叫你。调用 send、recv 的时候它不会告诉你缓冲区满了空了，而是确实去执行了，等到发完了、收完了再叫你，而不是可以发、可以收的时候叫你。这个层面上说其实没什么区别……</p>
<h1>7. ？？？</h1>
<p>这种“等系统叫你”的方式，会大大增加写出来的代码的复杂度。本来是 接收数据——处理数据 这样的流程，变成了 等系统叫你——接收数据——处理数据。而其中的“等系统叫你”当然不能呆在那边等，这样就和不用IO多路复用没什么两样了。到了“等系统叫你”这样的环节，你要把当前处理的代码 return 掉，然后去处理其他用户，然后等系统叫你的时候，又要重新回去执行对付这个用户的代码，还要回到刚才做一半的事情的状态里去，这个就很恶心：return都return了，这要怎么回。在这里通常是调用了一个新的函数，也就是一个处理代码，要被拆开成好几个不同的函数，就有了局部变量（“也就是状态”）在不同函数之间传递非常的麻烦。有个叫“协程”的技术可以用来解决这样的问题，但这不是这里要说的。协程可以模拟一种“伪return”，让代码的执行流程，可以有“从一个函数return”的效果；然后下次进来的时候能从这个“伪return”的地方继续执行，局部变量这样的状态就还在，写出来的代码也就和没用IO多路复用的时候差不多，大大降低了代码的复杂度。</p>
<p>这里提一下，为什么第二个示例程序不直接去掉 yield ，而是要执行 yield 然后由系统让你继续执行呢。因为对于 epoll、kqueue、iocp 来说，你告诉系统要等有“事件”的时候叫你，也是要时间要开销的，所以我就留在那边模拟这个开销了。而且你4核CPU跑你的程序的4个线程的时候，也不是一直四个线程都在上面的，操作系统还要执行其他程序对吧，比如桌面环境，比如后台的QQ，浏览器里网页上的动画还在动，或者你开在后面的音乐播放器……</p>
]]></content:encoded>
			<wfw:commentRss>http://blog.sorayuki.net/?feed=rss2&#038;p=640</wfw:commentRss>
		<slash:comments>1</slash:comments>
		</item>
		<item>
		<title>Windows动态链接库简介</title>
		<link>http://blog.sorayuki.net/?p=630</link>
		<comments>http://blog.sorayuki.net/?p=630#comments</comments>
		<pubDate>Sun, 04 Mar 2018 07:06:31 +0000</pubDate>
		<dc:creator><![CDATA[空雪梦见]]></dc:creator>
				<category><![CDATA[Windows编程]]></category>

		<guid isPermaLink="false">http://blog.sorayuki.net/?p=630</guid>
		<description><![CDATA[要说为什么写这篇，大概只是希望它能“有用”吧。 Windows下面可执行的代码通常会存在两种文件里面，一种是  &#8230; <a href="http://blog.sorayuki.net/?p=630" class="more-link">继续阅读<span class="screen-reader-text">Windows动态链接库简介</span> <span class="meta-nav">&#8594;</span></a>]]></description>
				<content:encoded><![CDATA[<p>要说为什么写这篇，大概只是希望它能“有用”吧。<span id="more-630"></span>
<p>Windows下面可执行的代码通常会存在两种文件里面，一种是 EXE 一种是 DLL。其中 EXE 大家都知道，双击就能运行。但是 DLL 双击的话，只会弹一些什么不知道如何打开文件啊、选择一个应用程序用来打开，这种。反正就是不让你开。</p>
<h1>1. 链接</h1>
<p>要说动态链接库的话，我觉得要从链接开始说起才能大概讲得清楚。那么先说链接，比如我有两个 C 语言的代码文件，一个叫 a.c 一个叫 b.c。完了在 a.c 里面只有这么一点内容：</p>
<pre>int add(int a, int b) { return a + b; }</pre>
<p>简单到爆了，就是给两个整数算和。然后在 b.c 里面调用它：</p>
<pre>#include &lt;stdio.h&gt;<br />int add(int a, int b);<br />int main() { printf("%d", add(1, 2)); return 0; }</pre>
<p>这样子的代码，编译链接之后执行，输出的结果是 3。……嗯，差不多是废话了。</p>
<p>现在我们来看看编译链接的过程。首先是编译， a.c 变成 a.obj，然后 b.c 变成 b.obj。也就是说，一个 .c 代码文件变成一个 .obj。这么说来，计算加法的那个代码，虽然在 b.c 里面调用，但是并不在 b.obj 里面，它在 a.obj 里面。<br />大概来研究一下 a.obj 里面到底是什么样的东西：这里要关注的，其实就里面的两个部分：一个部分叫“符号表”，a.c 里面的 add 函数，“add”这个名字就叫“符号”，编译成 obj 之后这个符号还保留着，链接的时候要用到；另一个部分叫“代码区”，add 函数里“把 a 和 b 拿出来加在一起又放到存返回值的地方”这一操作所对应的机器代码，就放在“代码区”里。然后符号表的“add”那边，就写了这个“add”对应的东西在代码区的哪里哪里。</p>
<p>然后再来看一下 b.obj 里面是什么。符号表里有 main，另外还多了个叫“重定位表”的东西，写明了有用到 printf 和 add，在哪里哪里用到，并没有说用到的东西在哪里（知道在哪里了还要链接器干嘛）。</p>
<p>好了现在我们拿到这么些东西，b.obj 里面写了“在这里这里用到 add”， a.obj 里写了“我这里有 add”。链接器看到了这两个，就把 a.obj 里面，代码区的 add 函数所在的位置，往 b.obj 里写了“我要add”的地方一填，就大功告成了。同理，printf 就是在标准库的符号表里面，找到 printf，然后把符号表的 printf 的那一栏里面标明的 printf 在代码区的哪里哪里，往 b.obj 里写了“我要printf”的地方一填，事情就差不多了。还有一步就是那个 main，程序运行起来的时候会先执行 C 语言标准库里的初始化代码，然后进 main 函数。至于 main 函数在哪里，标准库也不知道，于是标准库也就留了个地方，写上“我要main”，完了链接器把 main 在代码区里的位置朝这边一填，程序能运行了。</p>
<p><a href="http://blog.sorayuki.net/wp-content/uploads/2018/03/pic1.png"><img width="635" height="536" title="pic1" style="border: 0px currentcolor; border-image: none; display: inline; background-image: none;" alt="pic1" src="http://blog.sorayuki.net/wp-content/uploads/2018/03/pic1_thumb.png" border="0"></a></p>
<p>每个大格子里，最上面是文件名、库名；第二格是符号表，声明“我有什么”；第三格是代码区，放编译后的代码的；第四格是重定位表（可有可无），代表“我要什么”。这个是简化过的模型，实际场景中的 obj 文件和 库 文件要比这个复杂很多。</p>
<h1>2. 动态链接</h1>
<p>现在我们来改一改，刚才是 a.c 编译成 a.obj，然后 b.c 编译成 b.obj，最后 a.obj 和 b.obj 和标准库链接在一起，变成能跑的代码。现在改为， a.c 编译成 a.obj 然后链接成 a.dll， b.c 编译成 b.obj 链接成 b.exe，那么这其中会有什么变化……</p>
<p>根据上面的描述，要达到这样的效果，至少要有这么几个东西：一个是“我要什么”，一个是“我有什么”，然后才能搞起来。Windows系统确实是提供了这样的一套方式，但并不是上面 a.obj 的“我要什么”：你见过这么用的，是 DLL 而不是什么 OBJ 对吧。DLL 有 DLL 自己的“我有什么”的表示方式，这个就是“导出表”。导出表里面说白了也是上面说的符号表里的那些东西，但是和符号表不同：符号表的使用时机是链接的时候，这个时候有很多“内部符号”还留着的，这些“内部符号”在链接完成之后其实并没有什么用了。但是导出表就可以不必写这些东西：这个导出表又不是给链接器用的……</p>
<p>导出表存放在 exp 文件中。以刚才的 a.c 为例子，在 a.c 编译成 a.obj 之后，假如我要做的是一个 a.dll 文件，我有个 a.exp 文件里面是说了我要导出 add 函数，那就把 a.obj 和 a.exp 链接一下，生成 a.dll 就好了。画个图容易理解一些：</p>
<p><a href="http://blog.sorayuki.net/wp-content/uploads/2018/03/pic2.png"><img width="738" height="517" title="pic2" style="border: 0px currentcolor; border-image: none; display: inline; background-image: none;" alt="pic2" src="http://blog.sorayuki.net/wp-content/uploads/2018/03/pic2_thumb.png" border="0"></a></p>
<p>这个生成的 DLL 就包含了“我有什么”的部分了，有人要来找 add 的话，就能看到 a.dll 里有 add，还知道它在代码区的什么地方，要调用能调用了。</p>
<p>接下来还有一个，就是 b.obj 那边。现在 a.obj 变成了 a.dll， a.dll 又没有什么符号表之类的给你用，链接器没办法直接往 a.dll 上面链的：这一步是操作系统干的事情，其实不算链接器要干的事情。那么现在 b.obj 要做成 b.exe，需要有一种“DLL的仪式”，来声明“我要什么”。这个是“导入表”。导入表通常是 lib 文件，这种类型，写程序的人看到的应该比 exp 要多的多。导入表里其实也没什么太多东西，大致就是个“我要 xxx.dll 文件里的 xxx 函数，给我放到 xxx 全局变量里”。这个“全局变量”呢，就是个函数指针。这么讲有点抽象，我再画个图就简单多了：</p>
<p>
<a href="http://blog.sorayuki.net/wp-content/uploads/2018/03/pic3.png"><img width="455" height="467" title="pic3" style="border: 0px currentcolor; border-image: none; display: inline; background-image: none;" alt="pic3" src="http://blog.sorayuki.net/wp-content/uploads/2018/03/pic3_thumb.png" border="0"></a></p>
<p>那么这样的 lib 文件和 b.obj 链接的时候，就是这么一种景象：</p>
<p>
<a href="http://blog.sorayuki.net/wp-content/uploads/2018/03/pic4.png"><img width="1039" height="933" title="pic4" style="border: 0px currentcolor; border-image: none; display: inline; background-image: none;" alt="pic4" src="http://blog.sorayuki.net/wp-content/uploads/2018/03/pic4_thumb.png" border="0"></a></p>
<p>从这里面可以看到，它也有 b.obj 中重定位表需要找的“符号表”，里面有个 add。然后它也有代码区，里面确实是 add 的代码，不过是假的 add 的代码：它的功能就是把调用转发给 __imp_add 函数指针指向的函数。这个函数指针是属于导入表的一部分，导入表写明了这个函数指针要指向的是“a.dll 文件里的 add 函数”。这样 Windows 在启动 b.exe 文件的时候，就会看到它导入表里面写了“我要 a.dll 文件里的 add 函数”，Windows 就会加载 a.dll 文件，加载了之后把 a.dll 里导出表里写的 add 函数所在的地址，填入 __imp_add 全局变量。这样在 b.exe 执行到这个函数的时候，就会调用 a.dll 里的 add 了。</p>
<p>根据实际情况，有的时候也会跳过 先调用 add 再调用 __imp_add 函数指针 这样两步的步骤，而是简化为一步、直接去调用 __imp_add 函数指针。比如在声明 int add(int a, int b) 的时候加上 __declspec(dllimport) 这样的声明，生成的代码就会直接调用 __imp_add 函数指针。</p>
<h1>3. 使用方法</h1>
<p>说这么多，那么实际操作的时候应该是什么样的呢……我就以上面为例子，说说最简单的情况。</p>
<p>首先要介绍 def 文件，这种文件定义了导入导出的 DLL 叫什么名字，以及函数名是什么。基本语法非常简单，上面的情况就是：</p>
<pre>LIBRARY a.dll
EXPORTS
add</pre>
<p>第一行是 LIBRARY 后面加上 DLL 的文件名，第二行写 EXPORTS 单独一行，后面就都是函数名了，一个一行。比方说除了 add 还有一个 sub 函数，那就在下面加一行 sub 就行了。</p>
<p>之所以要介绍这种文件，是因为生成上面提到的 exp 文件和生成 lib 文件都要用到它。</p>
<h2>1. 在 MSVC 里使用</h2>
<p>不同编译器生成的方法不同，对于微软的 Visual Studio 来说，在命令行下面生成的方式是</p>
<pre>&gt;link /lib /out:a.lib /def:a.def
Microsoft (R) Library Manager Version 9.00.30729.207
Copyright (C) Microsoft Corporation.  All rights reserved.

LINK : warning LNK4068: /MACHINE not specified; defaulting to X86
   Creating library a.lib and object a.exp</pre>
<p>第一行是命令，后面几行是输出。可以看到它同时生成了 exp 文件和 lib 文件。输出的时候有个提示说没有指定机器类型默认给你选 x86 了。这个地方可以用 /MACHINE 来指定你要做 32 位的 exp/lib 文件还是 64 位的 exp/lib 文件。然后拿着这个 exp 文件就可以生成 DLL 了，同样是命令行：</p>
<pre>&gt;cl /nologo /c a.c
a.c

&gt;link /nologo /dll /out:a.dll a.exp a.obj

&gt;
</pre>
<p>这里把 a.c 编译成了 a.obj 然后再和 exp 文件链接在一起，生成了 a.dll。为了验证确实导出了 add 函数，可以用 CFF Explorer 工具打开这个 DLL，切换到 Export Directory 视图。</p>
<p>然后对于上面说的 b.c，这么编译链接就能运行了：</p>
<pre>&gt;cl /nologo /c b.c
b.c

&gt;link /nologo /out:b.exe b.obj a.lib

&gt;b.exe
3
&gt;</pre>
<h2>2. 在 Visual Studio 里使用</h2>
<p>用图形化开发环境的来做这种东西确实是容易。在 Visual Studio 里面，这个例子在同一个解决方案里给 a.dll 和 b.exe 分别创建工程 a 和工程 b 就行。假设两个创建的时候都选了“空工程”，那么要设置的地方，一个是右键点工程 a 选属性，弹出来的“属性页”里在“配置属性——常规——项目默认值——配置类型”那边，改成“动态库”，然后到“配置属性——链接器——输入——模块定义文件”，输入 def 文件的文件名。def 文件和源代码文件放在同一个目录下，为了方便编辑也可以把它添加到工程中。这样在构建工程 a 的时候，就会同时生成 lib 文件和 exp 文件了。</p>
<p>对于工程 b 来说，因为和工程 a 是同属于同一个解决方案，所以直接在工程 b 的“引用”那一栏添加对工程 a 的引用就行了。如果不是同属于一个解决方案，那么同样是右键点工程 b 选属性，弹出来的“属性页”里在“配置属性——链接器——输入——附加依赖项”那边添加 a.lib 即可。</p>
<h2>3. 在 MinGW-GCC 里使用</h2>
<p>Windows 下开发，如果是做开源项目，那么用到 GCC 的概率还是蛮大的。在 MinGW 里，也有一套工具用来搞这个 DLL 的。也还是这么几个套路：生成 .exp 和生成 .lib，然后用什么命令编译链接。<br />生成 exp 和 lib 文件用的工具是 dlltool，命令行非常直白，看得懂英语的话，就是指定输入 def 文件是啥输出 lib 文件是啥输出 exp 文件是啥。</p>
<pre>&gt; dlltool --input-def=a.def --output-exp=a.exp --output-lib=a.lib</pre>
<p>这货很高冷，啥也不输出（</p>
<p>得到 def 文件和 exp 文件之后，可以编译链接 a.c 和 b.c 了。命令行如下：</p>
<pre>&gt; gcc -c a.c

&gt; gcc -shared -o a.dll a.exp a.o

&gt; gcc -c b.c

&gt; gcc -o b.exe b.o a.lib

&gt; b.exe
3
&gt;</pre>
]]></content:encoded>
			<wfw:commentRss>http://blog.sorayuki.net/?feed=rss2&#038;p=630</wfw:commentRss>
		<slash:comments>2</slash:comments>
		</item>
		<item>
		<title>在OBS直播软件中使用FDK-AAC编码器</title>
		<link>http://blog.sorayuki.net/?p=626</link>
		<comments>http://blog.sorayuki.net/?p=626#comments</comments>
		<pubDate>Wed, 31 Jan 2018 16:10:19 +0000</pubDate>
		<dc:creator><![CDATA[空雪梦见]]></dc:creator>
				<category><![CDATA[程序设计]]></category>
		<category><![CDATA[计算机应用]]></category>

		<guid isPermaLink="false">http://blog.sorayuki.net/?p=626</guid>
		<description><![CDATA[现在在直播中用的最多的软件可能就是OBS了。这个免费开源（https://github.com/jp9000/ &#8230; <a href="http://blog.sorayuki.net/?p=626" class="more-link">继续阅读<span class="screen-reader-text">在OBS直播软件中使用FDK-AAC编码器</span> <span class="meta-nav">&#8594;</span></a>]]></description>
				<content:encoded><![CDATA[<p>现在在直播中用的最多的软件可能就是OBS了。这个免费开源（<a title="https://github.com/jp9000/obs-studio" href="https://github.com/jp9000/obs-studio">https://github.com/jp9000/obs-studio</a>）的直播软件功能很强大，也没有烦人的小广告，不要注册不要登录，所有基于RTMP的直播平台都能用。</p>
<p>这个直播软件在编码直播音频流的过程中，支持使用很多种不同的AAC编码器。它大概会按照 Core-audio（苹果iTunes里的那个）、FDK-AAC、Windows Media Foundation、FFMpeg这样的顺序一个个找下去找到能用的。虽然是这个顺序，但是实际上在正常安装的OBS里面，是找不到 obs-libfdk.dll 这样一个文件的。安装的时候就不带这个文件。其实我之前也比较纳闷，这个如果效果这么好，那么为什么网上比较少见到哪里直接下载这个编码器的EXE文件呢。下午在一个群里提了这样的疑问，结果收到的回答大概是FDK-AAC发布源代码可以，但是因为其中涉及到一些什么专利许可证的问题，发布EXE、DLL这样的二进制文件好像是不行的。但是这些编码器里面，FDK-AAC在低码率HE-AAC v2模式下是公认的效果比较好。有的时候如果想拿来玩玩看用用看试试效果的话，就要自己从源代码编译了。</p>
<p><span id="more-626"></span></p>
<p>&nbsp;</p>
<p>对于从源代码编译这种事，有的开源软件搞起来连程序员都会感到头疼，更不要说一般主播了。值得庆幸的是搞这个OBS的FDK-AAC插件并不麻烦，OBS那边也只要编译这个插件就行了，不用编译整个项目，甚至它用的CMake软件都可以不装。</p>
<p>代码主要涉及到要下载的源代码也就obs-studio和fdk-aac这两套。obs-studio到obs的github版本发布页面（<a title="https://github.com/jp9000/obs-studio/releases" href="https://github.com/jp9000/obs-studio/releases">https://github.com/jp9000/obs-studio/releases</a>）找到我安装的那个版本的源代码包，下载下来就行。对于fdk-aac，也是到github上的版本发布页面（<a title="https://github.com/mstorsjo/fdk-aac/releases" href="https://github.com/mstorsjo/fdk-aac/releases">https://github.com/mstorsjo/fdk-aac/releases</a>）下载。我下载下来是obs-studio-21.0.1.zip和fdk-aac-0.1.5.tar.gz两个文件。代码有了以后然后就是编译器。我考虑到很多主播又不是程序员，也没必要装特别硕大的Visual Studio这样的东西，于是选了相对较小的TDM-GCC编译器：它下载来的安装包才几十兆。</p>
<p>接下来遇到的问题是编译FDK-AAC。它的代码解压出来以后发现，没有给MinGW用的Makefile……虽然有configure那一套脚本，但是那还要装unix的sh环境，麻烦。我选择的方法是把它做给VC用的那个Makefile.vc拿来改改，改成Makefile.tdm。然后要对它进行修改，首先我们用的编译器不是VC，所以可执行文件的名字也就不是 cl 和 link、lib这种。因为这边只要编译成静态库、在链接obs插件的时候用就行了，所以link可以丢那边不管，直接把 AR = lib 改成 AR = ar 即可。然后是下面那个 CFLAGS，用的完全是VC的那一套参数。改成gcc用的就好了，我是这么改的：</p>
<pre>改之前：
CFLAGS   = /nologo /W3 /Ox /MT /EHsc /Dinline=__inline $(TARGET_FLAGS) $(AM_CPPFLAGS) $(XCFLAGS)
改之后：
CFLAGS   =  -O2 -Dinline=__inline $(TARGET_FLAGS) $(AM_CPPFLAGS) $(XCFLAGS)</pre>
<p>总之就是往简单了去。然后还有生成静态库时候的命令，找到 $(STATIC_LIB): $(FDK_OBJS) 这边，下面的命令我直接改成</p>
<pre>	$(AR) $(ARFLAGS) qf $@ $(FDK_OBJS)
	$(AR) $(ARFLAGS) s $@</pre>
<p>这样了。</p>
<p>还不算完成，因为nmake用的默认编译规则的语法和mingw32-make的不一样，所以还有一个地方要改。找到 .cpp.obj: 这个规则，改成 %.obj : %.cpp。这样就可以了。</p>
<p>然后拿到这样的Makefile.tdm之后，在TDM-GCC环境里面，用命令行 mingw32-make -f Makefile.tdm fdk-aac.lib，它能给你出个静态库出来。这一步就算完成了。</p>
<p>下一步是编译OBS的插件了。这个OBS插件用的FDK-AAC的头文件是把FDK里面那些所有include文件夹里的头文件全部放在一起的。所以在编译之前有个步骤要做，就是把头文件都放一起。方法是先新建一个文件夹比如叫 fdkinc，然后在里面创建 fdk-aac 文件夹，用一个for命令把头文件搞过来：</p>
<p>for /f %%a in ('dir include /s /b /ad') do copy /y "%%~a\*" ..\fdkinc\fdk-aac</p>
<p>于是现在环境够了，可以开始编译插件那个DLL文件了。</p>
<p>编译插件的时候也有个地方其实是缺的，那就是连接到 obs.dll 用的导入库。这个导入库正常来说，是编译obs的过程中生成的。现在我们没有，要手工做一个。TDM-GCC里有给你做这个的工具，大体上来说就是把插件中要用到的 obs.dll 里面的函数都列出来，写成一个 def 文件，比如这次用的是这样的：</p>
<pre>LIBRARY obs.dll
EXPORTS 
obs_data_set_default_int 
blog 
bfree 
obs_properties_create 
obs_properties_add_int 
obs_properties_add_bool 
obs_data_get_int 
obs_data_get_bool 
obs_encoder_audio 
bmalloc 
audio_output_get_channels 
audio_output_get_sample_rate 
obs_register_encoder_s 
text_lookup_getstr 
text_lookup_destroy 
obs_module_load_locale 
obs_data_set_default_bool 
</pre>
<p>保存成 obs.def，用dlltool命令：</p>
<p>dlltool --input-def=obs.def --output-lib=obs.lib</p>
<p>就出来lib了。现在可以编译链接插件了，进到obs-studio-21.0.1\plugins\obs-libfdk文件夹，一行命令：</p>
<p>gcc -shared -oobs-libfdk.dll -O2 -I"../../libobs" -I"../../../fdkinc" obs-libfdk.c "../../../fdk-aac-0.1.5/fdk-aac.lib" "../../../obs.lib"</p>
<p>这样就可以获得一个 obs-libfdk.dll 了。这个文件丢到 obs 的 plugins 文件夹对应的地方（32bit就放32bit，64bit就放64bit，看用的是什么版本的TDM-GCC编译器）。</p>
<p>这整个过程我写了个bat文件来完成。源代码文件放进来，然后tdm-gcc装到 tdm64 文件夹，运行bat就能生成DLL。但是我没做clean，所以32编完之后想要64的话要删掉全部文件重新来一次，64编完想要32的也是一样。</p>
<p>附：在 obs-libfdk.c 文件里，找到 //MPEG-4 AAC-LC 注释，前面有个数字是2，改成29就可以用 HE-AAC v2了。用5是没有v2的HE。</p>
<p><a href="http://blog.sorayuki.net/wp-content/uploads/2018/02/build-obs-libfdk.7z">点击下载： build-obs-libfdk</a></p>
]]></content:encoded>
			<wfw:commentRss>http://blog.sorayuki.net/?feed=rss2&#038;p=626</wfw:commentRss>
		<slash:comments>2</slash:comments>
		</item>
		<item>
		<title>Windows下控制子进程的句柄继承</title>
		<link>http://blog.sorayuki.net/?p=625</link>
		<comments>http://blog.sorayuki.net/?p=625#comments</comments>
		<pubDate>Wed, 03 Jan 2018 15:00:20 +0000</pubDate>
		<dc:creator><![CDATA[空雪梦见]]></dc:creator>
				<category><![CDATA[Windows编程]]></category>

		<guid isPermaLink="false">http://blog.sorayuki.net/?p=625</guid>
		<description><![CDATA[相信用 Windows API 写过重定向子进程的标准输入输出的人都弄过这东西。大概过程是不难，不过对初学者坑 &#8230; <a href="http://blog.sorayuki.net/?p=625" class="more-link">继续阅读<span class="screen-reader-text">Windows下控制子进程的句柄继承</span> <span class="meta-nav">&#8594;</span></a>]]></description>
				<content:encoded><![CDATA[<p>相信用 Windows API 写过重定向子进程的标准输入输出的人都弄过这东西。大概过程是不难，不过对初学者坑很多。几年前我写了个文大概说这个的 <a title="http://blog.sorayuki.net/?p=85" href="http://blog.sorayuki.net/?p=85">http://blog.sorayuki.net/?p=85</a> ，现在要说的也是这个话题，不过是另外的事。</p>
<p>之前的经验是这样的，比如我有一个进程，这个进程要开个子进程、并且发数据给子进程。那么做法是创建一对匿名管道，把管道读的那一端拿去给子进程，写的这一端自己留着。容易出错的地方在这个“自己拿着”和“拿去给子进程”。拿去给子进程可以理解为，这个句柄要设置成可继承的，子进程开启以后就带着这个句柄了，那么自己进程拿着的就可以关掉（CloseHandle）了；自己拿着，那么字面意思就是不继承给子进程了。API函数CreatePipe有个 SECURITY_ATTRIBUTES 参数，这个结构体里是可以设置创建的管道句柄能不能继承的，要么两个都能继承要么两个都不能继承。但是现在要的是写的那个不继承、读的那个继承。做法不止一种，不过思路是这么两个，一个是创建的时候两个都不能继承，然后把其中一个改成可以；另一个是创建的时候两个都能继承，然后把其中一个改成不行。改继承属性可以用 SetHandleInformation 也可以用 DuplicateHandle 复制一个能/不能继承的新句柄出来、随后把原来的关掉。我现在是用 SetHandleInformation 的方法，DuplicateHandle 的方法已经忘记是在哪里看的了，现在也不知道怎么比较好坏和必要性。</p>
<p>复杂一点的场景，是我要起两个进程，第一个进程的输出给第二个进程的输入。麻烦一点的办法可以两个都和自己通信、自己在中间做转发；简单的办法可以直接创建一对管道，一个子进程分一个、自己不留。现在这里要说的是后者，复杂性一个表现在管道是自己创建的，但是自己在起了子进程之后一个都不留；另一个表现在要刚好一个进程继承一个，如果有一个进程继承了两个，比如写的那一端一不小心两个进程都继承了，那么很不幸这一对进程永远跑不完了：写入数据的那个子进程写完了关掉了这个写入端，但是由于读取数据的那个子进程也持有这个写端，自己有自己都不知道，也就是说这个写端没法全部被关闭，那么读的那一端就永远在等待了。</p>
<p>虽然上面那个例子只要搞清楚关系、小心操作的话，也没有那么大问题。不过麻烦的事情在，假设我现在是一个有好几个线程在同时执行的程序，在其中一个线程A里，要调用 CreateProcess 的时候，它不知道其他线程比如B有没有创建什么会被继承的句柄，假设这个时候线程B也在调用 CreateProcess，那么线程B创建的这个子进程会把线程A创建的那些句柄也一起继承走。然后问题呢就会出在刚才说的那个场景上，比如一对管道，读取端永远阻塞因为写入端没有全部被关闭。也许会觉得这样的场景只要在要创建子进程的时候锁个整个进程范围的锁就好了，但如果这里面调用了什么第三方库，或者是工程上好多人同时开发一个项目，有些事情就不一定有那么可控了。而且这种Bug还可能在极偶然的情况下才会遇到那么一两次，调试的时候难以重现十分痛苦……啊别说了，害怕。</p>
<p><span id="more-625"></span>
<p>那么在 CreateProcess 的时候有没有什么更可靠的方法，比如不光是给每个句柄设置什么标志能不能继承，还能在调用 CreateProcess 的时候通过什么参数来显式指定我要这个、这个、这个和那个句柄？其实是有的，我以前没用过，最近看了一个博客之后才知道有这样的玩法。博客在这里：</p>
<p><a title="https://blogs.msdn.microsoft.com/oldnewthing/20111216-00/?p=8873/" href="https://blogs.msdn.microsoft.com/oldnewthing/20111216-00/?p=8873/">https://blogs.msdn.microsoft.com/oldnewthing/20111216-00/?p=8873/</a></p>
<p>其中很关键的一个示例函数的代码是</p>
<pre>BOOL CreateProcessWithExplicitHandles(
  __in_opt     LPCTSTR lpApplicationName,
  __inout_opt  LPTSTR lpCommandLine,
  __in_opt     LPSECURITY_ATTRIBUTES lpProcessAttributes,
  __in_opt     LPSECURITY_ATTRIBUTES lpThreadAttributes,
  __in         BOOL bInheritHandles,
  __in         DWORD dwCreationFlags,
  __in_opt     LPVOID lpEnvironment,
  __in_opt     LPCTSTR lpCurrentDirectory,
  __in         LPSTARTUPINFO lpStartupInfo,
  __out        LPPROCESS_INFORMATION lpProcessInformation,
    // here is the new stuff
  __in         DWORD cHandlesToInherit,
  __in_ecount(cHandlesToInherit) HANDLE *rgHandlesToInherit)
{
 BOOL fSuccess;
 BOOL fInitialized = FALSE;
 SIZE_T size = 0;
 LPPROC_THREAD_ATTRIBUTE_LIST lpAttributeList = NULL;

 fSuccess = cHandlesToInherit &lt; 0xFFFFFFFF / sizeof(HANDLE) &amp;&amp;
            lpStartupInfo-&gt;cb == sizeof(*lpStartupInfo);
 if (!fSuccess) {
  SetLastError(ERROR_INVALID_PARAMETER);
 }
 if (fSuccess) {
  fSuccess = InitializeProcThreadAttributeList(NULL, 1, 0, &amp;size) ||
             GetLastError() == ERROR_INSUFFICIENT_BUFFER;
 }
 if (fSuccess) {
  lpAttributeList = reinterpret_cast&lt;LPPROC_THREAD_ATTRIBUTE_LIST&gt;
                                (HeapAlloc(GetProcessHeap(), 0, size));
  fSuccess = lpAttributeList != NULL;
 }
 if (fSuccess) {
  fSuccess = InitializeProcThreadAttributeList(lpAttributeList,
                    1, 0, &amp;size);
 }
 if (fSuccess) {
  fInitialized = TRUE;
  fSuccess = UpdateProcThreadAttribute(lpAttributeList,
                    0, PROC_THREAD_ATTRIBUTE_HANDLE_LIST,
                    rgHandlesToInherit,
                    cHandlesToInherit * sizeof(HANDLE), NULL, NULL);
 }
 if (fSuccess) {
  STARTUPINFOEX info;
  <a href="http://blogs.msdn.com/b/oldnewthing/archive/2005/06/28/433341.aspx">ZeroMemory</a>(&amp;info, sizeof(info));
  info.StartupInfo = *lpStartupInfo;
  info.StartupInfo.cb = sizeof(info);
  info.lpAttributeList = lpAttributeList;
  fSuccess = CreateProcess(lpApplicationName,
                           lpCommandLine,
                           lpProcessAttributes,
                           lpThreadAttributes,
                           bInheritHandles,
                           dwCreationFlags | EXTENDED_STARTUPINFO_PRESENT,
                           lpEnvironment,
                           lpCurrentDirectory,
                           &amp;info.StartupInfo,
                           lpProcessInformation);
 }
 
 if (fInitialized) DeleteProcThreadAttributeList(lpAttributeList);
 if (lpAttributeList) HeapFree(GetProcessHeap(), 0, lpAttributeList);
 return fSuccess;
}</pre>
<p></p>
<p>我自己在用这个方法的时候倒是没有直接抄代码，里面有些地方被我换成更简单的代码。不过核心思想是，用 STARTUPINFOEX 来代替 STARTUPINFO，这样在那个带 EX 后缀的结构体里有个 lpAttributeList 可以用，里面可以塞一些比如我要继承那些句柄这样的信息。那么在你给的列表之外的句柄，就不会被子进程继承。</p>
<p>这样回到刚才的例子，我要开两个进程比如A和B，把A的标准输出连到B的标准输入上，就可以创建一对管道，反正都设成继承、也不用一个继承一个不继承了。然后，创建子进程的时候一人塞一个，用这种方式指定只继承其中一个、不继承另外一个，两个进程都开好了就把自己手上的全关了。剩下的就是等待子进程运行完成了。</p>
<p>在实验过程中我还发现有这样一个坑（？），Windows的那几个标准输入输出标准错误的句柄，就是用 GetStdHandles 获得的那几个，虽然可以用 DuplicateHandle 来复制，但是这复制出来的却不能继承。如果尝试在继承句柄列表里塞上这样复制出来的 Stdin、Stdout 或者 Stderr 句柄，CreateProcess 会调用失败，然后给你一个 1450（ERROR_NO_SYSTEM_RESOURCES） 错误。还有就是，虽然指定了要继承的句柄的列表，CreateProcess 的那个 bInheritHandles 函数还是要设置成 TRUE 的，不然你列表里指定的那些句柄它继承不下去。以及在要继承的句柄列表里指定的那些句柄，也必须是可继承的，如果有不可继承的在里面，CreateProcess 会报 87（ERROR_INVALID_PARAMETER） 错误。</p>
<p>我这边就拿一个程序就以用lame重编码MP3文件为例的sample吧。代码不一定就没问题，只演示一下用法</p>
<pre>#include &lt;windows.h&gt;
#include &lt;exception&gt;
#include &lt;string&gt;
#include &lt;vector&gt;
#include &lt;functional&gt;

#include &lt;atlbase.h&gt;

class OnExit
{
    std::function&lt;void()&gt; onExit_;
public:
    template&lt;class T&gt; OnExit(const T&amp; lambda) : onExit_(lambda) {}
    ~OnExit() { onExit_(); }
};

class ReEncoderErr : public std::exception 
{
    std::string msg_;
public:
    ReEncoderErr(const char* msg) : msg_(msg) {}
    const char* what() const { return msg_.c_str(); }
};

class ReEncoder
{
    CHandle hProcDec_, hProcEnc_;
    CHandle hPipeRead_, hPipeWrite_;

    void ClosePipePair()
    {
        hPipeRead_.Close();
        hPipeWrite_.Close();
    }

    void CreatePipePair()
    {
        ClosePipePair();

        HANDLE hPipeRead, hPipeWrite;
        SECURITY_ATTRIBUTES sa{};
        sa.bInheritHandle = TRUE;
        if (!CreatePipe(&amp;hPipeRead, &amp;hPipeWrite, &amp;sa, 0))
            throw ReEncoderErr("Fail to create pipe");

        hPipeRead_.Attach(hPipeRead);
        hPipeWrite_.Attach(hPipeWrite);
    }

    void RunLame(bool isEncoder)
    {
        wchar_t decCmdLine[] = L"lame.exe --decode input.mp3 -";
        wchar_t encCmdLine[] = L"lame.exe --preset medium - output.mp3";
        wchar_t* cmdLine;

        STARTUPINFOEXW si{};
        PROCESS_INFORMATION pi{};

        std::vector&lt;HANDLE&gt; handlesToInherit;

        si.StartupInfo.cb = sizeof(si);
        si.StartupInfo.dwFlags = STARTF_USESTDHANDLES;

        HANDLE hStdErr = GetStdHandle(STD_ERROR_HANDLE);

        si.StartupInfo.hStdError = hStdErr;

        if (isEncoder)
        {
            si.StartupInfo.hStdInput = hPipeRead_;
            handlesToInherit.push_back(hPipeRead_);
            cmdLine = encCmdLine;
        }
        else
        {
            si.StartupInfo.hStdOutput = hPipeWrite_;
            handlesToInherit.push_back(hPipeWrite_);
            cmdLine = decCmdLine;
        }

        SIZE_T nProcThreadAttrSize = 0;
        InitializeProcThreadAttributeList(0, 1, 0, &amp;nProcThreadAttrSize);
        if (nProcThreadAttrSize == 0)
            throw ReEncoderErr("Fail to get size of procthreadattr");

        std::vector&lt;char&gt; buf(nProcThreadAttrSize);
        si.lpAttributeList = reinterpret_cast&lt;LPPROC_THREAD_ATTRIBUTE_LIST&gt;(buf.data());
        if (InitializeProcThreadAttributeList(si.lpAttributeList, 1, 0, &amp;nProcThreadAttrSize) == FALSE)
            throw ReEncoderErr("Fail to init procthreadattr");

        OnExit clean([&amp;]() { DeleteProcThreadAttributeList(si.lpAttributeList); });

        if (UpdateProcThreadAttribute(si.lpAttributeList, 0, PROC_THREAD_ATTRIBUTE_HANDLE_LIST, handlesToInherit.data(), handlesToInherit.size() * sizeof(HANDLE), NULL, NULL) == FALSE)
            throw ReEncoderErr("Fail to update procthreadattr");
        
        if (!CreateProcessW(NULL, cmdLine, NULL, NULL, TRUE, EXTENDED_STARTUPINFO_PRESENT, NULL, NULL, &amp;si.StartupInfo, &amp;pi))
            throw ReEncoderErr("Fail to create process");

        CloseHandle(pi.hThread);

        if (isEncoder)
            hProcEnc_.Attach(pi.hProcess);
        else
            hProcDec_.Attach(pi.hProcess);
    }

public:
    void Run()
    {
        CreatePipePair();
        RunLame(false);
        RunLame(true);
        ClosePipePair();

        HANDLE handles[2] = { hProcEnc_.m_h, hProcDec_.m_h };
        WaitForMultipleObjects(2, handles, TRUE, INFINITE);
    }
};

#include &lt;iostream&gt;

int main(int argc, char* argv[])
{
    try 
    {
        ReEncoder reenc;
        reenc.Run();
        return 0;
    }
    catch (const std::exception&amp; exception)
    {
        std::cerr &lt;&lt; exception.what() &lt;&lt; std::endl;
        return 1;
    }
}
</pre>
]]></content:encoded>
			<wfw:commentRss>http://blog.sorayuki.net/?feed=rss2&#038;p=625</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>FFMS2读取多媒体文件中的声音的Bug</title>
		<link>http://blog.sorayuki.net/?p=623</link>
		<comments>http://blog.sorayuki.net/?p=623#comments</comments>
		<pubDate>Mon, 28 Aug 2017 15:10:08 +0000</pubDate>
		<dc:creator><![CDATA[空雪梦见]]></dc:creator>
				<category><![CDATA[多媒体]]></category>
		<category><![CDATA[程序设计]]></category>
		<category><![CDATA[问题解决]]></category>

		<guid isPermaLink="false">http://blog.sorayuki.net/?p=623</guid>
		<description><![CDATA[FFMS2是AviSynth的一个来源插件，以FFMPEG为支撑，能读入各种各样的多媒体文件。但是今天在使用它 &#8230; <a href="http://blog.sorayuki.net/?p=623" class="more-link">继续阅读<span class="screen-reader-text">FFMS2读取多媒体文件中的声音的Bug</span> <span class="meta-nav">&#8594;</span></a>]]></description>
				<content:encoded><![CDATA[<p>FFMS2是AviSynth的一个来源插件，以FFMPEG为支撑，能读入各种各样的多媒体文件。但是今天在使用它的时候，遇到了Bug。</p>
<p><span id="more-623"></span>
<p>这个Bug的具体表现是，如果我有一个MP4视频文件，我用某些切割工具对文件进行过切割，因为切割的时候不一定是在关键帧处切割的，而切割过程中又没有重新进行编码，有的工具在这样切割之后生成的文件，音频和视频的第一帧（关键帧）的时间戳都是小于0。小于一点点貌似没事，我遇到Bug的时候，这个时间戳我记得是负的4秒多。</p>
<p>对于这样的文件，我再用FFMS2在AviSynth里载入（记得要包含声音），载入之后用类似 last+last 这样的语句把它们连接起来。这样的AviSynth脚本送到播放器里面播放的话，到了两个片段的连接处，就会发现播放器死了。不是概率性出现而是每次必现。</p>
<p>我从GitHub下载了FFMS2的源代码（<a title="https://github.com/FFMS/ffms2" href="https://github.com/FFMS/ffms2">https://github.com/FFMS/ffms2</a>），在MSYS2环境中执行它那个 build-win-deps.sh 下载 ffmpeg 和编译（因为opencore-amr我用不到，为了节省时间，我就把它注释和屏蔽掉了）。之后用 VisualStudio 打开它那个工程，编译了FFMS2.dll。不论是Debug版本还是Release版本，自己编译的也还是会出现同样的问题。</p>
<p>但是因为是自己编译的，所以用 Debug 版本调试起来就很方便。在卡死的时候中断程序运行，会发现卡死的代码在 audiosource.cpp 的 FFMS_AudioSource::GetAudio 函数里。它不断在一个循环里打转，也不进什么函数，也不出去。很是郁闷。</p>
<p>我重新运行了程序，试图在它将要变成这个样子的临界点用条件断点（判断 Start 变量的值）断了下来。跟踪跟进去，发现和正常的时候相比，此时有个地方不太一样：它在正常的时候，是在数据不够的时候进 DecodeNextBlock 函数，然后 avcodec_decode_audio4 解码一个包，把解码结果 cache 起来。但是在我刚才所说的情况中，因为进了下一个片段，它会先 Seek 当前进度到文件开头，然后再调用 DecodeNextBlock。我观察到的结果是，它还没 Seek 的时候，avcodec_decode_audio4 函数运行的都没什么问题；但是它对这样的文件进行 Seek 之后，读取出来的 Packet 就再也没办法在 avcodec_decode_audio4 里解码后，通过 GotFrame 参数返回 1 了。</p>
<p>也就是说，经过这样跟踪下来，可以认为问题出在 FFMPEG 上，或者调用 FFMPEG 的方式有问题。</p>
<p>我是先考虑了 FFMPEG 可能有问题。因为 build-win-deps.sh 脚本是直接拉的 FFMPEG （git://source.ffmpeg.org/ffmpeg.git） 的 master 分支的代码。我尝试把它替换成去链接官网下载的带正式版本号的 FFMPEG （<a title="http://ffmpeg.zeranoe.com/builds/" href="http://ffmpeg.zeranoe.com/builds/">http://ffmpeg.zeranoe.com/builds/</a>） 的 DLL。具体方法是 build-win-deps.sh 脚本执行完成之后会产生一个 deps 文件夹，在里面可以找到一堆 libav*********.a 这样的文件。因为编译器用的是VC，所以这些 .a 文件其实是 .lib 文件。把它换成官网下的 FFMPEG 的 dev 压缩包里解压出来的文件名差不多的 lib 文件，然后再编译FFMS2工程，能得到一个比较小的 FFMS2.DLL （因为是动态链接到 FFMPEG）。配上 FFMPEG 官网下的 shared 那个压缩包里解压出来的 DLL 文件进行测试，会卡死的问题消失了。因此我判断是 FFMS2 拉的 FFMPEG 的 master 分支可能有什么问题，或者静态链接情况下会有什么问题、在动态链接的时候会消失的。</p>
<p>这样编译出来的 FFMS2 还有一个问题，它在 FFMS2_Init 函数里面，会对 FFMPEG 设置几个回调函数去拦截日志输出。如果用动态链接版的 FFMPEG 对 Avisynth 脚本进行转码，此时 FFMS2 和 FFMPEG.EXE 会用的同一套 FFMPEG 的DLL文件（AV****.DLL 那些）。这样在 Avisynth 加载 FFMS2 插件的时候，这个回调函数的设置会洗掉 FFMPEG.EXE 设置的回调，导致你转码过程中看不到任何进度信息。因为大概看了一下，FFMS2 也并没有想把 FFMPEG 的日志拿来干嘛用，所以我就直接把 FFMS2_Init 里几个注册日志回调的代码给注释掉了。再重新构建 FFMS2.DLL，然后拿一个用了 FFMS2 的 Avisynth 脚本去用 FFMPEG.EXE 转码，没有问题了。</p>
]]></content:encoded>
			<wfw:commentRss>http://blog.sorayuki.net/?feed=rss2&#038;p=623</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>FFMS2读取多媒体文件中的声音的Bug</title>
		<link>http://blog.sorayuki.net/?p=623</link>
		<comments>http://blog.sorayuki.net/?p=623#comments</comments>
		<pubDate>Mon, 28 Aug 2017 15:10:08 +0000</pubDate>
		<dc:creator><![CDATA[空雪梦见]]></dc:creator>
				<category><![CDATA[多媒体]]></category>
		<category><![CDATA[程序设计]]></category>
		<category><![CDATA[问题解决]]></category>

		<guid isPermaLink="false">http://blog.sorayuki.net/?p=623</guid>
		<description><![CDATA[FFMS2是AviSynth的一个来源插件，以FFMPEG为支撑，能读入各种各样的多媒体文件。但是今天在使用它 &#8230; <a href="http://blog.sorayuki.net/?p=623" class="more-link">继续阅读<span class="screen-reader-text">FFMS2读取多媒体文件中的声音的Bug</span> <span class="meta-nav">&#8594;</span></a>]]></description>
				<content:encoded><![CDATA[<p>FFMS2是AviSynth的一个来源插件，以FFMPEG为支撑，能读入各种各样的多媒体文件。但是今天在使用它的时候，遇到了Bug。</p>
<p><span id="more-623"></span>
<p>这个Bug的具体表现是，如果我有一个MP4视频文件，我用某些切割工具对文件进行过切割，因为切割的时候不一定是在关键帧处切割的，而切割过程中又没有重新进行编码，有的工具在这样切割之后生成的文件，音频和视频的第一帧（关键帧）的时间戳都是小于0。小于一点点貌似没事，我遇到Bug的时候，这个时间戳我记得是负的4秒多。</p>
<p>对于这样的文件，我再用FFMS2在AviSynth里载入（记得要包含声音），载入之后用类似 last+last 这样的语句把它们连接起来。这样的AviSynth脚本送到播放器里面播放的话，到了两个片段的连接处，就会发现播放器死了。不是概率性出现而是每次必现。</p>
<p>我从GitHub下载了FFMS2的源代码（<a title="https://github.com/FFMS/ffms2" href="https://github.com/FFMS/ffms2">https://github.com/FFMS/ffms2</a>），在MSYS2环境中执行它那个 build-win-deps.sh 下载 ffmpeg 和编译（因为opencore-amr我用不到，为了节省时间，我就把它注释和屏蔽掉了）。之后用 VisualStudio 打开它那个工程，编译了FFMS2.dll。不论是Debug版本还是Release版本，自己编译的也还是会出现同样的问题。</p>
<p>但是因为是自己编译的，所以用 Debug 版本调试起来就很方便。在卡死的时候中断程序运行，会发现卡死的代码在 audiosource.cpp 的 FFMS_AudioSource::GetAudio 函数里。它不断在一个循环里打转，也不进什么函数，也不出去。很是郁闷。</p>
<p>我重新运行了程序，试图在它将要变成这个样子的临界点用条件断点（判断 Start 变量的值）断了下来。跟踪跟进去，发现和正常的时候相比，此时有个地方不太一样：它在正常的时候，是在数据不够的时候进 DecodeNextBlock 函数，然后 avcodec_decode_audio4 解码一个包，把解码结果 cache 起来。但是在我刚才所说的情况中，因为进了下一个片段，它会先 Seek 当前进度到文件开头，然后再调用 DecodeNextBlock。我观察到的结果是，它还没 Seek 的时候，avcodec_decode_audio4 函数运行的都没什么问题；但是它对这样的文件进行 Seek 之后，读取出来的 Packet 就再也没办法在 avcodec_decode_audio4 里解码后，通过 GotFrame 参数返回 1 了。</p>
<p>也就是说，经过这样跟踪下来，可以认为问题出在 FFMPEG 上，或者调用 FFMPEG 的方式有问题。</p>
<p>我是先考虑了 FFMPEG 可能有问题。因为 build-win-deps.sh 脚本是直接拉的 FFMPEG （git://source.ffmpeg.org/ffmpeg.git） 的 master 分支的代码。我尝试把它替换成去链接官网下载的带正式版本号的 FFMPEG （<a title="http://ffmpeg.zeranoe.com/builds/" href="http://ffmpeg.zeranoe.com/builds/">http://ffmpeg.zeranoe.com/builds/</a>） 的 DLL。具体方法是 build-win-deps.sh 脚本执行完成之后会产生一个 deps 文件夹，在里面可以找到一堆 libav*********.a 这样的文件。因为编译器用的是VC，所以这些 .a 文件其实是 .lib 文件。把它换成官网下的 FFMPEG 的 dev 压缩包里解压出来的文件名差不多的 lib 文件，然后再编译FFMS2工程，能得到一个比较小的 FFMS2.DLL （因为是动态链接到 FFMPEG）。配上 FFMPEG 官网下的 shared 那个压缩包里解压出来的 DLL 文件进行测试，会卡死的问题消失了。因此我判断是 FFMS2 拉的 FFMPEG 的 master 分支可能有什么问题，或者静态链接情况下会有什么问题、在动态链接的时候会消失的。</p>
<p>这样编译出来的 FFMS2 还有一个问题，它在 FFMS2_Init 函数里面，会对 FFMPEG 设置几个回调函数去拦截日志输出。如果用动态链接版的 FFMPEG 对 Avisynth 脚本进行转码，此时 FFMS2 和 FFMPEG.EXE 会用的同一套 FFMPEG 的DLL文件（AV****.DLL 那些）。这样在 Avisynth 加载 FFMS2 插件的时候，这个回调函数的设置会洗掉 FFMPEG.EXE 设置的回调，导致你转码过程中看不到任何进度信息。因为大概看了一下，FFMS2 也并没有想把 FFMPEG 的日志拿来干嘛用，所以我就直接把 FFMS2_Init 里几个注册日志回调的代码给注释掉了。再重新构建 FFMS2.DLL，然后拿一个用了 FFMS2 的 Avisynth 脚本去用 FFMPEG.EXE 转码，没有问题了。</p>
]]></content:encoded>
			<wfw:commentRss>http://blog.sorayuki.net/?feed=rss2&#038;p=623</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>条件变量的伪唤醒</title>
		<link>http://blog.sorayuki.net/?p=602</link>
		<comments>http://blog.sorayuki.net/?p=602#comments</comments>
		<pubDate>Sat, 25 Mar 2017 05:18:08 +0000</pubDate>
		<dc:creator><![CDATA[空雪梦见]]></dc:creator>
				<category><![CDATA[多线程]]></category>

		<guid isPermaLink="false">http://blog.sorayuki.net/?p=602</guid>
		<description><![CDATA[起因是这样的，C++的新标准库添加了多线程的支持，有线程和线程同步的实现。我自己是做Windows开发比较多， &#8230; <a href="http://blog.sorayuki.net/?p=602" class="more-link">继续阅读<span class="screen-reader-text">条件变量的伪唤醒</span> <span class="meta-nav">&#8594;</span></a>]]></description>
				<content:encoded><![CDATA[<p>起因是这样的，C++的新标准库添加了多线程的支持，有线程和线程同步的实现。我自己是做Windows开发比较多，自然用过Event这样的线程同步对象。但是这个东西，在C++的标准库里面没有。我想到说它有一个“条件变量”，而这个“条件变量”是有一个wait / signal 功能的，这个类看起来能实现 Event 的一部分功能。但是在前几天，有个同事说条件变量的 wait 会在没有被 signal 的时候也自己唤醒。毕竟平时不做 posix 那样系统下面的开发，所以这样的事情没有听说过。那会儿其实我是不信的，但是用关键字“condition variable fake wake”在网上搜了一下以后，看到确实是有这样的事情，然后英语中也不是叫 fake，管它叫 spurious wakeup。</p>
<p><span id="more-602"></span>
<p>在网上搜了一大圈，结合多个文章和问答以后，得知在没有其他线程调用 pthread_cond_signal 或者 pthread_cond_broadcast 这样的调用的情况下， pthread_cond_wait 也可能会提前返回。程序员在调用 pthread_cond_wait 的时候，要准备好它可能在没有收到信号的时候就已经醒来的情况。通常的做法是把 pthread_cond_wait 放在一个 while 循环里，循环的条件是条件变量要等待的条件。这样在伪唤醒发生的时候，程序会再次检查条件是否满足。如果条件没有满足，执行流程会转到继续调用 pthread_cond_wait 那边去；如果满足，才继续执行。</p>
<p>在Stackoverflow上我找到了这样的解释： Linux 下的mutex，使用了一种叫做 futex 的更底层的对象来实现。这个 futex 有一个问题，就是在进程收到 signal （<strong>不是 conditional variable 的那个 signal，是 *nix 系统下用 kill 命令发送的那种“信号”</strong>）的时候，正在等待futex 而被挂起的线程，会被唤醒、并执行 signal 的处理函数。现在已经知道了什么一种能直观地看伪唤醒发生的情况，那么做一个实验吧。</p>
<pre>#include &lt;pthread.h&gt;
#include &lt;stdio.h&gt;
#include &lt;unistd.h&gt;
#include &lt;signal.h&gt;

pthread_t mainThread;

void sigHandler(int arg)
{
    const char* p;
    if (pthread_equal(pthread_self(), mainThread))
        p = "the same";
    else
        p = "different";
    printf("SIGUSR1 signal received on %s thread\n", p);
}

int main()
{
    pthread_mutex_t pmutex;
    pthread_cond_t pcond;

    signal(SIGUSR1, sigHandler);

    printf("process id: %d\n", getpid());

    mainThread = pthread_self();

    pthread_mutex_init(&amp;pmutex, 0);
    pthread_cond_init(&amp;pcond, 0);
    pthread_mutex_lock(&amp;pmutex);
    pthread_cond_wait(&amp;pcond, &amp;pmutex);
    puts("wake up!");
    pthread_mutex_unlock(&amp;pmutex);
    return 0;
}
</pre>
<p>很显然，这代码里面因为只有一个线程在跑，所以永远没人会去唤醒 pthread_cond_wait 的等待。但是根据上面那些网上调查所得来的信息，所期望的结果是在有signal那样的信号发来的时候， 线程会被唤醒，于是就在程序里注册了一个signal处理函数，在信号来的时候会输出信息并判断执行信号处理函数的线程是不是主线程，然后输出是不是在主线程收到信号的信息，然后pthread_cond_wait返回，程序输出 wake up! 信息后退出。</p>
<p>因为程序用到了 pthread 所以编译的时候要带上 -lpthread 参数。我在虚拟机里面编译运行这个程序，然后用 kill -SIGUSR1 给它发信号，结果看到了这样的输出。</p>
<p><a href="http://blog.sorayuki.net/wp-content/uploads/2017/03/screenshot1.jpg"><img title="screenshot1" style="border-left-width: 0px; border-right-width: 0px; background-image: none; border-bottom-width: 0px; padding-top: 0px; padding-left: 0px; display: inline; padding-right: 0px; border-top-width: 0px" border="0" alt="screenshot1" src="http://blog.sorayuki.net/wp-content/uploads/2017/03/screenshot1_thumb.jpg" width="391" height="138"></a> </p>
<p>与所期望的结果一致。 </p>
<p>那么现在问题是，这个 pthread_cond_wait 它为什么被唤醒然后发现不是 pthread_cond_signal 做的后，是返回而不是继续回去等待呢？ </p>
<p>在 pthread_cond_signal 函数的说明里，有这么样的描述 </p>
<blockquote>
<p>The <i>pthread_cond_broadcast</i>() and <i>pthread_cond_signal</i>() functions shall have no effect if there are no threads currently blocked on <i>cond</i>.</p>
</blockquote>
<p>也就是说，如果当前没有线程正在等待，那么就没效果。这个和Windows下的手动重置事件是不一样的。 </p>
<p>结合前面说的，信号来的时候线程会被唤醒，那么这个线程此时就不在 blocked 状态。因为在 pthread_cond_wait 等待的时候，和条件变量一起使用的锁是被释放的，所以其他线程在这段时间里是可以拿到锁、修改条件变量的等待条件、并且调用 pthread_cond_signal 的；但此时调用 pthread_cond_wait 的线程因为不是 blocked 状态，所以 pthread_cond_signal 的调用其实没有产生效果。如果 pthread_cond_wait 函数不再次检查条件、而是直接回去等待，那么就错过了这次机会，还有可能因此造成死锁（因为相当于没通知到）。</p>
<pre><p>线程1：---mutex上锁---------------检查条件不满足-------等待-----------------------------</p><p>      -----被kill发的信号唤醒------------------（此时应该做什么？）--------</p><p>
线程2：-------------尝试锁mutex----------------------------锁mutex成功----改变了检查条件-</p><p>      ---------------------pthread_cond_signal---解锁mutex------------
</p></pre>
<p>以上示例中，线程1因为 kill 的信号而错过了 pthread_cond_signal 的信号，如果继续回去等待。那么可能就再也没机会醒过来了。</p>
<p>所以条件变量的使用方式是这么设计的：先加锁，然后一个while循环，如果条件不满足那么就在条件变量上等待，等待结束之后再次检查条件是不是满足：因为如果伪唤醒发生了，那么这个时候条件其实是不满足的。</p>
<p>参考：</p>
<p><a title="http://stackoverflow.com/questions/21411912/what-can-wake-up-a-conditional-variable" href="http://stackoverflow.com/questions/21411912/what-can-wake-up-a-conditional-variable">http://stackoverflow.com/questions/21411912/what-can-wake-up-a-conditional-variable</a><br /><a title="http://man7.org/linux/man-pages/man2/futex.2.html" href="http://man7.org/linux/man-pages/man2/futex.2.html">http://man7.org/linux/man-pages/man2/futex.2.html</a><br /><a title="https://linux.die.net/man/3/pthread_cond_signal" href="https://linux.die.net/man/3/pthread_cond_signal">https://linux.die.net/man/3/pthread_cond_signal</a></p>
]]></content:encoded>
			<wfw:commentRss>http://blog.sorayuki.net/?feed=rss2&#038;p=602</wfw:commentRss>
		<slash:comments>1</slash:comments>
		</item>
		<item>
		<title>视频直播开发的技能树</title>
		<link>http://blog.sorayuki.net/?p=601</link>
		<comments>http://blog.sorayuki.net/?p=601#comments</comments>
		<pubDate>Mon, 20 Mar 2017 16:43:33 +0000</pubDate>
		<dc:creator><![CDATA[空雪梦见]]></dc:creator>
				<category><![CDATA[多媒体]]></category>

		<guid isPermaLink="false">http://blog.sorayuki.net/?p=601</guid>
		<description><![CDATA[现在直播什么的很火，还有拍短视频之类。很多人在开发这方面的软件。我在一个这方面开发的讨论群里看了一段时间以后， &#8230; <a href="http://blog.sorayuki.net/?p=601" class="more-link">继续阅读<span class="screen-reader-text">视频直播开发的技能树</span> <span class="meta-nav">&#8594;</span></a>]]></description>
				<content:encoded><![CDATA[<p>现在直播什么的很火，还有拍短视频之类。很多人在开发这方面的软件。我在一个这方面开发的讨论群里看了一段时间以后，有这种感觉。就是很多人是对多媒体方面的知识不太够，但是公司又要做，只能硬着头皮上。找资料的时候就会遇到这样一个问题，一些基础知识不会，看文章就看得有点迷糊或者看不懂。找别人写好的函数库来调用的话，只能实现一些简单的需求。所以我在想，我以前在字幕组搞过一段时间视频处理，这方面的知识能不能组织一下列出来，并不是全都要会才能做，里面很多我也不熟，只是在遇到问题时候好有个方向，知道要去查什么。</p>
<p>括号里的英文是用来方便搜索的。因为中文互联网里的资料搜起来很多实在不堪入目，搜英语资料虽然看起来比较累，不过根据经验确实是能少踩点坑</p>
<p>有想到的不完整的地方可能还会来补充。</p>
<p><span id="more-601"></span>
<pre>多媒体数据的表示方法
	视频的基本参数
		分辨率（resolution）
			常说的480p、720p、1080p指的是扫描线数量，也就是宽和高里的高。
		逐行扫描（progressive）/基于帧（frame-based）/隔行扫描（interlaced）/基于场（field-based）
			电脑上的视频大多是逐行扫描，但DVD等的视频是隔行扫描的。很多摄像机录出来的视频也是隔行扫描的。
		帧速率（frame rate）
			手机拍摄的视频帧率常常不稳定，是可变的。DVD里常见视频是30FPS隔行扫描但实际是24FPS逐行扫描的电影格式。
		像素比例/画面比例（sample aspect，display aspect等）
			电脑显示器和手机屏幕的像素高宽比例是1:1，但有些视频例如DVD不是这样。
		采样格式/像素格式/色彩空间，RGB，YUV，NV12等，BT.709和BT.601范围，不同像素格式之间转换
			虽然光的三原色是RGB，但因为一些历史原因（从黑白电视机发展而来）视频里常用的是YUV。
			可以参考Avisynth的帮助文档，在Advanced Topics的Colorspace Conversions里面有相关公式和参数。
			可以参考维基百科，里面有数据的具体存储顺序。（基于Planes，或者交错保存）
			BT.709和BT.601因为数值范围不同，弄错了会造成画面颜色偏灰、太浅或者太深等
	音频的基本参数
		采样格式
			有整数有浮点数，其中整数有大端优先（big-endian）和小端优先（little-endian）两种。
			MP3有使用16位整数、24位整数的，AAC使用32位浮点数。保存方式也有按声道分开保存和两个声道交叉保存的。
		采样率
			常见的有22050、32000、44100（CD）、48000（DVD）等，每秒有多少个采样的意思。
		声道数
			单声道（mono），立体声（stereo）。单声道并不一定是只有一边耳朵会响的意思。
多媒体数据的处理
	视频的处理
		OpenGL ES
			（安卓上可以通过OpenGL把要送去编码的视频画面渲染到一个Surface上，
			苹果可以渲染好画面再把数据读回内存送去编码。
			因为手机性能有限，使用显卡加速就变得特别重要）
			OpenGL渲染线程
				使用单独一个线程来跑OpenGL操作是一个常用手段。
			安卓的TextureView控件
			苹果手机的GLKView控件
			Windows上的Angle函数库
				可以用于在Windows上调用OpenGL ES函数，Angle库转成Direct3D执行具体操作。
				因为可以单步步入到函数里，所以用于调试OpenGL ES代码比手机真机更好用。
			Vertex Shader的编写
				视频上叠加一张图片，视频上叠加一串文字等，需要用它计算定位
			Fragment Shader的编写
				摄像头美颜效果，画面的亮度调整等，需要用它计算颜色。
				用于YUV三个通道合成成RGB，播放器要用到
			内存数据加载到OpenGL纹理（Texture）
			OpenGL纹理读回到内存
				DirectTexture：安卓上执行这种操作的性能优化手段
				TextureCache：苹果手机上执行这种操作的性能优化手段
			OES纹理
				安卓上OpenGL ES的扩展里有一种带有OES标识的纹理。
				对于屏幕映射到Surface要输送到Texture里的情况，要使用这种Texture才行
			画布上贴图
				最基本操作，用于显示一个Texture，
				或者一个Texture到另一个Texture的复制，
				一个Texture上叠加另外一些什么东西，等等很多
				贴的时候因为经过了两个Shader的处理，所以可以实现很多效果
			共享的OpenGL上下文组（shared opengl context）
				画面处理的同时如果还要在屏幕上预览效果，
				那么会需要共享的上下文使得一个纹理在不同上下文中使用
			Surface和Texutre的关系，和Framebuffer的关系
				Framebuffer更靠近OpenGL，Surface更靠近操作系统实现。
				安卓上有SurfaceTexture提供桥梁
			安卓的Grafika示例代码
				这个工程里有大段代码可以抄……里面的EGLCore类也是很好用。发扬拿来主义（
			苹果手机的GPUImage库
				很多拿来就用的省事的东西。避免自己造轮子的坑。
	音频的处理
		混音（audio mixing）
			两路不同的音频采样流合并成一个。
			算平均值不一定是个好的办法，有个 A+B-AB 的我用着感觉还不错。
			（根据A和B的符号不同有不同处理，具体带上audio mixing关键字用搜索引擎可以搜出来）
		改变音量
			根据音量大小乘以系数会发现听感上不对。因为这东西不是线性的，要根据“分贝”的概念去调整大小。
多媒体数据的编码
	常用视频编码：H.264
		帧类型
			关键帧、I帧，P帧，B帧
			帧内分片编码（麒麟芯片的手机硬件编码）
		NALU（网络抽象层单元）
			NALU的分割，start code，裸流表示方式，MP4封装时的表示方式
		帧保存顺序/解码顺序
			B帧因为双向参考，依赖前面的帧和后面的帧，需要两边都解码完了才能解码出来
			所以有B帧的情况下帧的保存顺序、解码顺序和播放顺序不同
		编码参数
			码率，码率控制方式（可变码率，固定码率。或者abr vbr crf等），量化值
			不同帧类型间的量化值比例
			关键帧间隔，GOP（Open GOP和Close GOP，I帧和IDR帧），从特定时间开始播放（Seek，只能从I帧开始解码）
			运动预测范围（merange）
			参考帧数量（reference frame count）
			CABAC，CAVLC
			编码复杂度（Profile：baseline，main，high。Level）
		SEI
			一些额外附加数据，可以放自己用来调试或者确认故障用的信息等
		解码参数（SPS，PPS）
			也是NALU。没有这些参数将无法解码。
		常见编解码器
			软件（用CPU编码） OpenH264，x264，Libavcodec(FFMPEG)
			PC硬件（显卡） Nvidia encoder，Intel QuickSync，DXVA，DXVA2
			安卓（专用芯片） MediaCodec，OpenMax，stagefright
			苹果手机 VideoToolbox
	常用音频编码：AAC（音频方面我特别不熟，所以没什么东西可以写）
		码率（比特率）
		音频的帧（一帧包含若干多个采样，比如1024个）
		两种音频编码格式：LC-AAC，HE-AAC
多媒体数据的封装
	帧（frame）、包（packet）的概念，多个流封装在一起（mux）的概念
	直播常用封装格式（FLV，HLS）
		FLV的OnMetaData信息
		HLS的请求方式、播放流程
	视频封装
		SPS和PPS
			如果直播推流过程中断开重连了这些编码器最开始输出的参数信息要重发。
			听说有数据流每个I帧之前都自带SPS和PPS。这种应该是不需要再专门发吧？
		时间戳（播放时间戳pts，解码时间戳dts）
			安卓硬件编码的解码时间戳
				使用安卓硬件编码时，编码器给出的数据只带有播放时间戳pts不带解码时间戳dts。
				有些播放器，例如Html5用的FLV.js，
				在视频流的pts们和dts们不同属于一个离散数值的集合的情况下
				（简单说就是dts出现过的数值，也会出现在pts里），会出现音画不同步。
				dts是单调递增的，但因为手机编码的帧率不稳定性
				导致dts不容易“在保证一定出现在pts数值的集合”的要求下计算出来
				需要耍一些手段把它坑出来……
		低延迟和秒开优化
			视频只能从I帧开始解码，
			I帧间隔越长又会导致编码效率越低（同等数据量下画面更糊）
			所以对于FLV格式的直播视频流，
			有一种手段就是把最开头一堆视频帧的时间戳都调成0，或者帧间隔时间非常非常小。
			让解码器快速解码完这一段，跟上直播的进度。
		画面方向
			MP4文件中保存了一个变换矩阵，用于表示画面的方向。
			比如一个1280x720的视频，可能播放的时候是720x1280的。
			用视频的四个角的坐标乘以变换矩阵，就能知道它是表示什么方向了。
		播放时间
			苹果手机有一种把第一张画面的时间戳调成负的若干秒的手段实现媒体文件切割。
			这种方式FFMPEG不认，转码后那一段又出来了。
		按照关键帧剪辑
			去除不要的GOP、调整所需部分的时间戳，就能做到不重新转码而剪辑视频。
		安卓的MediaMuxer
			虽然MediaExtractor能支持B帧，但是这个MediaMuxer是不支持B帧的。
			如果想要支持B帧，工程里要用一些其他什么第三方库替代掉它。
	RTMP推流
视频音频捕捉
	Windows平台（可以参考开源项目OBS-Studio）
		桌面捕捉
			桌面DC对内存DC复制
			Desktop Duplicator
		窗口捕捉
			窗口DC对内存DC复制
		游戏捕捉
			注入DLL，挂钩Direct3D或者OpenGL的API。跨进程共享一块内存或者共享一个纹理
			多显卡笔记本如果共享纹理会遇到捕捉进程和游戏进程在不同显卡的资源上运行而抓不到画的问题
		摄像头捕捉
			用VFW或者DirectShow可以获取摄像头数据
		声音捕捉
			用WASAPI捕捉系统声音，API调用起来比较麻烦的样子，可以参考OBS-Studio这个软件怎么调用的。
			麦克风声音就有超多手段可以做到，这里不赘述……
	安卓
		屏幕捕捉
			用Virtual Display来映射屏幕到一个Surface上，
			可以通过Surface定期到Texture再渲染到编码器的Surface的方式来稳定帧率。
			这里的Texture要使用OpenGL ES的扩展里，带有OES标识的那种，否则会不出东西。
		摄像头捕捉和声音捕捉我不会
		目前为止还没有公开的API能不影响用户使用的情况下获取系统声音的样子。
	苹果手机
		屏幕捕捉
			使用ReplayKit接口能获取画面，但是需要App主动支持
		系统声音捕捉
			ReplayKit接口送进来的声音数据是大端优先（Big-endian）的，不同于麦克风的小端优先（little-endian）。
			如果不加处理会听到很大的噪音
		摄像头捕捉和麦克风捕捉我不会
可能有帮助的程序和库
	ijkplayer
		哔哩哔哩弹幕视频网的开源播放器。基于FFMPEG里的FFPlay开发，
		对硬件解码支持更好，在安卓和苹果手机上使用，
		很有名，有很多大公司的项目也在用。
		在Windows下补上几个Windows下没有的函数把ijkmedia那个部分编译过的话，
		自己实现一下声音播放（比如接入PortAudio库）（SDL_Aout），视频交给Angle（SDL_Vout），也能使用。
		没实现的情况下直接跑一下代码打开一个视频文件，看它崩哪里，能大概猜到要实现一个什么。
	MPV
		基于MPlayer开发的播放器。
		在Windows上通过调用它的EXE程序的方法，可以当作自己的播放器核心用，
		具体可以参考SMPlayer看看它是怎么送参数调用MPV的。
		是GPL协议。
	FFMPEG
		从视频文件的拆分装到解码到滤镜处理一应俱全的多媒体库，
		硬件软件编解码都有，在Windows、安卓、苹果手机上都能用。
		根据编译参数不同可以是LGPL协议或者GPL协议。
	GPUImage
		苹果手机上用显卡进行视频处理的库。除了处理也包含了保存成视频文件或者录摄像头什么的功能。
		有一些坑容易掉进去，不过因为使用的人多，网上资料好找，具体要怎么修这些坑的资料也好找。
		有人用Java写了安卓的克隆版。不过功能没苹果的全。
	LFLiveKit
		来疯直播的推流库。在我上次给项目里改它代码的时候它还有些地方做得不好比如打时间戳等。
		不过好在源代码都有，问题可以自己修修。
	Grafika
		说是谷歌官方的示例代码。拿来编译运行就能跑，里面大段代码可以参考。
		不过代码功能比较有限，基本还是要自己根据其他地方的知识结合起来才好使。
	Kickflip
		这个库其实我没用过不过部门里的开发有在用。听说现成的功能挺多的，然而同时坑也挺多的。
		我没用过我就不说什么感受了。
	OpenCV
		CV是计算机视觉的缩写。识别个人脸在哪里、什么方向什么角度，是这个的范畴。
		放这里凑个数……因为其实我也没用过它（捂脸
	OBS-Studio
		这是一个跨平台的从捕捉到直播场景编辑到推流甚至还有简单导播功能一应俱全的软件。
		有不知道怎么实现的地方可以看看他是怎么实现的，代码里做了什么，
</pre>
]]></content:encoded>
			<wfw:commentRss>http://blog.sorayuki.net/?feed=rss2&#038;p=601</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
	</channel>
</rss>
