FFmpeg指定x265编码器线程数

转载请注明出处:https://cyc.wiki/index.php/2018/07/17/ffmpeg-x265-threads/


FFmpeg的-threads参数

FFmpeg调用编码器时,一般使用-threads参数对编码器使用的线程数进行设置。
比如:

ffmpeg -s 1920x1080 -framerate 25 -i input.yuv -c:v libx264 -threads 4 -y output.h264

对于x264编码器,这个参数的意义是明确的,既是编码器占用的线程数,也是编码器并行处理的帧数。

但对于x265编码器,这个参数的意义是否还具有同样的意义?
比如:

ffmpeg -s 1920x1080 -framerate 25 -i input.yuv -c:v libx265 -threads 4 -y output.hevc

这个答案存在分歧,虽然不清楚FFmpeg作者的用意,但确实会对一般使用者产生误导,所以在这里阐明。

FFmpeg中libx265.c的线程参数透传

FFmpeg中封装了调用x265编码器的libx265编码器模块,代码位于libavcodec/libx265.c,其中

    ctx->params->frameNumThreads = avctx->thread_count;
    ctx->params->fpsNum          = avctx->time_base.den;
    ctx->params->fpsDenom        = avctx->time_base.num * avctx->ticks_per_frame;
    ctx->params->sourceWidth     = avctx->width;
    ctx->params->sourceHeight    = avctx->height;

第一行就是FFmpeg传递threads参数的语句。FFmpeg编码器选项中的-threads会赋值给avctx->thread_count,从代码中可以看到,这个值被设置给了x265paramsframeNumThreads成员。
这个参数成员等效于x265命令行参数中的--frame-threads,官方文档给出的含义是:


--frame-threads, -F <integer>

Number of concurrently encoded frames. Using a single frame thread gives a slight improvement in compression, since the entire reference frames are always available for motion compensation, but it has severe performance implications. Default is an autodetected count based on the number of CPU cores and whether WPP is enabled or not.

Over-allocation of frame threads will not improve performance, it will generally just increase memory use.

Values: any value between 0 and 16. Default is 0, auto-detect


这意味着该参数代表了x265编码时并行处理的帧数。
那这是不是就代表了x265使用的线程数呢?并不是。HEVC标准在制定时就引入了帧内的WPP(Wavefront Parallel Processing,波前并行处理)机制,使得编码一帧时,所有CTU行可以在满足一定约束条件时并行处理。在x265中有--[no-]wpp命令行参数对WPP的开关进行控制。
当WPP开启时,每一帧都会有多个线程在并行处理,所以整个x265编码器的线程数就会大于--frame-threads所设定的值。

因此FFmpeg在对x265编码器进行调用时,-threads设置的不是x265编码器占有的线程数。

FFmpeg指定x265编码器线程数

那么正确的指定x265编码器线程数的方法是什么呢?答案是:

ffmpeg -s 1920x1080 -framerate 25 -i input.yuv -c:v libx265 -x265-params pools=4 -y output.hevc

这里用到的是x265编码器的--pools命令行参数,它的含义是:


--pools <string>, --numa-pools <string>

Comma seperated list of threads per NUMA node. If “none”, then no worker pools are created and only frame parallelism is possible. If NULL or “” (default) x265 will use all available threads on each NUMA node:


这个参数涉及到NUMA节点的分配,这里就不展开写了。总之,单个整数就代表了x265编码器线程池的大小,也就是x265编码器所占有的线程数。

Windows Internals – 1.3 概念与工具之进程、线程、虚拟内存

转载请注明出处:https://cyc.wiki/index.php/2017/07/14/windows-internals-1-3/


本文表格图片均出自《Windows Internals, Seventh Edition, Part 1》,侵删。
All tables and figures are copyrighted by Windows Internals, Seventh Edition, Part 1. Please contact me to delete if there’s any violation.


Windows进程

一个Windows进程包括以下内容:
– 一个私有的虚拟地址空间,该进程可以使用的一组虚拟内存地址
– 一个可执行程序,该进程最初的代码及数据,它会被映射到虚拟地址空间中
– 一组打开的句柄,对应于各种系统资源,如信号量、同步对象、文件等
– 一个安全上下文,这是一个访问令牌,标识了用户、安全组、权限等信息
– 一个进程ID,这个进程的唯一标识符
– 至少一个执行的线程

在Windows 10中,查看Windows正在运行的进程一般使用任务管理器。
在“进程”一页可以看到正在运行的应用、后台任务、系统服务等所对应的进程,以及其所包含的子进程。
而“详细”一页对应于Windows 7时代的任务管理器,用最原始的方式显示了进程名、进程号、状态等信息。

值得一提的是,Windows进程没有像Linux系统中进程树的组织形式,对于任何的Windows进程,系统只会记住它的父进程ID,其它的关系并不会保留。
例如进程A中创建了进程B,进程B中创建了进程C。如果先kill掉进程B,然后对进程A进行“结束进程树”操作,进程C将不受影响。所以Windows中描述的“进程树”含义并不准确,更准确的描述应该是进程的“父子对”。

线程

一个线程包括以下内容:
– 一组CPU寄存器的内容,表示了当前的处理器状态
– 两个栈,分别用于内核态和用户态
– 一个私有存储空间(thread-local storage,TLS),供子系统、运行时库、动态链接库等使用
– 一个线程ID,线程ID也不会与进程ID冲突

前三项也被称为线程的上下文(Context),对于不同的硬件架构会有不同的实现。

通常来说,线程调度是在内核态完成,频繁切换线程是一个很费资源的操作。为此Windows还提供了两种用户态的线程调度机制:纤程(Fiber)和用户态调度(User-mode scheduling, UMS)线程。这两种技术都不常用,所以就不详细介绍了。

进程与线程的组织结构如下图:

上面的VAD是虚拟地址描述符(virtual address descriptors),是用于记录这个进程用到的虚拟地址段。
中间的Handle Table就是句柄列表,指向了各种内核对象,如信号量、同步对象、文件等。

虚拟内存

Windows基于线性的地址空间实现了虚拟内存机制,使得每个进程仿佛能拥有自己私有的大的地址空间。在运行的时候,内存管理器会将每一页(通常大小为4KB)的虚拟内存空间对应到物理内存或磁盘文件上,并在缺页时把文件中的页调回物理内存中。

对于32位系统,总共的虚拟地址空间理论上是4GB(因为sizeof(void*) == 4,2^32 = 4GB),高地址段的2GB会被留给系统本身使用,低地址段的2GB才是留着应用使用。所以在32位系统中,应用进程所能拥有的最大内存空间就是2GB。

对于64位系统,理论的虚拟地址空间达到了16EB(因为sizeof(void*) == 8,2^64 = 16EB),在Windows 8.1+系统中,高地址段的128TB会保留给系统使用,低地址段的128TB会给应用使用,中间还有一大段是未映射的地址段。当然,要用上这些地址段至少你得有256TB的内存,所以目前来说就不用考虑了。


这一章剩下的内容比较零散,我自己能写的心得也不多,所以就不往下写了。
下一篇直接进入正题写第二章。

C++案例分析 – SQLite3 C API的C++封装(下)

转载请注明出处:https://cyc.wiki/index.php/2017/07/14/cpp-case-sqlite3-cpp-2/


永远不要说“精通C++”。


上一篇介绍了怎么用C++的特性优雅地封装SQLite3中的sqlite3_bind_*函数们。这一篇我们继续来看看怎么优雅地改写sqlite3_column_*函数们。

Github: SQLiteC++ (SQLiteCpp)

sqlite_column_*

在执行了SQL查询语句后,通过会使用sqlite3_step函数去逐步遍历查询结果的各行,而获取该行的各列数据的函数就是sqlite3_column_*。我们还是取上一篇的例子,假设数据表中第1列是int类型,第2列是double类型,第3列是字符串类型。

C API

const void *sqlite3_column_blob(sqlite3_stmt*, int iCol);
int sqlite3_column_bytes(sqlite3_stmt*, int iCol);
int sqlite3_column_bytes16(sqlite3_stmt*, int iCol);
double sqlite3_column_double(sqlite3_stmt*, int iCol);
int sqlite3_column_int(sqlite3_stmt*, int iCol);
sqlite3_int64 sqlite3_column_int64(sqlite3_stmt*, int iCol);
const unsigned char *sqlite3_column_text(sqlite3_stmt*, int iCol);
const void *sqlite3_column_text16(sqlite3_stmt*, int iCol);
int sqlite3_column_type(sqlite3_stmt*, int iCol);
sqlite3_value *sqlite3_column_value(sqlite3_stmt*, int iCol);

C API依然是这么简单粗暴,还是每一种类型的数据对应一个API的命名,但不同的是这一回所有函数的参数列表都一模一样,仅仅是返回值不同。还记得上一篇提到函数重载的实现本质上是由于参数列表被加进了函数签名,因此如果还想对不同的类型使用同样的函数就不能使用函数重载的方法了。

那怎么办呢?我们可以使用一个统一的中间的对象接收sqlite3_stmt*int iCol两个参数,然后利用类型转换操作符根据不同的类型调用不同的C API获取对应的数据,完成到目标类型的转换。

我不想记住函数的名称 – 类型转换操作符

我们来看SQLiteCpp中是怎么实现自动获取相应类型的结果的。
假设我们已经调用过sqlite3_step将游标移到了当前结果行,现在我们调用stmt.getColumn(index)获得一个Column对象:

Column Statement::getColumn(const int aIndex)
{
    checkRow();
    checkIndex(aIndex);

    // Share the Statement Object handle with the new Column created
    return Column(mStmtPtr, aIndex);
}

这个函数的内容很简单,除了一些合法检查,就是构造了一个Column对象并返回,Column对象是构造参数是stmt的指针和列序号。

Column::Column(Statement::Ptr& aStmtPtr, int aIndex) noexcept : // nothrow
    mStmtPtr(aStmtPtr),
    mIndex(aIndex)
{
}

而Column对象的构造函数更是干净,直接把传进来的参数在初始化列表里一存,留下两个空空的大括号。可能你会纳闷这机关究竟在哪呢?当然现在卖这个关子没什么意义,上面都已经剧透过了,就是在类型转换操作符里。

    /// Inline cast operator to int
    inline operator int() const
    {
        return getInt();
    }
    /// Inline cast operator to 32bits unsigned integer
    inline operator unsigned int() const
    {
        return getUInt();
    }
    /// Inline cast operator to 64bits integer
    inline operator long long() const
    {
        return getInt64();
    }
    /// Inline cast operator to double
    inline operator double() const
    {
        return getDouble();
    }
    /// Inline cast operator to char*
    inline operator const char*() const
    {
        return getText();
    }
    /// Inline cast operator to void*
    inline operator const void*() const
    {
        return getBlob();
    }

这里面的get函数相信大家也能猜到怎么写了:

// Return the integer value of the column specified by its index starting at 0
int Column::getInt() const noexcept // nothrow
{
    return sqlite3_column_int(mStmtPtr, mIndex);
}

// Return the unsigned integer value of the column specified by its index starting at 0
unsigned Column::getUInt() const noexcept // nothrow
{
    return static_cast<unsigned>(getInt64());
}

// Return the 64bits integer value of the column specified by its index starting at 0
long long Column::getInt64() const noexcept // nothrow
{
    return sqlite3_column_int64(mStmtPtr, mIndex);
}

// Return the double value of the column specified by its index starting at 0
double Column::getDouble() const noexcept // nothrow
{
    return sqlite3_column_double(mStmtPtr, mIndex);
}

// Return a pointer to the text value (NULL terminated string) of the column specified by its index starting at 0
const char* Column::getText(const char* apDefaultValue /* = "" */) const noexcept // nothrow
{
    const char* pText = reinterpret_cast<const char*>(sqlite3_column_text(mStmtPtr, mIndex));
    return (pText?pText:apDefaultValue);
}

// Return a pointer to the blob value (*not* NULL terminated) of the column specified by its index starting at 0
const void* Column::getBlob() const noexcept // nothrow
{
    return sqlite3_column_blob(mStmtPtr, mIndex);
}

我们最后来看我们的例子里要怎么获取各列的数据:

int a = stmt.getColumn(0);
double b = stmt.getColumn(1);
const char* c = stmt.getColumn(2);

好,总结一下这里发生的事情。
1. stmt.getColumn返回了一个临时构造Column对象,该对象持有获取该列数据所需的参数——stmt指针和列序号,但此时没有真正去获取。
2. 在a/b/c变量初始化过程中发生了隐式的类型转换,触发了int/double/const char*类型转换操作符。
3. 在类型转换操作符中调用了相应的C API获得该列数据,并作为操作符结果返回。

类型转换操作符也可以被显式类型转换触发,例如我想得到b的double值后转换成int,可以这么写:

int bi = static_cast<double>(stmt.getColumn(1)); // warning: double -> int implicit conversion

经过这么封装,不同类型的数据都可以用同一个函数getColumn进行获取。但值得注意的是执行什么类型的C API取决于类型转换的目标类型,显式的还好说,隐式的类型转换有时会忽略而搞错类型;或者是列序号填错导致想要转换的目标类型不是该列的类型,这些都很可能发生C API的失败。所以设计上必须能很好的给出错误信息,以供开发者调试。

为了解决列序号可能填错的问题,有没有更好的办法呢?在数据库应用中,我们通常会把数据表中的一行看作一个整体,以结构体或类的形式保存数据,那有没有办法把结果行直接转换成对应的结构体或类的实例呢?
有,解决的方式很像上一篇第二个问题的解决方法。

我想要参数序号能被自动地填入 – 变长参数模板 + 编译期整数序列

我们来看看SQLiteCpp中怎么让结果行数据直接存入对应的结构体。
getColumns函数定义如下:

    template<typename T, int N>
    T Statement::getColumns()
    {
        checkRow();
        checkIndex(N - 1);
        return getColumns<T>(std::make_integer_sequence<int, N>{});
    }

    // Helper function called by getColums<typename T, int N>
    template<typename T, const int... Is>
    T Statement::getColumns(const std::integer_sequence<int, Is...>)
    {
        return T(Column(mStmtPtr, Is)...);
    }

第一个函数是入口,进来先做了一下合法检查,然后调用第二个工具函数。
上一篇讲过了std::integer_sequence和它的一些推导,可以知道这里传入的参数

std::make_integer_sequence<int, N>

等同于

std::integer_sequence<int, /* a sequence 0, 1, 2, ..., N-1 */>

然后看这个std::integer_sequence传入第二个工具函数以后,帮助推导出了第二个模板参数包Is,即Is参数包的内容为推导为0, 1, 2, …, N-1。
这样,展开参数包Is的语句

return T(Column(mStmtPtr, Is)...);

等同于

return T(Column(mStmtPtr, 0), Column(mStmtPtr, 1), ..., Column(mStmtPtr, N-1));

接下来看我们具体要怎么使用getColumns获得结构体。

为了能接收一个结果行的数据,我们先定义一个结构体:

struct Row {
    int a;
    double b;
    const char* c;
    Row(int a, double b, const char* c) : a(a), b(b), c(c) {}
}

然后调用getColumns,指定模板参数为接收结果的结构体Row和列数3:

auto row = stmt.getColumns<Row, 3>();

根据上面的分析,工具函数中返回的Row对象等同于

return Row(Column(mStmtPtr, 0), Column(mStmtPtr, 1), Column(mStmtPtr, 2));

这里会去调用Row的构造函数,并把每个Column对象隐式转换成相应的参数类型,在隐式转换过程中调用C API获取数据。

参考资料:
Parameter Pack
Integer Sequence


这个案例分析就告一段落,总结一下这里解决问题的思路:
1. 要灵活处理不同的参数类型,可以考虑函数重载或者函数模板。
2. 要灵活处理不同的返回类型,可以考虑类型转换操作符。
3. 要灵活处理不同长度的参数,可以考虑变长参数模板(C++11特性),必要时使用编译期整数序列(C++14特性)作为辅助。


2017.7.18 更新:

之前在分析getColumns函数的时候举到一个例子,通过定义结构体

struct Row {
    int a;
    double b;
    const char* c;
    Row(int a, double b, const char* c) : a(a), b(b), c(c) {}
}

来接收返回的结果行。

不知道大家有没有跟我一样的疑问:为什么这里需要声明一个构造函数呢?如何不声明会怎么样?
我试过把这行构造函数删去,会得到以下编译错误:
error C2440: '<function-style-cast>': cannot convert from 'initializer list' to 'Statement_getColumns_Test::TestBody::Test'
这个错误意味着,在调用

return T(Column(mStmtPtr, Is)...);

时,Column(mStmtPtr, Is)...,类型为initializer list,要先被转换成Test结构体,再调用T(T&&)移动构造函数。而由于不存在initializer list到Test结构体的转换方式,所以报错了。

那有没有办法不需要声明一个构造函数就实现initializer list的直接构造呢?
其实只要把T()改成T{}就可以了:

return T{Column(mStmtPtr, Is)...};

这样有两种用法:
1. 如果没有定义相应参数列表的构造函数,则是以默认initializer list初始化的方式构造结构体,初始化列表里的元素次序与结构体成员定义的次序保持一致。
2. 如果定义了相应参数列表的构造函数,则是以T{}的形式调用构造函数,与T()的行为是基本一致的。

以这个改进建议我向SQLiteCpp项目的作者提出了一个issue,并获得了作者的采纳。作者已经针对我的建议提交了一个fix

C++案例分析 – SQLite3 C API的C++封装(上)

转载请注明出处:https://cyc.wiki/index.php/2017/07/12/cpp-case-sqlite3-cpp-1/


永远不要说“精通C++”。


这其实是来源于我的一个工作项目,其中一个需求就是用C++将SQLite3原来的C API进行一下封装。当时封装做的也就那样,参考了一些别的实现,勉强能用,但是觉得C++封装后做出来的东西确实很优雅。刚好在Github上找到了类似的项目和实现方法,就拿出来解析一下。

Github: SQLiteC++ (SQLiteCpp)

主要拿SQLite3最常用的其中两个操作做例子:sqlite3_bind_*sqlite3_column_*

sqlite3_bind_*

sqlite3_bind_*的作用就是将对应位置的参数值绑定到预备查询语句字符串当中的“?”处。例如SELECT * FROM t WHERE c1=? AND c2=? AND c3=?,里面有三个参数需要填入值。这里我们假设列c1是int类型,列c2是double类型,列c3是字符串类型。

C API

int sqlite3_bind_blob(sqlite3_stmt*, int, const void*, int n, void(*)(void*));
int sqlite3_bind_blob64(sqlite3_stmt*, int, const void*, sqlite3_uint64,
                        void(*)(void*));
int sqlite3_bind_double(sqlite3_stmt*, int, double);
int sqlite3_bind_int(sqlite3_stmt*, int, int);
int sqlite3_bind_int64(sqlite3_stmt*, int, sqlite3_int64);
int sqlite3_bind_null(sqlite3_stmt*, int);
int sqlite3_bind_text(sqlite3_stmt*,int,const char*,int,void(*)(void*));
int sqlite3_bind_text16(sqlite3_stmt*, int, const void*, int, void(*)(void*));
int sqlite3_bind_text64(sqlite3_stmt*, int, const char*, sqlite3_uint64,
                         void(*)(void*), unsigned char encoding);
int sqlite3_bind_value(sqlite3_stmt*, int, const sqlite3_value*);
int sqlite3_bind_zeroblob(sqlite3_stmt*, int, int n);
int sqlite3_bind_zeroblob64(sqlite3_stmt*, int, sqlite3_uint64);

C API可以说是简单粗暴,每一种类型的参数有一个对应名字的API函数,前两个参数一样,第一个是语句指针,第二个是参数的序号,后面的参数就跟参数值有关。

于是填入参数需要下面的语句:

sqlite3_bind_int(stmt, 1, 10);
sqlite3_bind_double(stmt, 2, 20.2);
sqlite3_bind_text(stmt, 3, "30", -1, SQLITE_TRANSIENT);

看到这组API,也许你不想要记住函数的名称,并且想要参数序号能被自动地填入。好,那我们来看C++怎么帮你实现。

我不想记住函数的名称 – 函数重载

这应该是C++很基本的一个语言特性了,所以就不多介绍。这里只说下函数重载之所以能在C++中实现,根本原因是C++改变了函数签名的规则,将函数参数列表(不包括返回值)加入了函数签名中,所以不同函数参数列表的同名函数本质上就是不同的符号。C中没有这么做,符号基本上就是函数名本身。在C++中用extern “C”定义的函数,其实就是把函数参数列表从函数签名中扒掉,于是C的程序就可以找到extern “C”定义的函数。

SQLiteCpp做了如下封装:

// Bind an int value to a parameter "?", "?NNN", ":VVV", "@VVV" or "$VVV" in the SQL prepared statement
void Statement::bind(const int aIndex, const int aValue)
{
    const int ret = sqlite3_bind_int(mStmtPtr, aIndex, aValue);
    check(ret);
}

// Bind a 32bits unsigned int value to a parameter "?", "?NNN", ":VVV", "@VVV" or "$VVV" in the SQL prepared statement
void Statement::bind(const int aIndex, const unsigned aValue)
{
    const int ret = sqlite3_bind_int64(mStmtPtr, aIndex, aValue);
    check(ret);
}

// Bind a 64bits int value to a parameter "?", "?NNN", ":VVV", "@VVV" or "$VVV" in the SQL prepared statement
void Statement::bind(const int aIndex, const long long aValue)
{
    const int ret = sqlite3_bind_int64(mStmtPtr, aIndex, aValue);
    check(ret);
}

// Bind a double (64bits float) value to a parameter "?", "?NNN", ":VVV", "@VVV" or "$VVV" in the SQL prepared statement
void Statement::bind(const int aIndex, const double aValue)
{
    const int ret = sqlite3_bind_double(mStmtPtr, aIndex, aValue);
    check(ret);
}

// Bind a string value to a parameter "?", "?NNN", ":VVV", "@VVV" or "$VVV" in the SQL prepared statement
void Statement::bind(const int aIndex, const std::string& aValue)
{
    const int ret = sqlite3_bind_text(mStmtPtr, aIndex, aValue.c_str(),
                                      static_cast<int>(aValue.size()), SQLITE_TRANSIENT);
    check(ret);
}

// Bind a text value to a parameter "?", "?NNN", ":VVV", "@VVV" or "$VVV" in the SQL prepared statement
void Statement::bind(const int aIndex, const char* apValue)
{
    const int ret = sqlite3_bind_text(mStmtPtr, aIndex, apValue, -1, SQLITE_TRANSIENT);
    check(ret);
}

// Bind a binary blob value to a parameter "?", "?NNN", ":VVV", "@VVV" or "$VVV" in the SQL prepared statement
void Statement::bind(const int aIndex, const void* apValue, const int aSize)
{
    const int ret = sqlite3_bind_blob(mStmtPtr, aIndex, apValue, aSize, SQLITE_TRANSIENT);
    check(ret);
}

非常简单,就这样使用同样的函数名也能绑定不同类型的函数了。之前的实现代码变成了:

stmt.bind(1, 10);
stmt.bind(2, 20.2);
stmt.bind(3, "30");

我想要参数序号能被自动地填入 – 变长参数模板

变长参数模板(Variadic Template)是C++11引入的一个新特性,可以把模板玩得很花,也很实用。我们先来看下SQLiteCpp里的实现:

/// implementation detail for variadic bind.
namespace detail {
template<class F, class ...Args, std::size_t ... I>
inline void invoke_with_index(F&& f, std::integer_sequence<std::size_t, I...>, const Args& ...args)
{
    std::initializer_list<int> { (f(I+1, args), 0)... };
}

/// implementation detail for variadic bind.
template<class F, class ...Args>
inline void invoke_with_index(F&& f, const Args& ... args)
{
    invoke_with_index(std::forward<F>(f), std::index_sequence_for<Args...>(), args...);
}
}

template<class ...Args>
void bind(SQLite::Statement& s, const Args& ... args)
{
    static_assert(sizeof...(args) > 0, "please invoke bind with one or more args");

    auto f=[&s](std::size_t index, const auto& value)
    {
        s.bind(index, value);
    };
    detail::invoke_with_index(f, args...);
}

这个实现中还利用了C++14的编译期整数序列(std::integer_sequence)特性,免去了变长参数模板展开常用的递归模式。其实不使用C++14的特性,单用C++11的变长参数模板也能实现自动填写参数序号。

上面两个是工具函数,为最下面的bind函数服务。我们先来看bind函数:

template<class ...Args>
void bind(SQLite::Statement& s, const Args& ... args)
{
    static_assert(sizeof...(args) > 0, "please invoke bind with one or more args");

    auto f=[&s](std::size_t index, const auto& value)
    {
        s.bind(index, value);
    };
    detail::invoke_with_index(f, args...);
}

template<class... Args>声明了Args是一个类型模板的参数包,这个对应于传统的类型模板template<class T>声明T是一个类型模板的参数,在推导模板的时候类型模板参数包可以包含不定长度的模板类型。

const Args& ... args声明了参数args是一个函数参数包,类型为const Args&...,在调用的时候函数参数包可以对应不定长度的函数参数。

sizeof...运算符可以获得参数包args的参数个数。此处args后不带…,说明此处args以参数包形式出现。

f是一个无返回值的lambda表达式,代表着传入index和value后,调用s.bind(index, value)的一段逻辑。

detail::invoke_with_index(f, args...)调用工具函数,参数分别是f和args…,此处args以展开后的参数包形式出现。

接着我们看上面的第二个函数:

template<class F, class ...Args>
inline void invoke_with_index(F&& f, const Args& ... args)
{
    invoke_with_index(std::forward<F>(f), std::index_sequence_for<Args...>(), args...);
}

这还是一个带参数包的函数模板,F对应于将要传入的lambda的类型,Args跟之前一样是一个类型模板参数包。
接下来就是调用第一个工具函数。

第一个参数是将要传入的lambda,代表怎么处理这些个参数。std::forward完美转发,这是C++11右值的一个重要特性,可以单开一篇写,这里可以先忽略它。

第二个参数std::index_sequence_for<Args...>()用到了C++14的编译期整数序列特性。在C++14的头文件中有以下定义:

template<class T, T N>
using make_integer_sequence = std::integer_sequence<T, /* a sequence 0, 1, 2, ..., N-1 */ >;
template<std::size_t N>
using make_index_sequence = make_integer_sequence<std::size_t, N>;
template<class... T>
using index_sequence_for = std::make_index_sequence<sizeof...(T)>;

经过上面的推导,最终传进去的就是一个std::integer_sequence<std::size_t, 0, 1, 2, ..., N-1>,N为变长参数的个数。

第三个参数就是展开后的参数包args。

最后我们看第一个工具函数:

template<class F, class ...Args, std::size_t ... I>
inline void invoke_with_index(F&& f, std::integer_sequence<std::size_t, I...>, const Args& ...args)
{
    std::initializer_list<int> { (f(I+1, args), 0)... };
}

模板参数中,第一个为类型模板参数F,第二个为类型模板参数包Args,第三个为非类型模板参数包I,I对应于一组类型为std::size_t的数值。

函数参数中,第一个对应将要传入的lambda,第二个是编译期整数序列,其实就是上面传进来的0, 1, 2, …, N-1,用于推导中模板参数中的非类型模板参数包I,第三个是参数包args。

结合上面的分析,f是处理参数的逻辑,args是参数列表,I中含有各参数对应的序号,看起来万事大吉,只要展开调用就完事了。就像这样:

(f(I+1, args))...;

结果会收到一个编译错误,说I和args在这里必须要展开。可是我已经把整个表达式用…展开了啊?事情没有那么简单,参数包展开的位置是有严格的限制的,基本只在函数调用的参数列表里可以展开。但也还有一种情况,可以在初始化列表里的大括号里展开,并且这里还有一个好处,初始化列表里的大括号里展开能保证顺序执行。于是就写成这样:

std::initializer_list<int> { (f(I+1, args), 0)... };

这个意思是用初始化列表初始化一个匿名的std::initializer_list<int>(其实大括号初始化列表本身就是这个类型)。由于列表的元素必须是int类型,因此展开时用逗号表达式,先执行f(I+1, args),再把表达式的值替换成0。展开后的样子就是:

std::initializer_list<int> { (f(1, arg0), 0), (f(2, arg1), 0), ..., (f(N, argN-1), 0) };

顺序执行完所有逗号表达式后其实就是:

std::initializer_list<int> { 0, 0, ..., 0 };

这样既调用了f(1, arg0)到f(N, argN-1),又能符合初始化列表的语法和参数包展开的语法。
详细的讨论参考这个Stackoverflow帖子

最后之前例子的调用变成:

bind(stmt, 10, 20.2, "30");

是不是觉得世界怎么这么美好。

参考资料:
Parameter Pack
Integer Sequence

Windows Internals – 1.2 概念与工具之Windows API

转载请注明出处:https://cyc.wiki/index.php/2017/07/11/windows-internals-1-2/


本文表格图片均出自《Windows Internals, Seventh Edition, Part 1》,侵删。
All tables and figures are copyrighted by Windows Internals, Seventh Edition, Part 1. Please contact me to delete if there’s any violation.


Windows API

Windows API是Windows操作系统的用户态系统编程接口。在64位系统诞生以前,32位Windows操作系统的API被称作Win32 API,以区别之前的16位Windows API。现在有了64位的API之后,还沿用了Win32 API的叫法,32位和64位的Windows API都习惯被称为Win32 API

Win32 API是一组C语言的函数,数量多达上千条。Windows API Index列出了Windows API的所有类别,而Windows 8.1 API Sets则列出了Windows 8.1所支持的所有Windows API,以及它所依赖的链接库文件名。这些列出的Windows API(包括一些暴露出接口但没有向外公布的内部API)就涵盖了开发者在用户态所能实现的所有系统功能,其它任何形式的系统编程接口基本上都是基于这一组最底层最基本的Win32 API进行封装而实现的。

COM

正因为C风格的Win32 API过于庞杂,缺乏合适的组织结构,命名风格也不统一,后来出现的COM API取代了Win32 API成为新一代的系统编程接口。

COM(Component Object Model,组件对象模型)基于以下两个设计准则:
1. 对象Client端(调用者)通过一组定义好的接口协议与Server端(实现的dll库)进行通信,这些接口通过类似虚函数表的机制将接口调用映射到一组逻辑相关的方法实现。
2. 实现是通过dll动态加载,而不是静态链接到Client端。

第一点非常关键,它使得方法的调用成为了一种范式,这种范式可以以任何合适的编程语言实现,从而使得组件被COM的接口封装后,可以用C、C++、VB、C#等任何一种.NET平台支持的语言进行调用,大大地增加了系统组件的扩展性。
同时,在调用的范式中可以预先对要调用的方法进行检查其存在性,使得版本兼容性大大提升。举个例子,有一个库A1,客户端C2调用了库A1的新版本A2的一个新加接口,而运行环境中还是库A1的dll。如果使用C++直接链接的方法调用,客户端C2因为在库A1的dll中找不到新加接口的符号而崩溃退出。但如果使用COM封装后的库,客户端C2在调用不存在的接口时,会优雅的抛出Exception,只要接住进行处理,就能继续运行下去。

The Windows Runtime (WinRT)

Windows 8引入了当时称为Metro App的磁贴应用,后来更名为Windows Store App,在Windows 10中就叫做UWP App。支持这种应用的开发而引入的API就被称作WinRT API。Windows Runtime (WinRT)跟Windows RT要区分开,后者是那个命短的ARM操作系统(Surface 3)。

WinRT是基于COM技术开发的,加入了一些新的扩展信息,最主要的是将接口的定义写在了的一种元数据文件.WINMD里。WINMD文件是二进制文件,可以用VS中的对象查看器打开,能看到里面所定义的WinRT接口。

跟COM一样,WinRT接口也可以在各种语言中进行调用,除了C++、VB、C#这.NET三剑客之外,还支持JavaScript,这使得可以用一些前端的技术写UWP App。注意这里面的C++并不是纯C++,而是一种叫做C++/CX的语言,专为WinRT编程设计。如果你看到一个.cpp文件里面出现的不是*而是^,那这就是一个C++/CX的文件。我在微软里的第一个项目就碰上了C++/CX,当时整得一头雾水。后来发现,其实可以先用C#写一遍,然后对应翻译过去就可以了。

 
在Windows 10的系统中,可以存在两种应用,一种是经典桌面应用,也就是我们一直以来熟知的那种应用;另一种是UWP应用,也就是在Windows 8只能全屏显示的磁贴、到了Windows 10基本跟经典应用区分不开的那种应用。
对于这两种应用,经典桌面应用一般使用Win32 API和COM API,但也可以使用一部分WinRT API。而UWP应用一般使用WinRT API,但也可以使用一部分Win32 API和COM API。

The .NET Framework

.NET Framework应该是继MFC之后,编写带界面的Windows应用最常用的框架。它主要由两部分组成:
1. 公共语言运行时(CLR) 这是.NET Framework的引擎部分,负责将抽象的中间语言翻译成在硬件上运行的指令,实现其实是一个COM组件的Server端,里面的功能由底层的Windows API提供。
2. .NET Framework类库(FCL) 这是.NET Framework API的封装部分,向上提供.NET Framework API,向下调用CLR层提供的COM接口。

.NET Framework和WinRT在上层API技术实现上非常相像,但是底层跟Win32 API的对接不一样,因为.NET Framework是面向经典桌面应用,而WinRT是短期面向UWP应用,长期面向OneCore系统。

Windows Internals – 1.1 概念与工具之Windows版本

转载请注明出处:https://cyc.wiki/index.php/2017/07/09/windows-internals-1-1/


本文表格图片均出自《Windows Internals, Seventh Edition, Part 1》,侵删。
All tables and figures are copyrighted by Windows Internals, Seventh Edition, Part 1. Please contact me to delete if there’s any violation.


Windows 版本号

第一章开头主要讲解了Windows操作系统的版本更迭以及未来的更新计划。

下面的表格列出了Windows操作系统发布的产品名称以及其对应的内部版本号:

Product Name Internal Version Number Release Date
Windows NT 3.1 3.1 Jul 1993
Windows NT 3.5 3.5 Sep 1994
Windows NT 3.51 3.51 May 1995
Windows NT 4.0 4.0 Jul 1996
Windows 2000 5.0 Dec 1999
Windows XP 5.1 Aug 2001
Windows Server 2003 5.2 Mar 2003
Windows Server 2003 R2 5.2 Dec 2005
Windows Vista 6.0 Jan 2007
Windows Server 2008 6.0 (SP1) Mar 2008
Windows 7 6.1 Oct 2009
Windows Server 2008 R2 6.1 Oct 2009
Windows 8 6.2 Oct 2012
Windows Server 2012 6.2 Oct 2012
Windows 8.1 6.3 Oct 2013
Windows Server 2012 R2 6.3 Oct 2013
Windows 10 10.0.10240 Jul 2015
Windows 10 version 1511 10.0.10586 Nov 2015
Windows 10 version 1607 10.0.14393 Jul 2016
Windows Server 2016 10.0.14393 Oct 2016

发现规律没?
在Windows 7以前,Windows的产品名称和内部版本号是对应的,NT时代的系统命名很直接,就是Windows NT x.y(其中4.0就是大名鼎鼎的Windows 95);到了Windows 2000,XP,Vista,也基本上是按照架构的更迭去更新大版本号或小版本号。
但是Windows 7为什么还叫做6.1,后面的也都不再更新大版本号,直到Windows 10发布呢?作者告诉我们,原来是因为Windows XP太火了,有大量的软件为Windows XP平台设计而开发,其中检测操作系统版本的条件很多都是大版本号>=5且小版本号>=1,于是Vista的版本号升至6.0时,有大量的XP系统上开发的软件无法运行,成为了Vista这个背锅王背的其中一个锅。为了避免这类事情再次发生,Windows就再也不敢随便改版本号了,每次就在小版本号上加一点就好了。
这个局面一直持续到了Windows 10发布。至于Windows 10为什么叫10不叫9,有人说是因为判断Windows 9x系统的版本条件是匹配字段以”Windows 9″开头,于是就会跟Windows 9重复,其实看了上面这个表你就明白这就是一个段子而已,真正的程序员都是读NT的版本的。

Windows 10

说说我跟Windows 10的结缘吧。我在2015年夏天进入微软实习,加入操作系统工程院,那时候正好是Windows 10发布和升级的时候。有一天我就发现,电梯口对着的那间会议室被装扮成了这个模样:

经常可以看到一些大佬们在里边开会,还有投影幕布上跳动的数字。亲身处在这个环境中,心情还是有点激动的。出了升级失败的bug之后,也会跟着担心这个bug啥时候能解决。虽然我的实习内容跟这个没什么关系,但是能感受到这样紧张的氛围。

等我正式入职微软以后,确实就在参与Windows 10操作系统的开发工作了。每天来到公司,点下测试机的系统升级的重启按钮,pull下最新代码敲下编译命令,然后出去泡杯咖啡,就这样开始每天的工作。以前觉得重装系统费事,现在每天重装一遍,美滋滋。

书上提到了Windows 10两个重要特点:

Windows as a service

在PC时代,软件迭代的速度比较慢,人们学习和接受新的软件功能的速度也比较慢,传统软件工程的迭代周期会比较长,以前Windows大概两年左右出一个新版本。有点像是攒气憋大招,当然也有空大的状况(又说你了,Vista)。
后来进入互联网时代了,软件都往云上部署,想上什么新功能直接就上线,过两天出bug了马上又能撤回来。Windows这种步伐已经完全赶不上用户的需求,于是从Windows 10开始,Windows将不再开发全新的版本,而是直接在Windows 10上进行迭代更新,目前是每年有两次大的更新。其实就是在向互联网产品看齐,将Windows作为一种服务,通过云端更灵活地进行更新迭代和个人定制(Office 365就是传统软件上云的一个成功例子)。
所以为啥Windows 10刚出来的时候bug这么多,还不是你们逼的(手动滑稽)。另外插句题外话,Windows Update劝大家还是打开吧,很多我们搞的bug都得在Update里面修。。

我自己画了个表列了一下目前Windows 10的版本和内部代号:

产品版本号 内部代号
Windows 10 TH1
Windows 10 Version 1511 TH2
Windows 10 Version 1607: Anniversary Update RS1
Windows 10 Version 1703: Creator Update RS2

OneCore

能够谈这个问题的公司估计目前只有微软一个。Windows基本覆盖了我们日常生活中使用到的大部分电子设备,PC、平板、手机(这个虽然没人用但是可以有)、游戏主机Xbox、混合现实设备HoloLens,还有地铁上、公交上、自动售货机上偶尔能看到的蓝屏。它们都是Windows,但是应用却不能通用。
从Windows 8开始,Windows开始了它的内核一体化进程。Windows 8和Windows Phone 8实现了同一套内核共享。Windows 8.1和Windows Phone 8.1实现了同一套WinRT API共享。到了Windows 10,真正实现了OneCore平台,实现了在PC、手机、Xbox、HoloLens、IoT设备上共享同一个操作系统平台。Windows 10上的Univeral Windows Apps就是这个共享平台的产物,它可以在所有的Windows 10设备上运行。
那OneCore的进程是不是就结束了?当然没有,实现了共享之后下一步就是把非共享的部分清除出去,这样才能真正的实现纯的OneCore Windows。最新发布在Surface Laptop上的Windows 10 S就是这样一个纯OneCore的Windows(这么描述其实还不够准确,系统本身的OneCore化就没有完全),只能运行UWP应用。经典桌面应用中这种粗犷的运行方式具有太多的问题,安全性不好,系统文件容易被损坏,兼容性也存在问题。未来终将是UWP应用的天下,只是改革历程会持续比较长的时间。

Windows Internals – 0 开篇

开篇

一直就想开一个技术博客,但是总是想不到从哪写起。从大二开始,我就接触各种各样的项目,写过大大小小的软件,带界面的、不带界面的,单机的、分布式的,手机上的、服务器上的,Windows的、Linux的,使用了各种各样的技术,踩过各种各样的坑,但是都没有把它们记录下来,这些经历有些能化成简历上的一条项目经历,有些就随着时间消逝了,后来想想多少有点可惜。我可以找各种各样的借口,但是最后都归结为一条——懒,现在的人喜欢叫拖延症。

从接触编程开始,我就一直对大型的软件架构很感兴趣,我一贯不赞同所谓快猛糙的软件开发方法,你可以敏捷,但也要保证质量和可扩展性,不择手段地实现一些丑陋的功能只是杀鸡取卵,这样不能建成软件的大厦。我加入微软写Windows,这也是其中的一条原因吧。不是每个人都有幸能看到这个人类迄今为止最大规模的软件之一的源码,虽然也不是每个人都稀罕。但是起码我自己感觉在这个极其庞大的系统中工作,了解它的运作原理,以及亲身经过它的开发、构建、测试、发布,本身是一件挺有趣的事。

这段时间刚好是stablization阶段,事情不是太多,就从公司图书馆借了一本书,Windows Internals (7th),中文译名叫深入解析Windows操作系统,目前中文译版只出到第六版。在借出成功过去一个星期后,我问图书馆我的书怎么还没有收到,图书馆说你借的书正在从美国寄过来的路上,下个星期就能到了。我顿时感叹,我司的图书馆好良心啊!于是觉得这么借来的书,不仔细看看好像对不起它长途跋涉的旅程。何奈这本书跟Windows本身一样巨厚,估计看过的东西很难记下来,就趁此机会开这个博客,把读书心得记录于此。

关于这本书

Windows Internals, Part 1: System architecture, processes, threads, memory management, and more, 7th Edition
By Pavel Yosifovich, Alex Ionescu, Mark E. Russinovich, David A. Solomon
https://www.microsoftpressstore.com/store/windows-internals-part-1-system-architecture-processes-9780735684188

目前Part 2还没有正式出版。

以下是这本书之前的版本历史以及其对应描述的Windows版本:

年代 版本 名称 Windows版本
1992 1 Inside Windows NT Windows NT 3.1
1998 2 Inside Windows NT, Second Edition Windows NT 4.0
2000 3 Inside Windows 2000, Third Edition Windows 2000
2004 4 Windows Internals, Fourth Edition Windows XP, Windows Server 2003
2009 5 Windows Internals, Fifth Edition Windows Vista, Windows Server 2008
2012 6 Windows Internals, Sixth Edition Windows 7, Windows Server 2008 R2
2017 7 Windows Internals, Seventh Edition Windows 10, Windows Server 2016

从这个列表以及书的版本更替多少也能看出来Windows在哪个版本经历了大的架构改变。

这本书讲的是Windows的运作原理,并不是面向使用者和应用开发者去描述如何去使用Windows或如何在Windows平台上开发应用。我应该是这本书更适合的受众之一,它上面讲到的每一个技术点我甚至都能直接从Windows源码中找到对应。因此我会尽可能地结合我工作的经历和内容,把我的理解写下来。但是受限于我的技术水平和知识储备,会有很多地方理解不到位或者有误,希望读者能包容并提醒我改正。

这本书目前的借期是3个月,我也没有雄心壮志3个月一定读完。但是前两章一定会仔细读完,在这里写下心得,以此为证。