<?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; Linux编程</title>
	<atom:link href="https://blog.gmem.cc/tag/linux%e7%bc%96%e7%a8%8b/feed" rel="self" type="application/rss+xml" />
	<link>https://blog.gmem.cc</link>
	<description></description>
	<lastBuildDate>Sun, 19 Apr 2026 07:54:29 +0000</lastBuildDate>
	<language>en-US</language>
		<sy:updatePeriod>hourly</sy:updatePeriod>
		<sy:updateFrequency>1</sy:updateFrequency>
	<generator>http://wordpress.org/?v=3.9.14</generator>
	<item>
		<title>Linux内核编程知识集锦</title>
		<link>https://blog.gmem.cc/linux-kernel-programming-faq</link>
		<comments>https://blog.gmem.cc/linux-kernel-programming-faq#comments</comments>
		<pubDate>Sat, 07 Jan 2012 07:26:08 +0000</pubDate>
		<dc:creator><![CDATA[Alex]]></dc:creator>
				<category><![CDATA[C]]></category>
		<category><![CDATA[Linux]]></category>
		<category><![CDATA[kernel]]></category>
		<category><![CDATA[Linux编程]]></category>
		<category><![CDATA[系统编程]]></category>

		<guid isPermaLink="false">https://blog.gmem.cc/?p=35145</guid>
		<description><![CDATA[<p>编译内核 下载源码 [crayon-69e74a6c7fdda918652091/]  根据需要，切换分支。 安装工具 [crayon-69e74a6c7fde0431774694/] 配置内核 [crayon-69e74a6c7fde3515376038/] 你也可以手工直接编辑.config文件。每个选项都可以设置以下值之一： 取值 说明 y 将相应特性构建到内核中 n 不包含此特性 m 构建为模块，这样可以按需加载 注意： 除非你使用initrd，否则绝不要把挂载根文件系统必需的驱动程序(硬件驱动以及文件系统驱动)编译成模块 如果系统中有网卡，将它们的驱动编译成模块。这样就能够在 /etc/modules.conf 中用别名定义哪一块网卡第1，哪一块第2，等等。如果您将驱动程序编译进了内核，它们加载的顺序将取决于当初它们链接进内核的顺序，而这不一定是您想要的 <a class="read-more" href="https://blog.gmem.cc/linux-kernel-programming-faq">[...]</a></p>
<p>The post <a rel="nofollow" href="https://blog.gmem.cc/linux-kernel-programming-faq">Linux内核编程知识集锦</a> appeared first on <a rel="nofollow" href="https://blog.gmem.cc">绿色记忆</a>.</p>
]]></description>
				<content:encoded><![CDATA[<div class="wri_content_clear_both"><div class="blog_h1"><span class="graybg">编译内核</span></div>
<div class="blog_h2"><span class="graybg">下载源码</span></div>
<pre class="crayon-plain-tag">git clone https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux-stable.git</pre>
<p> 根据需要，切换分支。</p>
<div class="blog_h2"><span class="graybg">安装工具</span></div>
<pre class="crayon-plain-tag">apt install git fakeroot build-essential ncurses-dev xz-utils libssl-dev bc flex libelf-dev bison</pre>
<div class="blog_h2"><span class="graybg">配置内核</span></div>
<pre class="crayon-plain-tag">cd linux-stable

# 使用当前系统内核配置
cp -v /boot/config-$(uname -r) .config

# 使用图形化界面进行配置
make menuconfig

# 使用字符界面配置
make config

# 在现有的内核设置文件基础上建立一个新的设置文件，只会向用户提供有关新内核特性的问题，
# 在新内核升级的过程 中，make oldconfig非常有用
make oldconfig

# 自动的尽量选择 y 选项
make allyesconfig

# 自动的尽量选择 m 选项
make allmodconfig</pre>
<p>你也可以手工直接编辑.config文件。每个选项都可以设置以下值之一：</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 100px; text-align: center;">取值</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>y</td>
<td>将相应特性构建到内核中</td>
</tr>
<tr>
<td>n</td>
<td>不包含此特性</td>
</tr>
<tr>
<td>m</td>
<td>
<p>构建为模块，这样可以按需加载</p>
<p>注意：</p>
<ol>
<li>除非你使用initrd，否则绝不要把挂载根文件系统必需的驱动程序(硬件驱动以及文件系统驱动)编译成模块</li>
<li>如果系统中有网卡，将它们的驱动编译成模块。这样就能够在 /etc/modules.conf 中用别名定义哪一块网卡第1，哪一块第2，等等。如果您将驱动程序编译进了内核，它们加载的顺序将取决于当初它们链接进内核的顺序，而这不一定是您想要的</li>
</ol>
</td>
</tr>
</tbody>
</table>
<div class="blog_h2"><span class="graybg">编译内核</span></div>
<pre class="crayon-plain-tag"># 编译
make -j 8

# 编译bzImage
make bzImage</pre>
<div class="blog_h3"><span class="graybg">内核文件格式</span></div>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 100px; text-align: center;">格式</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>vmlinux</td>
<td>编译出来的最原始的内核文件，未压缩</td>
</tr>
<tr>
<td>zImage</td>
<td>zImage是ARM Linux常用的一种压缩映像文件，是vmlinux经过gzip压缩产生的</td>
</tr>
<tr>
<td>bzImage</td>
<td>bz表示big zImage，和zImage的区别是，zImage解压缩内核到低端内存（第一个640K），bzImage解压缩内核到高端内存（1M以上）。如果内核比较小，那么采用zImage或bzImage都行，如果比较大应该用bzImage</td>
</tr>
<tr>
<td>uImage</td>
<td>uImage是U-boot专用的映像文件，它是在zImage之前加上一个长度为0x40的头，说明这个映像文件的类型、加载位置、生成时间、大小等信息。换句话说，如果直接从uImage的0x40位置开始执行，zImage和uImage没有任何区别</td>
</tr>
<tr>
<td>vmlinuz</td>
<td>是bzImage/zImage文件的拷贝或指向bzImage/zImage的链接</td>
</tr>
<tr>
<td>initrd</td>
<td>initramfs</td>
</tr>
</tbody>
</table>
<div class="blog_h2"><span class="graybg">安装内核</span></div>
<pre class="crayon-plain-tag"># 安装内核模块到系统
sudo make modules_install
# 安装内核到系统
sudo make install  </pre>
<p>更新<a href="/linux-kernel-study-note-vol5#initramfs">initramfs</a>，指向最新的内核： </p>
<pre class="crayon-plain-tag">sudo update-initramfs -c -k 4.15.0</pre>
<p>更新GRUB BootLoader：</p>
<pre class="crayon-plain-tag">sudo update-grub</pre>
<p>重启后就可以使用新内核了。 </p>
<div class="blog_h1"><span class="graybg">调试内核</span></div>
<div class="blog_h2"><span class="graybg">编译内核</span></div>
<p>设置内核选项：</p>
<pre class="crayon-plain-tag">make O=build/ defconfig

make O=build/ menuconfig

# Processor type and features ----&gt;
#     [ ]   Randomize the address of the kernel image (KASLR) 

# Kernel hacking  ---&gt;
#     [*] Kernel debugging
#     Compile-time checks and compiler options  ---&gt;
#         [*] Compile the kernel with debug info
#         [*]   Provide GDB scripts for kernel debugging</pre>
<p>然后按正常流程构建。</p>
<p>&nbsp;</p>
<div class="blog_h1"><span class="graybg">阅读源码</span></div>
<div class="blog_h2"><span class="graybg">使用Clion</span></div>
<p>内核项目本身是基于make的，我们需要将其转换为CMake项目。</p>
<p>首先，安装scan-build</p>
<pre class="crayon-plain-tag">sudo -H pip install scan-build
# 或者
sudo apt install bear</pre>
<p>这是一个构建拦截工具（build interceptor） ，它能够拦截make的构建过程，并生成一个<pre class="crayon-plain-tag">compile_commands.json</pre>文件。这个文件叫<a href="https://clang.llvm.org/docs/JSONCompilationDatabase.html">JSON Compilation Database</a>，这种文件格式描述了如何独立于某种构建系统，来Replay编译过程。</p>
<p>拦截构建过程：</p>
<pre class="crayon-plain-tag">cd linux-stable
intercept-build make -j8
# 或者
bear make -j8

# 拦截其它目标
bear make tools/all
bear make bzImage</pre>
<p>从compile_commands.json生成CMakeList.txt文件：</p>
<pre class="crayon-plain-tag">cd ..
git clone https://github.com/habemus-papadum/kernel-grok.git
cd linux-stable
../kernel-grok/generate_cmake</pre>
<p>如果报头文件找不到的错误，在CMakeList.txt中增加：</p>
<pre class="crayon-plain-tag">set(CMAKE_C_COMPILER "gcc")
include_directories(".")
include_directories("./include")</pre>
<div class="blog_h2"><span class="graybg">使用VSCode</span></div>
<p>首先配置并编译好内核，然后在源码目录下执行：</p>
<pre class="crayon-plain-tag">git clone https://github.com/amezin/vscode-linux-kernel.git .vscode

# 生成compile_commands.json
python .vscode/generate_compdb.py
# 如果不是编译的x64内核，则修改c_cpp_properties.json的intelliSenseMode。可选值：
# gcc-x86    gcc-x64    gcc-arm    gcc-arm64</pre>
<p>&nbsp;</p>
</div><p>The post <a rel="nofollow" href="https://blog.gmem.cc/linux-kernel-programming-faq">Linux内核编程知识集锦</a> appeared first on <a rel="nofollow" href="https://blog.gmem.cc">绿色记忆</a>.</p>
]]></content:encoded>
			<wfw:commentRss>https://blog.gmem.cc/linux-kernel-programming-faq/feed</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>Linux进程间通信</title>
		<link>https://blog.gmem.cc/linux-ipc</link>
		<comments>https://blog.gmem.cc/linux-ipc#comments</comments>
		<pubDate>Thu, 03 Sep 2009 03:54:26 +0000</pubDate>
		<dc:creator><![CDATA[Alex]]></dc:creator>
				<category><![CDATA[C]]></category>
		<category><![CDATA[Linux]]></category>
		<category><![CDATA[Linux知识]]></category>
		<category><![CDATA[Linux编程]]></category>
		<category><![CDATA[系统编程]]></category>

		<guid isPermaLink="false">http://blog.gmem.cc/?p=6964</guid>
		<description><![CDATA[<p>管道 当从一个进程连接数据流到另外一个进程时，使用术语“管道”。通常是把一个进程的输出通过管道连接到另外一个进程的输入。Shell命令通过管道字符可以实现命令的连接： [crayon-69e74a6c80583535754604/] popen函数 Linux提供了类似的API，允许通过编程的方式，利用管道在两个程序之间传递数据。在两个程序之间进行数据传递的最简单方式是使用popen/pclose函数： [crayon-69e74a6c80587080659846/] 下面是一个简单的示例，执行uname命令并获取其输出： [crayon-69e74a6c8058a657395915/] pipe调用 在底层，Linux提供了pipe函数，通过该函数可以在两个进程之间传递数据，不需要启动Shell，该函数提供了对读写数据的更多控制： [crayon-69e74a6c8058c926519300/] 初看，pipe函数没有什么价值，但由于fork调用创建新进程时，默认原先打开的文件描述符仍然保持打开状态，因此，管道可以被父子进程共享，从而用来在其间进行数据传递，下面是pipe在父子进程之间使用的例子： [crayon-69e74a6c8058f126196396/] 上面的例子中，父子进程运行的是相同的程序，如果两个进程是完全不同的程序呢？通过exec()调用后，只需要将文件描述符传递给新进程就可以继续使用管道了，因为文件描述符本质上只是一个数字而已： [crayon-69e74a6c80592805528733/] 关于管道的使用，应当注意： 对于已经关闭写端的管道，对其指向read()调用不会阻塞，而会立即返回0。这与读取无效文件描述符不同，后者会返回-1 如果通过fork()调用使用管道，就会存在两个不同的文件描述符可以用来向管道写数据，一个在父进程中，一个在子进程中。只有在父子文件中均把针对管道的写描述符关闭，管道才认为是关闭的，对其进行read才会立即返回 将管道用作标准输入输出 通过管道连接两个进程，具有更加简洁的方法： [crayon-69e74a6c80594886644058/] 命名管道 <a class="read-more" href="https://blog.gmem.cc/linux-ipc">[...]</a></p>
<p>The post <a rel="nofollow" href="https://blog.gmem.cc/linux-ipc">Linux进程间通信</a> appeared first on <a rel="nofollow" href="https://blog.gmem.cc">绿色记忆</a>.</p>
]]></description>
				<content:encoded><![CDATA[<div class="wri_content_clear_both"><div class="blog_h2"><span class="graybg">管道</span></div>
<p>当<span style="background-color: #c0c0c0;">从一个进程连接数据流到另外一个进程</span>时，使用术语“管道”。通常是把一个进程的<span style="background-color: #c0c0c0;">输出通过管道连接到</span>另外一个进程的<span style="background-color: #c0c0c0;">输入</span>。Shell命令通过管道字符可以实现命令的连接：</p>
<pre class="crayon-plain-tag">cmd1 | cmd2

#cmd1的标准输入来自终端键盘
#cmd1的标准输出传递给cmd2，作为它的标准输入
#cmd2的标准输出连接到终端屏幕</pre>
<div class="blog_h3"><span class="graybg">popen函数</span></div>
<p>Linux提供了类似的API，允许通过编程的方式，利用管道在两个程序之间传递数据。在两个程序之间进行数据传递的最简单方式是使用popen/pclose函数：</p>
<pre class="crayon-plain-tag">#include &lt;stdio.h&gt;
/**
 * 允许将另外一个程序作为新进程启动，并可以传递数据或者接收数据
 * @param command 需要运行的程序和参数
 * @param open_mode 打开模式，必须是r或者w
 *     如果是r，被调用程序的输出可以被当前程序使用，通过返回的文件指针进行fread读取
 *     如果是w，当前程序可以通过fwrite向被调用程序发送数据，后者可以在stdin上读取这些数据
 */
FILE *popen( const char *command, const char *open_mode );
/**
 * 关闭文件指针，该函数只有在新进程结束后才会返回，否则会一直阻塞
 */
int pclose( FILE *stream_to_close );</pre>
<p>下面是一个简单的示例，执行uname命令并获取其输出：</p>
<pre class="crayon-plain-tag">#include &lt;unistd.h&gt;
#include &lt;stdlib.h&gt;
#include &lt;stdio.h&gt;
#include &lt;string.h&gt;
int main()
{
    char buffer[BUFSIZ + 1];
    int chars_read;
    memset( buffer, 0, sizeof ( buffer ) );
    FILE *read_fp = popen( "uname -a", "r" ); //创建新进程并读取其标准输出
    if ( read_fp != NULL )
    {
        //像读取文件一样，将新进程的标准输出读取到缓冲区
        chars_read = fread( buffer, sizeof(char), BUFSIZ, read_fp );
        if ( chars_read &gt; 0 )
        {
            printf( "Output: %s\n", buffer );
        }
        pclose( read_fp );
        exit( EXIT_SUCCESS );
    }
    exit( EXIT_FAILURE );
}</pre>
<div class="blog_h3"><span class="graybg">pipe调用</span></div>
<p>在底层，Linux提供了pipe函数，通过该函数可以在两个进程之间传递数据，不需要启动Shell，该函数提供了对读写数据的更多控制：</p>
<pre class="crayon-plain-tag">#include &lt;unistd.h&gt;
/**
 * 创建一个管道，该函数对入参数组填上两个新的文件描述符，然后返回0
 * 返回的两个文件描述符通过一种特殊的方式连接：依据FIFO原则，写入
 * file_descriptor[1]的数据，都可以从file_descriptor[0]中读取回来
 *
 * @param file_descriptor 长度为2的文件描述符数组
 * @return 如果成功返回0否则返回-1并设置errno：
 *     EMFILE：进程使用的文件描述符过多
 *     ENFILE：系统的文件表已满
 *     EFAULT：文件描述符无效
 */
int pipe( int file_descriptor[2] );</pre>
<p>初看，pipe函数没有什么价值，但由于fork调用创建新进程时，默认<span style="background-color: #c0c0c0;">原先打开的文件描述符仍然保持打开状态</span>，因此，管道可以被父子进程共享，从而用来在其间进行数据传递，下面是pipe在父子进程之间使用的例子：</p>
<pre class="crayon-plain-tag">#include &lt;unistd.h&gt;
#include &lt;stdlib.h&gt;
#include &lt;stdio.h&gt;
#include &lt;string.h&gt;
int main()
{
    int data_processed;
    int file_pipes[2];
    const char some_data[] = "MSG";
    char buffer[BUFSIZ + 1];
    pid_t fork_result;
    memset( buffer, '\0', sizeof ( buffer ) );
    if ( pipe( file_pipes ) == 0 ) //创建一个共享的管道
    {
        fork_result = fork(); //创建子进程
        if ( fork_result == -1 ) exit( EXIT_FAILURE );
        if ( fork_result == 0 )
        {
            //这里是子进程，从文件描述符0中读取数据
            data_processed = read( file_pipes[0], buffer, BUFSIZ );
            printf( "Read %d bytes: %s\n", data_processed, buffer );
            exit( EXIT_SUCCESS );
        }
        else
        {
            //这里是父进程，向文件描述符1中写入数据
            data_processed = write( file_pipes[1], some_data, strlen( some_data ) );
            printf( "Wrote %d bytes\n", data_processed );
        }
    }
    exit( EXIT_SUCCESS );
}</pre>
<p>上面的例子中，父子进程运行的是相同的程序，如果两个进程是完全不同的程序呢？通过exec()调用后，只需要将文件描述符传递给新进程就可以继续使用管道了，因为文件描述符本质上只是一个数字而已：</p>
<pre class="crayon-plain-tag">//将管道文件描述符保存到字符串中
sprintf(buffer, "%d", file_pipes[0]);
//传递给子进程
execl("command", buffer, (char *)0);
//读取参数为文件描述符
sscanf(argv[0], "%d", &amp;file_descriptor);
//从文件描述符中读取数据
data_processed = read( file_descriptor, buffer, BUFSIZ );</pre>
<p>关于管道的使用，应当注意：</p>
<ol>
<li>对于已经关闭写端的管道，对其指向read()调用不会阻塞，而会立即返回0。这与读取无效文件描述符不同，后者会返回-1</li>
<li>如果通过fork()调用使用管道，就会存在<span style="background-color: #c0c0c0;">两个不同的文件描述符可以用来向管道写数据</span>，一个在父进程中，一个在子进程中。只有在父子文件中均把针对管道的写描述符关闭，管道才认为是关闭的，对其进行read才会立即返回</li>
</ol>
<div class="blog_h3"><span class="graybg">将管道用作标准输入输出</span></div>
<p>通过管道连接两个进程，具有更加简洁的方法：</p>
<pre class="crayon-plain-tag">if ( fork_result == ( pid_t ) 0 ) // 子进程
{
    close( 0 ); //关闭标准输入
    //复制管道读，根据dup的特点，它将复制为最小数值的文件描述符，即作为标准输入
    dup( file_pipes[0] );
    //关闭管道中两个文件描述符
    close( file_pipes[0] );
    close( file_pipes[1] );
    execlp( "od", "od", "-c", ( char * ) 0 );
}</pre>
<div class="blog_h3"><span class="graybg">命名管道</span></div>
<p>要在两个不相关（不具备共同祖先）的进程之间传递数据，可以使用命名管道（Named pipe），<span style="background-color: #c0c0c0;">命名管道</span>又被称为<span style="background-color: #c0c0c0;">FIFO文件</span>。命名管道是一种特殊类型的文件，在文件系统中以文件名的形式存在，其行为却与前一节所述的管道类似。可以通过Shell命令<pre class="crayon-plain-tag">mknode</pre> 或者<pre class="crayon-plain-tag">mkfifo</pre> 来创建命名管道：</p>
<pre class="crayon-plain-tag">mkfifo /tmp/my_fifo
#尝试读取这个空白的命名管道
cat &lt; /tmp/my_fifo  #阻塞

#在另外一个终端尝试写入这个空白的命名管道
echo "Hello World" &gt; /tmp/my_fifo 
#第一个终端读取到内容并输出在屏幕上</pre>
<p>在程序中，可以使用以下两个调用：</p>
<pre class="crayon-plain-tag">#include &lt;sys/types.h&gt;
#include &lt;sys/stat.h&gt;
int mkfifo( const char *filename, mode_t mode );
int mknod(const char *filename, mode_t mode | S_IFIFO, (dev_t) 0);

//创建命名管道的例子：
int main()
{
    int res = mkfifo( "/tmp/my_fifo", 0777 ); //尝试创建777权限的文件，当然需要受到用户掩码umask的制约
    if ( res == 0 ) exit( EXIT_SUCCESS );
}</pre>
<p>通过open命令访问FIFO文件时，需要注意一个限制：不能以<pre class="crayon-plain-tag">O_RDWR</pre> 模式打开，因为FIFO只是为了<span style="background-color: #c0c0c0;">单向的传递数据</span>。如果以读写方式打开，进程将从管道中读取到自己写入的数据。如果确实需要双向的数据传输，可以使用一对命名管道。此外选项<pre class="crayon-plain-tag">O_NONBLOCK</pre> 也会影响对管道的读写请求的处理方式：</p>
<pre class="crayon-plain-tag">//调用一直阻塞，除非另外一个进程以写方式打开同一命名管道
open( const char *path, O_RDONLY );
//即使没有其它进程以写模式打开同一命名管道，调用也会成功并立即返回
open(const char *path, O_RDONLY | O_NONBLOCK);
//调用一直阻塞，除非另外一个进程以读方式打开同一命名管道
open( const char *path, O_WRONLY );
//调用会立即返回，但是如果没有另外一个进程以读模式打开命名管道，调用将返回-1，并且FIFO也不会被打开
open(const char *path, O_WRONLY | O_NONBLOCK);</pre>
<div class="blog_h2"><span class="graybg">信号量</span></div>
<p>与线程之间通信的信号量类似，Linux还提供了更通用的，可以在不同进程之间进行通信的信号量机制，这些信号量接口都是针对成组的通用信号量进行操作，而不是针对一个二进制信号量。</p>
<pre class="crayon-plain-tag">#include &lt;sys/sem.h&gt;
/**
 * 创建一个新的信号量，或者获取一个已经存在的信号量
 * @param key  一个整数，不相关的进程可以通过同一key来访问同一信号量
 *             特殊值IPC_PRIVATE表示创建一个只有当前进程才能看见的信号量
 * @param num_sems 需要的信号量的数目，一般为1
 * @param sem_flags 位或标记。低9位类似于文件权限；IPC_CREAT表示创建一个新的信号量；
 *                  IPC_EXCL | IPC_CREAT 表示确保获得一个新的、唯一的信号量，如果
 *                  信号量已经存在，会返回错误
 * @return 成功返回正整数，表示信号量的唯一标识（sem_id）；失败返回-1
 */
int semget( key_t key, int num_sems, int sem_flags );
/**
 * 用于改变信号量的值，这是一个原子操作
 * @param sem_id 信号量的唯一标识
 * @param sem_ops 指向一个结构的指针
 * @param num_sem_ops
 *
 */
struct sembuf
{
    short sem_num; //信号量的数量，除非需要使用一组信号量，否则取值0
    short sem_op;  //信号量在一次操作中需要改变的值，可以使用非1值来改变信号量
                   //通常只会用到两个值：-1表示P操作，表示等待信号量可用；+1表示V操作，表示发送信号量可用的信息
    short sem_flg; //通常设置为SEM_UNDO，它使得操作系统跟踪该信号量的修改情况
                   //如果进程没有释放持有的信号量就终止，操作系统会代为释放
};
int semop( int sem_id, struct sembuf *sem_ops, size_t num_sem_ops );
/**
 * 直接控制信号量信息
 * @param sem_id 信号量的唯一标识
 * @param sem_num 信号量的数量，除非需要使用一组信号量，否则取值0
 * @param command 需要指向的操作
 * @param semun 提供命令参数的联合体
 */
union semun
{
    int val;
    struct semid_ds *buf;
    unsigned short *array;
};
int semctl( int sem_id, int sem_num, int command, union semun semun );</pre>
<div class="blog_h2"><span class="graybg">共享内存</span></div>
<p>共享内存允许不相关的进程<span style="background-color: #c0c0c0;">访问同一块逻辑内存</span>。这是一种在进程之间传递数据的非常有效的方式，大部分实现都把共享内存安排为同一段物理内存。共享内存是进程地址空间中的一个特殊的范围。共享内存没有通过同步机制，因此需要使用其它同步机制来对共享内存的访问进行同步。</p>
<p>与共享内存相关的函数有：</p>
<pre class="crayon-plain-tag">#include &lt;cygwin/types.h&gt;
#include &lt;stddef.h&gt;

//#include &lt;sys/shm.h&gt;

/**
 * 创建共享内存
 * @param key 共享内存段的命名，特殊键IPC_PRIVATE表示创建进程私有的共享内存
 * @param size 共享内存的容量
 * @param shmflg 位或，包含9个代表访问权限的位，IPC_CREAT用于创建一个新的共享内存
 *               给此函数传递已经存在的key并不是错误，此时的IPC_CREAT会被忽略
 *
 * @return 如果成功，返回一个正整数，作为共享内存的标识符；否则返回-1
 */
int shmget( key_t key, size_t size, int shmflg );
/**
 * 第一次创建共享内存段时，它不能被任何进程访问。要启用对共享内存的访问，必须
 * 将其连接到一个进程的地址空间中，这通过shmat函数完成
 * @param shm_id 共享内存标识符
 * @param shm_addr 连接到当前进程的地址位置，通常设置为空指针，表示让系统选择，否则硬件依赖性太高
 * @param shmflg 标记位：SHM_RND与shm_addr联用，来控制连接地址；SHM_RDONLY 是共享内存对当前进程只读
 *
 * @return 如果成功，返回指向共享内存第一个字节的指针；否则返回-1
 */
void *shmat( int shm_id, const void *shm_addr, int shmflg );
/**
 * 将共享内存段从当前进程分离，该函数不会删除共享内存，只是使当前进程不再能访问它
 * @param shm_addr shmat的返回值
 */
int shmdt( const void *shm_addr );
/**
 * 控制共享内存
 * @param shm_id 共享内存标识符
 * @param cmd 采取的动作：
 *            IPC_STAT 将shmid_ds中的数据设置为共享内存的当前关联值
 *            IPC_SET 如果有足够权限，则把shmid_ds中的值设置到共享内存
 *            IPC_RMID 删除共享内存段
 * @param buf 指针，指向共享内存模式和访问权限的结构
 */
struct shmid_ds
{
    uid_t uid;
    uid_t gid;
    mode_t mode;
};
int shmctl( int shm_id, int cmd, struct shmid_ds *buf );</pre>
<div class="blog_h2"><span class="graybg">消息队列</span></div>
<p>消息队列类似于命名管道，但是不具有打开、关闭管道以及阻塞通信方面的复杂性。消息队列提供了一种从一个进程向另外一个进程发送数据块的机制，每个数据块被认为含有一个类型。下面是消息队列相关API：</p>
<pre class="crayon-plain-tag">MSGMAX
//单个消息的最大字节数
MSGMNB
//队列最大深度

#include &lt;sys/msg.h&gt;
/**
 * 创建和访问一个消息队列
 * @param key 队列的名字，IPC_PRIVATE用于创建私有队列
 * @param msgflg 标记位，包含9个权限位，IPC_CREAT必须与这些位或才能创建新的队列
 * @return 如果成功返回消息队列的标识符，否则返回-1
 */
int msgget( key_t key, int msgflg );
/**
 * 把消息放入到队列中
 * @param msqid 消息队列标识符
 * @param msg_ptr 待发送消息的指针，目标应当是一个结构，且第一个成员变量是long型，用于表示消息类型
 * @param msg_sz msg_ptr指向的消息的长度，不包括long型的消息类型的长度
 * @param msgflg 控制当队列满或者队列消息到达系统范围限制时的行为，位或
 *               IPC_NOWAIT：立即返回-1，不发送消息；如果该标记被清除，则发送进程挂起直到队列有空闲
 */
int msgsnd( int msqid, const void *msg_ptr, size_t msg_sz, int msgflg );
/**
 * 从队列里接收一个消息
 * @param msqid 消息队列标识符
 * @param msg_ptr 准备接收消息的指针
 * @param msg_sz msg_ptr指向消息的长度，不包括long型消息类型的长度
 * @param msgtype 消息类型，用于实现简单的优先级机制：如果小于0获取消息类型小于等于其绝对值的第一个消息
 *                如果为0获取第一个消息；如果大于0获取对应类型的第一个消息
 * @param msgflg 控制当队列为空时的行为，位或。IPC_NOWAIT类似msgsnd
 */
int msgrcv( int msqid, void *msg_ptr, size_t msg_sz, long int msgtype, int msgflg );
/**
 * 控制消息队列
 * @param msqid 消息队列标识符
 * @param cmd 命令。IPC_STAT，将消息队列关联值设置到buf；IPC_SET，将buf中的值设置到消息队列
 *            IPC_RMID，删除消息队列
 * @param buf 存放命令参数
 */
struct msqid_ds
{
    uid_t uid;
    uid_t gid;
    mode_t mode;
};
int msgctl( int msqid, int cmd, struct msqid_ds *buf );</pre>
<p>&nbsp;</p>
</div><p>The post <a rel="nofollow" href="https://blog.gmem.cc/linux-ipc">Linux进程间通信</a> appeared first on <a rel="nofollow" href="https://blog.gmem.cc">绿色记忆</a>.</p>
]]></content:encoded>
			<wfw:commentRss>https://blog.gmem.cc/linux-ipc/feed</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>Linux信号、进程和会话</title>
		<link>https://blog.gmem.cc/linux-signals-processes-and-sessions</link>
		<comments>https://blog.gmem.cc/linux-signals-processes-and-sessions#comments</comments>
		<pubDate>Mon, 10 Aug 2009 06:47:11 +0000</pubDate>
		<dc:creator><![CDATA[Alex]]></dc:creator>
				<category><![CDATA[C]]></category>
		<category><![CDATA[Linux]]></category>
		<category><![CDATA[Linux知识]]></category>
		<category><![CDATA[Linux编程]]></category>
		<category><![CDATA[并发编程]]></category>
		<category><![CDATA[系统编程]]></category>

		<guid isPermaLink="false">http://blog.gmem.cc/?p=5772</guid>
		<description><![CDATA[<p>进程 进程和信号是Linux操作环境的基础部分，控制着Linux和其它类UNIX系统的几乎所有活动。 UNIX标准对进程的定义：其中运行着一个或者多个线程的地址空间，以及这些线程需要的系统资源。Linux系统的进程是非常轻量级的。 进程基础知识 每个Linux进程包含以下部分： PID：进程的唯一表示，是范围从2~32768的正整数，数字回绕一圈后，重新从2开始计数（数字1被系统第一个进程init占用） 程序代码：以自动方式加载到内存，正常情况下Linux进程不能对用来存放程序代码的内存区域进行写操作，因而可以被多个进程安全的共享 函数库：系统函数库也可以被共享，不管多少进程调用printf，内存中只需要它的一份副本即可，这种做法类似于微软的DLL机制但更为复杂 进程数据：存放进程的全局变量 栈空间：进程有属于自己的栈空间，用于保存函数中的局部变量、控制函数的调用和返回 环境空间：包含专为该进程建立的环境变量 程序计数器：记录进程执行到的位置，即在执行线程中的位置。进程可以包含多个执行线程 Linux系统使用一个被称为进程表的结构来存放当前加载到内存的所有进程的信息。这些信息包括：进程ID、进程状态、进程命令字符串以及其它一些ps命令输出的信息。操作系统通过PID对进程进行管理，早起的UNIX系统只能同时运行256个进程。 进程状态 注意：这些状态也适用于线程。进程中的线程通常处于不同的状态。 通过ps的STAT列，可以查看进程的状态，其代码如下表：  STAT代码 说明  S 睡眠。通常是在等待某个事件的发生，如信号、输入、Time slot R <a class="read-more" href="https://blog.gmem.cc/linux-signals-processes-and-sessions">[...]</a></p>
<p>The post <a rel="nofollow" href="https://blog.gmem.cc/linux-signals-processes-and-sessions">Linux信号、进程和会话</a> appeared first on <a rel="nofollow" href="https://blog.gmem.cc">绿色记忆</a>.</p>
]]></description>
				<content:encoded><![CDATA[<div class="wri_content_clear_both"><div class="blog_h1"><span class="graybg">进程</span></div>
<p>进程和信号是Linux操作环境的基础部分，控制着Linux和其它类UNIX系统的几乎所有活动。</p>
<p>UNIX标准对进程的定义：其中运行着一个或者多个线程的<span style="background-color: #c0c0c0;">地址空间</span>，以及这些线程需要的<span style="background-color: #c0c0c0;">系统资源</span>。Linux系统的进程是<span style="background-color: #c0c0c0;">非常轻量级</span>的。</p>
<div class="blog_h2"><span class="graybg">进程基础知识</span></div>
<p>每个Linux进程包含以下部分：</p>
<ol>
<li>PID：进程的唯一表示，是范围从2~32768的正整数，数字回绕一圈后，重新从2开始计数（数字1被系统第一个进程init占用）</li>
<li>程序代码：以自动方式加载到内存，正常情况下Linux进程不能对用来存放程序代码的内存区域进行写操作，因而可以被多个进程安全的共享</li>
<li>函数库：系统函数库也可以被共享，不管多少进程调用printf，内存中只需要它的一份副本即可，这种做法类似于微软的DLL机制但更为复杂</li>
<li>进程数据：存放进程的全局变量</li>
<li>栈空间：进程有属于自己的栈空间，用于保存函数中的局部变量、控制函数的调用和返回</li>
<li>环境空间：包含专为该进程建立的环境变量</li>
<li>程序计数器：记录进程执行到的位置，即在执行线程中的位置。进程可以包含多个执行线程</li>
</ol>
<p>Linux系统使用一个被称为<span style="background-color: #c0c0c0;">进程表</span>的结构来存放当前加载到内存的<span style="background-color: #c0c0c0;">所有进程</span>的信息。这些信息包括：进程ID、进程状态、进程命令字符串以及其它一些ps命令输出的信息。操作系统通过PID对进程进行管理，早起的UNIX系统只能同时运行256个进程。</p>
<div class="blog_h3"><span class="graybg">进程状态</span></div>
<p>注意：<span style="background-color: #c0c0c0;">这些状态也适用于线程</span>。进程中的线程通常处于不同的状态。</p>
<p>通过ps的STAT列，可以查看进程的状态，其代码如下表：</p>
<table style="width: 100%;" border="1" cellspacing="0" cellpadding="5">
<thead>
<tr>
<td style="width: 150px; text-align: center;"> STAT代码</td>
<td style="text-align: center;">说明 </td>
</tr>
</thead>
<tbody>
<tr>
<td>S</td>
<td>睡眠。通常是在等待某个事件的发生，如信号、输入、Time slot</td>
</tr>
<tr>
<td>R</td>
<td>运行。严格来说应是“可运行”，即在运行队列中，处于正在执行或即将运行状态</td>
</tr>
<tr>
<td>D</td>
<td>
<p>不可中断的睡眠（Uninterruptible sleep）。通常是在等待输入或输出（网络、磁盘、其它外设的IO）完成</p>
<p>处于此状态的进程，无法处理信号（<span style="background-color: #c0c0c0;">无法被信号唤醒，只能被它所等待的东西唤醒，或超时，如果在睡眠前设置了超时的话</span>），即使是kill -9 也无法处理</p>
<p>不允许中断的原因是，保护系统数据一致，防止数据读取错误</p>
</td>
</tr>
<tr>
<td>T</td>
<td>
<p>停止。通常被Shell作业控制所停止，或者进程正处于调试器的控制下</p>
<p>在Terminal中键入Ctrl + Z会导致当前<span style="background-color: #c0c0c0;">前台进程停止，暂停运行</span>。<span style="background-color: #c0c0c0;">此时输入bg，则让该进程继续在后台运行</span></p>
</td>
</tr>
<tr>
<td>Z</td>
<td>
<p>死（Defunct）进程或者僵尸（Zombie）进程</p>
<p>子进程死亡后，处于僵尸状态，其父进程负责收集其退出码等信息并完全释放它</p>
</td>
</tr>
<tr>
<td>N</td>
<td>低优先级任务（nice）</td>
</tr>
<tr>
<td>W</td>
<td>分页。不适用于2.6+内核</td>
</tr>
<tr>
<td>s</td>
<td>进程是Session leader</td>
</tr>
<tr>
<td>+</td>
<td>进程属于前台进程组</td>
</tr>
<tr>
<td>l</td>
<td>进程是多线程的</td>
</tr>
<tr>
<td>&lt;</td>
<td>高优先级任务</td>
</tr>
</tbody>
</table>
<div class="blog_h3"><span class="graybg">系统进程</span></div>
<p>一般而言，每个Linux进程都是由另外一个被称为<span style="background-color: #c0c0c0;">父进程</span>的进程所启动的，前者相对的被称为<span style="background-color: #c0c0c0;">子进程</span>。在Linux系统启动时，它运行一个PID为1的init进程，可以把该进程看做操作系统的进程管理器，它是所有进程的祖先进程。</p>
<p>启动进程并等待其结束，是Linux中最基本的进程管理任务。应用程序可以通过fork、exec、wait等系统调用完成这些任务。</p>
<div class="blog_h3"><span class="graybg">进程调度</span></div>
<p>每个进程被分配以非常短暂的时间片，在时间片范围内进程代码被CPU执行，由于CPU非常快、时间片又非常短，你会感觉到多个程序同时运行的假象。Linux内核使用进程调度器来决定下一个时间片应该分配给哪个进程，其判断的依据是进程的优先级，优先级高的进程获得的时间片更多。在Linux中进程的运行时间<span style="background-color: #c0c0c0;">不可能超过分配给它的时间片</span>，Linux使用的是<span style="background-color: #c0c0c0;">抢占式处理</span>，因此进程的挂起、继续运行不需要彼此之间的协作。</p>
<div class="blog_h3"><span class="graybg">进程死亡</span></div>
<p>进程调用<pre class="crayon-plain-tag">exit()</pre> 系统调用可以让自身退出，进程占用的内存将被释放，并利用信号通知其父进程。</p>
<p>父进程可能先于子进程死亡，这种情况下，init进程可以成为子进程的养父。</p>
<p>子进程在死亡后，将处于僵尸状态。这种状态的进程不能被调度，并占据少量的系统资源，以保证其父进程可以访问其退出码等信息。父进程负责完全的释放子进程。</p>
<div class="blog_h3"><span class="graybg">进程和线程</span></div>
<p>进程包含：虚拟地址空间、打开的系统对象的描述符、安全上下文、进程标识符、环境变量、最小-最大工作集大小，以及最少一个线程 —— 主线程。</p>
<p>线程是进程内部的一个可调度的实体（执行路径），进程内的<span style="background-color: #c0c0c0;">所有线程共享地址空间、打开的描述符</span>。每个线程维护自己的<span style="color: #242729;">exception handlers、调度优先级、线程本地存储、线程标识符、线程上下文结构。线程可以具有自己的安全上下文。</span></p>
<p><span style="color: #242729;">线程上下文中包括的数据项：寄存器数据、内核栈、线程环境块、位于进程地址空间的用户栈。</span></p>
<div class="blog_h2"><span class="graybg">Linux进程相关API</span></div>
<div class="blog_h3"><span class="graybg">启动进程</span></div>
<p>可以在程序内部启动另外一个程序，从而创建新的进程，可以通过库函数system()完成：</p>
<pre class="crayon-plain-tag">#include &lt;stdlib.h&gt;
/**
 * 运行指定的命令并等待其完成
 * @param cmd 需要执行的命令
 * @return 如果无法启动Shell返回127，其它错误返回-1，否则返回所执行命令的退出码
 */
int system( const char *cmd );</pre>
<p>上述函数的重大缺点是必须等待子进程的完成，并且依赖于Shell，因此使用的不多。</p>
<div class="blog_h3"><span class="graybg">替换进程映像</span></div>
<p>exec系列函数用于<span style="background-color: #c0c0c0;">把当前进程替换为一个新的进程（新进程执行结束后不会返回原进程）</span>。 新的程序启动后，原有的程序就不再运行了：</p>
<pre class="crayon-plain-tag">#include &lt;unistd.h&gt;
char **environ; // 该全局变量可用来设置传递到新程序的环境变量

// ***p函数通过搜索PATH环境变量来查找新的可执行文件的路径，如果目标程序不在PATH中，则path参数应当使用绝对路径
// ***e函数支持通过envp数组指定环境变量

// 下面三个函数支持变长参数列表，此列表以一个空指针结束
int execl(const char *path, const char *arg0, ..., (char *)0);
int execlp(const char *file, const char *arg0, ..., (char *)0);
int execle(const char *path, const char *arg0, ..., (char *)0, char *const envp[]);

// 下面三个函数使用数组来表示参数列表
int execv( const char *path, char * const argv[] );
int execvp( const char *file, char * const argv[] );
int execve( const char *path, char * const argv[], char * const envp[] );

//举例：
char * const ps_argv[] = { "ps", "ax", 0 };
char * const ps_envp[] = { "PATH=/bin:/usr/bin", "TERM=console", 0 };
execl("/bin/ps", "ps", "ax", 0);
execlp("ps", "ps", "ax", 0);
execle("/bin/ps", "ps", "ax", 0, ps_envp);
execv("/bin/ps", ps_argv);
execvp("ps", ps_argv);
execve("/bin/ps", ps_argv, ps_envp);</pre>
<p>一般情况下，<span style="background-color: #c0c0c0;">exec函数是不会返回的，除非发生错误</span>，此时返回-1并设置errno。</p>
<p>exec启动的新进程保留了原进程的许多特性，特别是，原进程打开的文件描述符仍然有效，除非这些描述符的close on exec flag标记位被设置。任何在原进程中打开的目录流都会在新进程中被关闭。</p>
<div class="blog_h3"><span class="graybg">复制进程映像</span></div>
<p>系统调用fork允许创建以当前进程为模板，<span style="background-color: #c0c0c0;">复制</span>出一个新的进程。fork调用会在进程表中创建一个新的表项，其中很多属性都和原进程相同，包括所执行的代码。但<span style="background-color: #c0c0c0;">新进程具有自己的数据空间、环境、文件描述符</span>。fork函数的原型如下：</p>
<pre class="crayon-plain-tag">#include &lt;sys/types.h&gt;
#include &lt;unistd.h&gt;
pid_t fork( void );</pre>
<p>fork()函数<span style="background-color: #c0c0c0;">很巧妙，其具有“两次返回”的效果</span>，实质上对应了父子进程的不同执行路径。对于父进程，fork()返回子进程的ID；而对于子进程，fork()总是返回0。通过这一特点，可以判断当前执行的代码是父进程还是子进程：</p>
<pre class="crayon-plain-tag">pid_t new_pid;
new_pid = fork();
switch ( new_pid )
{
    case -1 :   /* 错误 */
        break;
    case 0 :    /* 子进程 */
        break;
    default :   /* 父进程 */
        break;
}</pre>
<p>结合fork、exec函数，创建新进程的条件就完备了。</p>
<div class="blog_h3"><span class="graybg">等待进程结束</span></div>
<p>当fork启动一个子进程后，子进程就有了自己的生命周期，并将独立运行，有时候，需要知道子进程何时结束，可以在父进程中用wait系统调用：</p>
<pre class="crayon-plain-tag">#include &lt;sys/types.h&gt;
#include &lt;sys/wait.h&gt;
/**
 * 暂停父进程的执行，直到其子进程结束
 * @param stat_loc 存放状态信息，用于了解子进程的退出状态（即子进程main函数的返回值或者exit函数的退出码）
 *     WIFEXITED(stat_val)     如果子进程正常结束，则取值非0
 *     WEXITSTATUS(stat_val)   如果WIFEXITED非0，取值子进程的退出码
 *     WIFSIGNALED(stat_val)   如果子进程死于未捕获的信号，则取值非0
 *     WTERMSIG(stat_val)      如果WIFSIGNALED非0，取值目标信号的代码
 *     WIFSTOPPED(stat_val)    如果子进程意外终止，取值非0
 *     WSTOPSIG(stat_val)      如果WIFSTOPPED非0，返回信号代码
 *
 * @return 子进程的PID
 */
pid_t wait( int *stat_loc );

/**
 * 等到某个特定的子进程结束
 * @param pid 子进程的PID，如果指定为-1将返回任一子进程的信息
 * @param stat_loc 如果不是空指针，则用来存放状态信息
 * @param option 用于定制waitpid的行为
 */
pid_t waitpid( pid_t pid, int *stat_loc, int options );

/**
 * 检查某个子进程是否结束，立即返回
 * 
 * @return 如果目标子进程没有结束或者意外终止，返回0；否则返回子进程PID
 *         如果函数调用失败，返回-1并设置errno
 */
waitpid(child_pid, (int *) 0, WNOHANG);</pre>
<div class="blog_h2"><span class="graybg">僵尸进程</span></div>
<p>使用fork创建的子进程终止时，其与父进程的关联还会保持，直到<span style="background-color: #c0c0c0;">父进程也正常终止或者父进程调用wait</span>才会结束。在此之前，虽然子进程已经无事可做，但是其在进程表中的项不会被删除，其退出码需要被保存，以备后续父进程的wait调用，这种进程被称为僵尸进程（Zombie，也称为死defunct进程）。</p>
<p>如果父进程<span style="background-color: #c0c0c0;">异常终止</span>，子进程自动把PID=1的进程（init）作为自己的父进程，这类僵尸进程会一直保存在进程表中直到init发现并释放它。</p>
<div class="blog_h1"><span class="graybg"><a id="session"></a>会话</span></div>
<p>在Linux中，会话（Session）通常是指Shell会话，即会话的概念和Shell是分不开的。所谓Shell是用户访问Linux系统的接口，是用户与内核之间的桥梁。</p>
<p>在大部分的Linux发行版中，BASH是默认的Shell实现。每当你：</p>
<ol>
<li>本地打开Terminal窗口</li>
<li>通过SSH远程登录</li>
</ol>
<p>时，一个关联的新会话会被自动创建。不管本地还是远程登录，<span style="background-color: #c0c0c0;">用户都会得到一个与终端（Terminal）相关联的（Shell）进程，该进程作为Session Leader</span>，会话的ID就是该进程的PID。</p>
<p>你也可以编程式的创建新的会话，调用<pre class="crayon-plain-tag">pid_t setsid(void) </pre> 函数可以让当前进程（不管它是否为Shell）作为Session Leader，创建新的会话。如果当前进程已经是Session Leader则会出错。新的会话中只有一个进程，并且它没有关联终端，因此你需要对其输入、输出进行重定向。</p>
<div class="blog_h2"><span class="graybg">会话的终止</span></div>
<p>当终端被挂断（Hangup），即：</p>
<ol>
<li>对于本地Terminal，窗口被关闭</li>
<li>对于SSH终端，网络连接被断开</li>
</ol>
<p>时，Session Leader会接收到SIGHUP信号而退出。在Session Leader退出前，它会<span style="background-color: #c0c0c0;">向所有子进程也发送SIGHUP信号</span>，通常会导致会话中所有进程都结束掉。要想让某个子进程超越Session的生命周期而存活，你可以：</p>
<ol>
<li>让Session Leader主动退出，一般来说就是在Shell里调用exit/logout等命令。默认情况（Shell选项huponexit=off）下，主动退出不会发送SIGHUP给子进程</li>
<li>守护进程化：即所谓Double fork技巧，让Shell的子进程fork出孙子进程，在孙子进程中执行程序逻辑，而子进程立即退出（孙子进程变成孤儿进程）。由于Shell<span style="background-color: #c0c0c0;">只会将SIGHUP发送给直接子进程</span>，孙子进程就不会受到影响了</li>
<li>使用<pre class="crayon-plain-tag">setsid()</pre> 系统调用，让某个子进程变成新的Session的Leader，自立门户</li>
</ol>
<div class="blog_h2"><span class="graybg">进程组</span></div>
<p>顾名思义，进程组（Process group）就是包含了1~N个进程的分组。进程组的主要作用是利于信号的分发——当信号发送给进程组时，其内部的所有进程都会接收到信号。</p>
<p>一个会话中可以包含1~N个进程组，进程组不允许跨会话迁移，进程没有资格创建属于其它会话的进程组，进程也不能加入属于其它会话的进程组。</p>
<div class="blog_h2"><span class="graybg">终端</span></div>
<p>一个Session有且只有一个<a href="/io-faq#terminal">终端</a>，该终端称为控制终端（controlling terminal），改变Session关联的终端这一操作，只能由Session Leader完成。</p>
<p>终端的生命周期可能：</p>
<ol>
<li>与Session相同，这类终端是随着会话而创建的伪终端</li>
<li>与系统相同，这类终端是随着系统启动的，<span style="background-color: #c0c0c0;">init进程在会话结束后重启getty来监听该终端</span></li>
</ol>
<div class="blog_h1"><span class="graybg">信号</span></div>
<div class="blog_h2"><span class="graybg">信号的工作机制</span></div>
<p>软中断信号（简称信号，signal）用来通知进程发生了异步事件（通常是某种错误）。信号是进程之间（包括用户进程之间、用户进程与内核进程之间）进行通信的一种简单方式，信号不会给目标进程发送任何数据。</p>
<p>使用信号并挂起程序（例如pause调用）是Linux程序设计的一个重要部分，这意味着程序不需要总是在执行，在一个无限循环中检查某个事件是否发生，相反，它可以等待事件的发生。这种机制对于只有一个CPU的多任务环境非常重要，进程共享一个处理器，繁忙的循环会对系统性能造成极大的影响。</p>
<div class="blog_h3"><span class="graybg">信号产生时机</span></div>
<p>尽管用户可以通过命令手工发出信号，但是内核是主要的信号发出者。以下场景下会目标进程会收到信号：</p>
<ol>
<li>检测到一个可能出现的硬件故障，如电源故障</li>
<li>程序出现异常行为，比如尝试访问进程外部内存空间、尝试写入只读内存区域</li>
<li>用户从终端向目标程序发出某些指令（ctrl + z、ctrl + c等），终端进程会接收到对应信号</li>
<li>进程的一个子进程终止时</li>
</ol>
<div class="blog_h3"><span class="graybg">信号如何发送给目标进程</span></div>
<p>在进程的进程表项（内核通过进程表对进程进行管理，每个进程在进程表中占有一项）中，有一个<span style="background-color: #c0c0c0;">信号域</span>，信号域的<span style="background-color: #c0c0c0;">每一个slot对应一种信号</span>，对于同一种信号，进程无法知道在处理前来过多少个。</p>
<p>内核通过设置进程的信号域对应slot，来给进程发送信号。</p>
<p>当内核将信号发送给正在睡眠的进程时：</p>
<ol>
<li>如果进程处于可中断的睡眠状态，则唤醒之</li>
<li>如果进程处于不可中断的睡眠状态：则仅仅设置slot</li>
</ol>
<div class="blog_h3"><span class="graybg">进程何时检查是否收到信号</span></div>
<p>进程在以下场景下检查自己是否收到信号：</p>
<ol>
<li>进程即将从内核态返回到用户态时：进程在内核态运行时，信号不起作用，直到其将要返回用户态时才进行处理，信号处理函数在进程上下文中进行</li>
<li>进程即将进入或离开一个适当的低调度优先级睡眠状态时</li>
</ol>
<div class="blog_h3"><span class="graybg">进程如何处理收到的信号</span></div>
<p>进程接收到信号后，可以：</p>
<ol>
<li>忽略该信号</li>
<li>捕获该信号：当前进程继续执行前，调用一个用户定义的函数。这种处理机制类似于中断处理程序</li>
<li>让内核执行与该信号相关的默认动作，大部分信号的默认动作导致进程终止</li>
</ol>
<div class="blog_h2"><span class="graybg">谁接收信号</span></div>
<p>根据POSIX标准，<span style="background-color: #c0c0c0;">信号为进程而产生，但是仅仅其中一个线程可以接收信号并处理</span>。至于哪个线程负责处理信号，取决于实现。</p>
<p>对于Linux：</p>
<ol>
<li>信号可以针对进程的整体而产生，例如kill命令产生的信号</li>
<li>信号可以针对特定线程，例如SIGSEGV、SIGFPE信号，或者pthread_kill命令产生的信号</li>
<li>对于针对进程整体的信号，任何没有被信号阻塞的线程都可以处理之。如果有多个这样的线程（has the signal unblocked），内核会随机的选择一个</li>
</ol>
<p>对于pthreads：每个线程具有独立的信号栈设置（signal stack settings），但是新线程总是从父线程拷贝此设置</p>
<div class="blog_h2"><span class="graybg">常见信号列表（Ubuntu 14）</span></div>
<table style="width: 100%;" border="1" cellspacing="0" cellpadding="5">
<thead>
<tr>
<td style="width: 80px; text-align: center;"> 信号</td>
<td style="width: 30px; text-align: center;"> No.</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td> SIGHUP</td>
<td>1</td>
<td>
<p>如果进程通过终端运行，而终端忽然关闭后，进程将收到该信号。HUP是hang up的简写</p>
<p>在终端被关闭时，交互式的Shell会重新发送SIGHUP信号给所有任务（Jobs），不管是运行中的还是挂起（Stopped）的。挂起的任务还会收到SIGCONT信号，确保它们会处理SIGHUP。要阻止向某个任务发送SIGHUP，可以对其调用disown命令</p>
<p>对于BASH等Shell，调用exit、logout时，是否向所有任务发送SIGHUP取决于Shell选项：<pre class="crayon-plain-tag">shopt | grep huponexit</pre> ，该选项默认值是off，即不发送</p>
</td>
</tr>
<tr>
<td> SIGINT</td>
<td>2 </td>
<td>进程被中断（<span style="background-color: #c0c0c0;">int</span>errupted），当通过终端按ctrl + c导致进程接收到该信号</td>
</tr>
<tr>
<td> SIGQUIT</td>
<td>3 </td>
<td>与SIGINT类似，只是该信号是由ctrl + \，该信号会在终结进程时生成core dump（核心转储，即进程的内存映像，可以后续分析）</td>
</tr>
<tr>
<td> SIGILL</td>
<td>4 </td>
<td>非法（<span style="background-color: #c0c0c0;">Ill</span>egal）指令。程序执行了CPU无法理解的机器码时将收到该信号</td>
</tr>
<tr>
<td> SIGTRAP</td>
<td>5 </td>
<td>主要用于调试和程序跟踪</td>
</tr>
<tr>
<td> SIGABRT</td>
<td>6 </td>
<td>程序调用abort()函数时触发，导致程序紧急停止 </td>
</tr>
<tr>
<td> SIGBUS</td>
<td>7 </td>
<td>尝试以错误的方式访问内存时触发</td>
</tr>
<tr>
<td> SIGFPE</td>
<td>8 </td>
<td>程序中出现浮点数异常（<span style="background-color: #c0c0c0;">f</span>loating <span style="background-color: #c0c0c0;">p</span>oint <span style="background-color: #c0c0c0;">e</span>xception）时触发</td>
</tr>
<tr>
<td> SIGKIL</td>
<td>9 </td>
<td>立即终止进程，该信号不能被忽略。可以由ctrl + c引发</td>
</tr>
<tr>
<td> SIGUSR1</td>
<td>10 </td>
<td>供编程人员使用</td>
</tr>
<tr>
<td> SIGSEGV</td>
<td>11 </td>
<td>段错误，无效内存段访问，尝试越界访问内存（不是分配给当前进程的内存）时触发</td>
</tr>
<tr>
<td> SIGUSR2</td>
<td>12 </td>
<td>供编程人员使用</td>
</tr>
<tr>
<td> SIGPIPE</td>
<td>13 </td>
<td>当进程通过管道机制，将信息输出给目标进程的输入时，目标进程挂掉，当前进程收到此信号</td>
</tr>
<tr>
<td> SIGALRM</td>
<td>14 </td>
<td>进程调用alarm()函数，定时器到期后，系统通过该信号提示进程</td>
</tr>
<tr>
<td> SIGTERM</td>
<td>15 </td>
<td>这是一个一般的、用于“礼貌的”终结进程的信号。与SIGKIL不同，该信号可能被阻塞、处理或者忽略</td>
</tr>
<tr>
<td> SIGCHLD</td>
<td>17 </td>
<td>进程先前通过fork() 创建了子进程，这些子进程中的一个或者多个挂掉时，父进程收到此信号 </td>
</tr>
<tr>
<td> SIGCONT</td>
<td>18 </td>
<td>可以使由SIGSTOP导致休眠的进程恢复 </td>
</tr>
<tr>
<td> SIGSTOP</td>
<td>19 </td>
<td>如果系统发送该信号给进程，进程的状态将被保存，并且不再获得CPU周期</td>
</tr>
<tr>
<td> SIGTSTP</td>
<td>20 </td>
<td>Terminal SToP。本质上与SIGSTOP一样，该信号由终端操作ctrl + z导致</td>
</tr>
<tr>
<td> SIGTTIN</td>
<td>21 </td>
<td>当后台运行的进程尝试从stdin读取数据时，系统发送该信号给它。目标进程的典型响应是进入暂停，一直到进入前台时，SIGCONT信号到达 </td>
</tr>
<tr>
<td> SIGTTOU</td>
<td>22 </td>
<td>类似于SIGTTIN，当后台进程尝试写数据到stdout时触发</td>
</tr>
<tr>
<td> SIGURG</td>
<td>23 </td>
<td>带外数据（out-of-band，OOB）到达时，使用网络连接的进程接收到该信号。带外数据不使用与普通数据相同的通道。对于TCP协议，由于没有所谓带外通道，是通过URG位实现的。带外数据通常是一些紧急的重要数据</td>
</tr>
<tr>
<td> SIGXCPU</td>
<td>24 </td>
<td>系统发送该信号到使用CPU到达限制的进程 </td>
</tr>
<tr>
<td> SIGXFSZ</td>
<td>25 </td>
<td>系统发送该信号到尝试创建超过尺寸限制的文件的进程</td>
</tr>
<tr>
<td> SIGVTALRM</td>
<td>26 </td>
<td>与SIGALRM类似，但不通过真实时间计时，而是通过目标进程使用的的CPU时间计时</td>
</tr>
<tr>
<td> SIGPROF</td>
<td>27 </td>
<td>与SIGVTALRM类似，但是计时除了目标进程使用的CPU时间，而包括为了目标进程服务的系统代码执行时间</td>
</tr>
<tr>
<td> SIGIO</td>
<td>29 </td>
<td>亦即SIGPOLL。当有输入等待进程处理，或者输出通道可以供进程写入时，系统给进程发出该信号</td>
</tr>
<tr>
<td> SIGPWR</td>
<td>30 </td>
<td>当切换到紧急备用电源时，进程接收到该信号</td>
</tr>
<tr>
<td> SIGSYS</td>
<td>31 </td>
<td>未使用</td>
</tr>
</tbody>
</table>
<div class="blog_h2"><span class="graybg">Linux信号相关API</span></div>
<div class="blog_h3"><span class="graybg">处理信号</span></div>
<p>通过signal可以设置信号的处理函数：</p>
<pre class="crayon-plain-tag">#include &lt;signal.h&gt;
/**
 * 设置信号的处理函数
 * @param sig 信号
 * @param func 处理回调函数
 * @return 返回先前的信号处理函数的指针，如果未定义信号处理函数返回SIG_ERR并设置errno为正数
 *         如果给出一个无效的信号，或者尝试处理不可捕获、不可忽略的信号（例如SIGKILL），则将errno设置为EINVAL
 */
typedef void (*signal_handler_t )( int );
signal_handler_t signal( int sig, signal_handler_t );

//两个特殊的信号处理函数：
// SIG_IGN 忽略信号
// SIG_DFL 恢复此信号的默认处理行为

#include &lt;unistd.h&gt;
/**
 * 导致当前进程暂停执行，直到接收到一个信号
 * 当暂停被一个信号中断时，返回-1并且设置errno为EINTR
 */
int pause( void );</pre>
<p>信号处理函数中调用某些函数是不安全的，例如printf，最好是在信号处理函数中设置一个标记，然后在主程序中检查标记再调用某些函数。下面是信号处理函数的例子：</p>
<pre class="crayon-plain-tag">#include &lt;signal.h&gt;
#include &lt;stdio.h&gt;
#include &lt;unistd.h&gt;
void ouch( int sig )
{
    printf( "OUCH! - I got signal %d\n", sig );
    //该信号处理函数恢复默认行为:停止程序
    signal( SIGINT, SIG_DFL );
}

int main()
{
    //进程启动后，设置信号的处理函数
    signal( SIGINT, ouch );
    while ( 1 )
    {
        //此循环会不停执行，除非接收到信号
        printf( "Hello World!\n" );
        sleep( 1 );
    }
}</pre>
<p>X/Open和UNIX规范推荐了更加健壮的信号编程接口：</p>
<pre class="crayon-plain-tag">#include &lt;signal.h&gt;
/**
 * 指定接收到sig信号后采取的动作
 * @param sig 处理的信号
 * @param act 需要指向的动作
 * @param oact 如果不为空，此函数调用前sig信号的处理动作被转储到该指针
 * @return 如果成功，返回0，失败返回-1，如果给出的信号无效或者对不允许忽略或者
 *         捕获的信号进行忽略或者捕获，则设置errno=EINVAL
 */
int sigaction( int sig, const struct sigaction *act, struct sigaction *oact );
typedef  void (*sa_handler_t)( int );
struct sigaction
{
    sa_handler_t sa_handler; //信号处理函数，包括SIG_DFL、SIG_IGN
    //一个信号集，在sa_handler被调用之前，此信号集中的信号不会传递给进程，
    //可以防止信号处理函数尚未执行完毕就接收到新信号并重入信号处理函数的情况
    //信号处理函数在指向过程中，可能被新的信号中断而再次调用，这不仅仅是递归调用
    //的问题，更牵涉到可重入（安全的进入和再次指向）的问题
    sigset_t sa_mask;
    //标记位：
    //SA_RESETHAND 表示处理函数调用后（入口第一句后），即重置默认处理函数（SIG_DFL）
    //SA_NOCLDSTOP 子进程停止后不产生SIGCHLD信号
    //SA_RESTART   重启可中断函数而不是给出EINTR错误。许多系统调用是可中断的，也就是
    //             说接收到信号后系统调用会返回一个错误并设置errno=EINTR
    //             以表示该调用因为信号而返回。设置该标记后，调用将重启而不是被信号中断
    //SA_NODEFER   捕获到信号时不将其加入到信号屏蔽掩码中。通常的做法：为防止同一信号不
    //             断到达，新接收到的信号会被加入到掩码中，直到处理函数指向完毕
    int sa_flags;
};

//向信号集中增加一个信号
int sigaddset( sigset_t *set, int signo );
//创建空白信号集
int sigemptyset( sigset_t *set );
//创建包含所有已定义信号的信号集
int sigfillset( sigset_t *set );
//从信号集中删除指定的信号
int sigdelset( sigset_t *set, int signo );
//判断信号是否为信号集的成员，如果是返回1否则返回0，如果给定的信号无效返回-1并设置EINVAL
int sigismember( sigset_t *set, int signo );
/**
 * 根据how指定的方式修改进程的信号屏蔽掩码
 * @param how
 *        SIG_BLOCK    将set加入到进程的掩码
 *        SIG_SETMASK  将进程的掩码设置为set
 *        SIG_UNBLOCK  从进程的掩码中删除set
 * @param set 新的信号屏蔽掩码，如果设置为空，仅仅是把当前信号屏蔽掩码保存到oset
 * @param oset 原先的信号屏蔽掩码
 * @return 如果成功返回0；如果how无效返回-1并设置errno=EINVAL
 *
 */
int sigprocmask( int how, const sigset_t *set, sigset_t *oset );

/**
 * 如果一个信号被进程阻塞，就不会传递给进程而停留在待处理状态。
 * 该函数可以查看阻塞的信号中那些处于待处理状态
 */
int sigpending( sigset_t *set );

/**
 * 挂起进程自己，等待信号集中某个信号到达
 * 如果接收到的信号终止了程序，该调用不会返回；否则返回-1并设置errno=EINTR
 */
int sigsuspend( const sigset_t *sigmask );</pre>
<p>下面是对前一个信号处理例子的改写：</p>
<pre class="crayon-plain-tag">#include &lt;signal.h&gt;
#include &lt;stdio.h&gt;
#include &lt;unistd.h&gt;
void ouch( int sig )
{
    printf( "OUCH! - I got signal %d\n", sig );
}
int main()
{
    struct sigaction act;
    act.sa_handler = ouch; //设置信号处理函数
    sigemptyset( &amp;act.sa_mask ); //创建空的信号屏蔽掩码
    act.sa_flags = SA_RESETHAND;
    sigaction( SIGINT, &amp;act, 0 );
    while ( 1 )
    {
        printf( "Hello World!\n" );
        sleep( 1 );
    }
}</pre>
<p>信号处理函数中会遇到可重入问题，下表列出可以被信号处理函数安全调用的函数，他们本身是可重入、或者本身不会再生成信号：<img class="size-full wp-image-6937 aligncenter" src="https://blog.gmem.cc/wp-content/uploads/2009/08/safe-func-in-sig-hdl.png" alt="safe-func-in-sig-hdl" width="98%" /></p>
<div class="blog_h3"><span class="graybg">发送信号</span></div>
<p>进程可以调用kill函数来向<span style="background-color: #c0c0c0;">包括其自身在内的进程</span>发送一个信号。如果进程没有发送目标信号的权限，对kill的调用就会失败，失败的常见原因是目标进程是由另外一个用户所拥有。下面的函数可以用来发送信号：</p>
<pre class="crayon-plain-tag">#include &lt;sys/types.h&gt;
#include &lt;signal.h&gt;
/**
 * 将信号发送给指定进程，如果成功返回0，失败返回-1并设置errno，errno可以为以下值：
 * EINVAL  给定的信号无效
 * EPERM   发送进程的权限不够，一般只能发送给同一用户的进程，超级用户可以发送信号给所有进程
 * ESRCH   目标进程不存在
 *
 */
int kill( pid_t pid, int sig );

#include &lt;unistd.h&gt;
/**
 * 在延迟seconds秒以后，发送一个SIGALRM信号
 * 每个进程只能有一个闹钟，因此后续的调用将导致重新计时并返回上一次闹钟设置的剩余秒数
 * 如果seconds设置为0则取消闹钟请求
 */
unsigned int alarm( unsigned int seconds );</pre>
</div><p>The post <a rel="nofollow" href="https://blog.gmem.cc/linux-signals-processes-and-sessions">Linux信号、进程和会话</a> appeared first on <a rel="nofollow" href="https://blog.gmem.cc">绿色记忆</a>.</p>
]]></content:encoded>
			<wfw:commentRss>https://blog.gmem.cc/linux-signals-processes-and-sessions/feed</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>Linux编程知识集锦</title>
		<link>https://blog.gmem.cc/linux-programming-faq</link>
		<comments>https://blog.gmem.cc/linux-programming-faq#comments</comments>
		<pubDate>Thu, 25 Jun 2009 05:39:47 +0000</pubDate>
		<dc:creator><![CDATA[Alex]]></dc:creator>
				<category><![CDATA[C]]></category>
		<category><![CDATA[Linux]]></category>
		<category><![CDATA[Linux编程]]></category>
		<category><![CDATA[系统编程]]></category>

		<guid isPermaLink="false">http://blog.gmem.cc/?p=6883</guid>
		<description><![CDATA[<p>库 共享对象 共享库（Shared libraries），在程序执行起始时被自动加载（而非执行过程中随时动态加载）。在链接阶段，必须有共享库才能链接。 soname 使用共享库时，运行时加载的库，应当与链接时期望的库的“版本”一致，即功能上没有不兼容的变化。二进制文件（库、可执行文件）本身知道其依赖的共享库的版本。 如何识别这种变化并没有一致的规范，某些UNIX系统通过soname来版本化共享库，所谓soname就是在共享库名称后附加可选的数字后缀，例如libx.so.1。仅当共享库的接口发生不兼容变化时soname才改变，如果libx从1.0到1.9维持了一致性的接口，它们的soname应该一致。注意soname和文件名不是一回事，1.3版本的libx的文件名可能叫libx.so.1.3，但是它的soname可能是libx.so.1。 通过soname，可以很容易的允许不同版本的共享库存在于同一个系统中（相比Windows的DLL地狱）。不带后缀的example.so常常是指向最新版本的符号链接。不带后缀的so用于链接器，因为gcc不知道去寻找有后缀版本，例如-lpthread只能让gcc去寻找libpthread.so。 soname可以具有主次版本号区分，使用额外的点号分隔。例如：[crayon-69e74a6c8175d789422031-i/]，MAJOR表示ABI不再向后兼容，MINOR表示ABI仍然向后兼容，BUGFIX意味着属于内部改变，ABI不发生变化。 当安装libx库到系统时，可以建立符号链接[crayon-69e74a6c81761241865684-i/] ，当libx出现不兼容升级时，则需要修改前述符号链接，例如[crayon-69e74a6c81763701214822-i/] 。这样，新的程序总是和最新的libx版本进行链接。 另一方面，这种符号链接用法可以扩展为，将某个soname链接到特定文件，例如[crayon-69e74a6c81765104571708-i/] ，如果1.3版本有一个BUG在1.3.2中修复，可以修改前述符号链接为[crayon-69e74a6c81767198063720-i/] ，这样基于libx.so.1链接的可执行文件在获得BUG修复的同时，能够找到正确的共享库。 动态加载库 动态加载库（Dynamically loaded libraries），类似于Windows的DLL，运行期间，使用到的时候进行加载。编译链接都不需要目标库文件。 和DLL的区别 DLL中包含两类函数：导出（exported）函数、内部（internal）函数，只有导出函数可以被外部模块使用。SO中的所有函数均可以被外部使用，不需要特殊的export语句导出函数（在大部分UNIX系统中，默认所有符号被导出）。 静态库 静态库（Static libraries），简单的把一些obj文件打包为.a归档文件。优势是执行速度通常更快，而且是静态链接，因此不会出现找不到库、版本错误导致无法执行的情况。缺点是难以维护，库出现BUG则所有依赖都得重现链接 有时，如果依赖库声明的顺序不正确会导致：undefined reference错误。对于静态链接库libA，如果它依赖于链接库libB，那么，在Linker的参数列表中，libA必须在libB前面出现。动态链接库则无此顺序要求。 <a class="read-more" href="https://blog.gmem.cc/linux-programming-faq">[...]</a></p>
<p>The post <a rel="nofollow" href="https://blog.gmem.cc/linux-programming-faq">Linux编程知识集锦</a> appeared first on <a rel="nofollow" href="https://blog.gmem.cc">绿色记忆</a>.</p>
]]></description>
				<content:encoded><![CDATA[<div class="wri_content_clear_both"><div class="blog_h1"><span class="graybg">库</span></div>
<div class="blog_h2"><span class="graybg">共享对象</span></div>
<p>共享库（Shared libraries），在<span style="background-color: #c0c0c0;">程序执行起始时被自动加载（而非执行过程中随时动态加载）</span>。在<span style="background-color: #c0c0c0;">链接阶段，必须有共享库才能链接</span>。</p>
<div class="blog_h3"><span class="graybg"><a id="soname"></a>soname</span></div>
<p>使用共享库时，<span style="background-color: #c0c0c0;">运行时加载的库，应当与链接时期望的库的“版本”一致</span>，即功能上没有不兼容的变化。二进制文件（库、可执行文件）本身知道其依赖的共享库的版本。</p>
<p>如何识别这种变化并没有一致的规范，<span style="background-color: #c0c0c0;">某些UNIX系统通过<strong>soname</strong>来版本化共享库</span>，所谓soname就是在共享库名称后附加可选的数字后缀，例如libx.so.1。仅当共享库的接口发生不兼容变化时soname才改变，如果libx从1.0到1.9维持了一致性的接口，它们的soname应该一致。注意soname和文件名不是一回事，1.3版本的libx的文件名可能叫libx.so.1.3，但是它的soname可能是libx.so.1。</p>
<p>通过soname，可以很容易的允许不同版本的共享库存在于同一个系统中（相比Windows的DLL地狱）。<span style="background-color: #c0c0c0;">不带后缀的example.so常常是指向最新版本的符号链接</span>。不带后缀的so用于链接器，因<span style="background-color: #c0c0c0;">为gcc不知道去寻找有后缀版本</span>，例如-lpthread只能让gcc去寻找libpthread.so。</p>
<p>soname可以具有主次版本号区分，使用额外的点号分隔。例如：<pre class="crayon-plain-tag">libFOO.so.MAJOR.MINOR.BUGFIX</pre>，MAJOR表示ABI不再向后兼容，MINOR表示ABI仍然向后兼容，BUGFIX意味着属于内部改变，ABI不发生变化。</p>
<p>当安装libx库到系统时，可以建立符号链接<pre class="crayon-plain-tag">libx.so -&gt; libx.so.1</pre> ，当libx出现不兼容升级时，则需要修改前述符号链接，例如<pre class="crayon-plain-tag">libx.so -&gt; libx.so.2</pre> 。这样，新的程序总是和最新的libx版本进行链接。</p>
<p>另一方面，这种符号链接用法可以扩展为，将某个soname链接到特定文件，例如<pre class="crayon-plain-tag">libx.so.1 -&gt; libx.so.1.3</pre> ，如果1.3版本有一个BUG在1.3.2中修复，可以修改前述符号链接为<pre class="crayon-plain-tag">libx.so.1 -&gt; libx.so.1.3.2</pre> ，这样基于libx.so.1链接的可执行文件在获得BUG修复的同时，能够找到正确的共享库。</p>
<div class="blog_h2"><span class="graybg">动态加载库</span></div>
<p>动态加载库（Dynamically loaded libraries），类似于Windows的DLL，运行期间，使用到的时候进行加载。编译链接都不需要目标库文件。</p>
<div class="blog_h3"><span class="graybg">和DLL的区别</span></div>
<p>DLL中包含两类函数：导出（exported）函数、内部（internal）函数，只有导出函数可以被外部模块使用。SO中的所有函数均可以被外部使用，不需要特殊的export语句导出函数（在大部分UNIX系统中，默认所有符号被导出）。</p>
<div class="blog_h2"><span class="graybg">静态库</span></div>
<p>静态库（Static libraries），简单的<span style="background-color: #c0c0c0;">把一些obj文件打包为.a归档文件</span>。优势是执行速度通常更快，而且是静态链接，因此不会出现找不到库、版本错误导致无法执行的情况。缺点是难以维护，库出现BUG则所有依赖<span style="background-color: #c0c0c0;">都得重现链接</span></p>
<p>有时，如果依赖库声明的顺序不正确会导致：undefined reference错误。对于静态链接库<span style="background-color: #c0c0c0;">libA</span>，如果它<span style="background-color: #c0c0c0;">依赖于链接库libB</span>，那么，在Linker的参数列表中，<span style="background-color: #c0c0c0;">libA必须在libB前面出现</span>。动态链接库则无此顺序要求。</p>
<div class="blog_h2"><span class="graybg">C语言库</span></div>
<div class="blog_h3"><span class="graybg">C标准库</span></div>
<p>属于C编程语言的一部分，它是一个规范（Specification），它定义了若干函数，以及这些函数应该具有什么样的行为。</p>
<p>C标准库的更多内容参考<a href="/c-study-note#stdc">C语言学习笔记</a>。主要头文件包括assert.h ctype.h errno.h float.h limits.h locale.h math.h setjmp.h signal.h stdarg.h stddef.h stdio.h stdlib.h string.h wchar.h time.h</p>
<div class="blog_h3"><span class="graybg">POSIX C库</span></div>
<p>某种程度上是C标准库的超集 dlopen, fork等函数由POSIX库定义。</p>
<div class="blog_h3"><span class="graybg">GNU C库</span></div>
<p>GNU的glibc库是C标准库、POSIX C库的超集，在Linux下通常作为共享对象位于/lib/x86_64-linux-gnu/libc.so...</p>
<p>glibc使用C语言的GCC方言编写，是Linux系统的基石。glibc简化了你和系统调用的交互（进行了函数封装）。不使用glibc的情况下，你可以直接写汇编代码来执行系统调用。如果仅仅需要使用C标准库的函数，可以使用musl-libc或者dietlibc等。 </p>
<p>libanl libc libc_nonshared libcrypt libdl libg libmvec libpthread libresolv librt libutil等都来自glibc库。</p>
<div class="blog_h1"><span class="graybg">GNU</span></div>
<div class="blog_h2"><span class="graybg">简介</span></div>
<p>操作系统内核仅仅是Linux的一小部分，而大部分应用程序由Linux社区开发。Linux社区支持自由软件的概念，遵从GNU（GNU's not UNIX）通用许可证（GPL）。</p>
<p>GNU项目由Richard Stallman创立，目的是创建一个与UNIX系统兼容，但不受UNIX名字、源代码私有权限制的OS和开发环境。下面列出一些著名的GNU软件：</p>
<table style="width: 100%;" border="1" cellspacing="0" cellpadding="5">
<thead>
<tr>
<td style="width: 150px; text-align: center;"> 软件</td>
<td style="text-align: center;">说明 </td>
</tr>
</thead>
<tbody>
<tr>
<td>GCC</td>
<td>GNU编辑器套件，包括GNU C编译器</td>
</tr>
<tr>
<td>G++ </td>
<td>C++编译器，GCC的一部分 </td>
</tr>
<tr>
<td>GDB</td>
<td>源代码级调试器</td>
</tr>
<tr>
<td>GNU make</td>
<td>UNIX make的免费版本</td>
</tr>
<tr>
<td>Autotools</td>
<td>包含Autoconf、Automake、Libtool、Gettext等组件的自动化构建套件</td>
</tr>
<tr>
<td>Bison</td>
<td>与UNIX yacc兼容的语法分析程序生成器</td>
</tr>
<tr>
<td>bash</td>
<td>命令解释器</td>
</tr>
<tr>
<td>GNU Emacs</td>
<td>文本编辑器</td>
</tr>
</tbody>
</table>
<p>除上表以外，还有大量软件，例如电子表格、源代码管理工具、编译器、因特网工具、桌面环境（GNOME、KDE），都是在GPL条款下发布的。由于GNU软件的重要贡献，现在很多人把Linux称为GNU/Linux。</p>
<p>关于GNU的软件的更多内容：</p>
<ol>
<li>GCC / GDB：将在本文后续内容中详细介绍</li>
<li>Make可以参考：<a href="/gnu-make-study-note">GNU Make学习笔记</a></li>
<li>Autotools可以参考：<a href="/autotools-study-note">Autotools学习笔记</a></li>
</ol>
<div class="blog_h1"><span class="graybg">GCC</span></div>
<div class="blog_h2"><span class="graybg">简介</span></div>
<p>GCC是GNU项目的C/C++编译器。</p>
<p>当调用gcc时，正常是执行从<span style="background-color: #c0c0c0;">预处理、汇编、编译、链接的整个过程</span>，通过某些选项可以在某个步骤中断此处理过程。例如：<pre class="crayon-plain-tag">-c</pre>表示不运行链接，只输出汇编器的目标文件。</p>
<p>以<pre class="crayon-plain-tag">-f</pre>、<pre class="crayon-plain-tag">-W</pre>开头的选项（例如-fmove-loop-invariants、-Wformat），通常包含正/反作用的一对选项， -ffoo的反选项是-fno-foo。</p>
<p>C++程序可以使用g++命令来编译。g++是gcc的一个前端应用</p>
<div class="blog_h2"><span class="graybg">环境变量</span></div>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 35%; text-align: center;">变量</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>LIBRARY_PATH</td>
<td>编译期间，GCC基于此环境变量来定位（静态）库所在的目录</td>
</tr>
<tr>
<td>LD_LIBRARY_PATH</td>
<td>
<p>链接、运行时需要的共享库文件的位置，通常/lib、 /usr/lib默认可用</p>
<ol>
<li>链接时：可以通过此环境变量指定目标共享库的位置</li>
<li>运行时：基于此环境变量寻找共享库</li>
</ol>
<p>可执行程序或者库所依赖的其它共享库，可以通过<pre class="crayon-plain-tag">ldd</pre>获知</p>
<p>在<pre class="crayon-plain-tag">/etc/ld.so.conf</pre>文件中也可以列出共享库的位置</p>
</td>
</tr>
<tr>
<td>PKG_CONFIG_PATH</td>
<td><pre class="crayon-plain-tag">pkg-config</pre>命令搜索<pre class="crayon-plain-tag">.pc</pre>文件的路径</td>
</tr>
<tr>
<td>CPATH</td>
<td>C/C++头文件搜索位置</td>
</tr>
<tr>
<td>C_INCLUDE_PATH</td>
<td>C头文件搜索位置</td>
</tr>
<tr>
<td>CPLUS_INCLUDE_PATH</td>
<td>C++头文件搜索位置C++头文件搜索位置</td>
</tr>
</tbody>
</table>
<div class="blog_h2"><span class="graybg">安装</span></div>
<pre class="crayon-plain-tag">cd ~/CPP/lib/gcc/7.2/src
contrib/download_prerequisites
mkdir build &amp;&amp; cd build
../configure -v --build=x86_64-linux-gnu --host=x86_64-linux-gnu --target=x86_64-linux-gnu 
                --prefix=/home/alex/CPP/lib/gcc/7.2.0 
                --enable-languages=c,c++ --disable-multilib 
                --program-suffix=-7.1   # 程序名字后缀

# 构建
make -j 8
# 安装
make install </pre>
<div class="blog_h2"><span class="graybg"><a id="gcc"></a>gcc命令</span></div>
<div class="blog_h3"><span class="graybg">格式</span></div>
<pre class="crayon-plain-tag"># 下面列出最常用的选项，g++基本上接受与gcc同样的选项
gcc [-c|-S|-E] [-std=standard]
    [-g] [-pg] [-Olevel]
    [-Wwarn...] [-Wpedantic]
    [-Idir...] [-Ldir...]
    [-Dmacro[=defn]...] [-Umacro]
    [-foption...] [-mmachine-option...]
    [-o outfile] [@file] infile...</pre>
<div class="blog_h3"><span class="graybg">选项</span></div>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 20%; text-align: center;">选项</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td colspan="2"><strong><em>控制输出</em></strong></td>
</tr>
<tr>
<td>-E</td>
<td>仅执行编译预处理</td>
</tr>
<tr>
<td>-S</td>
<td>将C代码转换为汇编代码</td>
</tr>
<tr>
<td>-c</td>
<td>仅执行编译操作，不进行链接操作</td>
</tr>
<tr>
<td>-o</td>
<td>指定生成的输出文件文件</td>
</tr>
<tr>
<td colspan="2"><strong><em>C方言控制</em></strong></td>
</tr>
<tr>
<td>-ansi</td>
<td>在C模式下，等价于 <pre class="crayon-plain-tag">-std=c90</pre>。在C++模式下，等价于<pre class="crayon-plain-tag"> -std=c++98</pre></td>
</tr>
<tr>
<td>-std</td>
<td>
<p>控制使用的方言：</p>
<ol>
<li><pre class="crayon-plain-tag">c90</pre> 在C模式下等价于-ansi，在C++模式下等价于-std=c++98</li>
<li><pre class="crayon-plain-tag">c99</pre> ISO C99标准</li>
<li><pre class="crayon-plain-tag">c11</pre> ISO C11标准</li>
<li><pre class="crayon-plain-tag">gnu90</pre> ISO C90的GNU方言，包含了一些C99特性</li>
<li><pre class="crayon-plain-tag">gnu99</pre> <pre class="crayon-plain-tag">gnu11</pre> 其它GNU的C方言</li>
<li><pre class="crayon-plain-tag">c++98</pre> <pre class="crayon-plain-tag">c++03</pre> ISO C++标准，以及2003技术报告</li>
<li><pre class="crayon-plain-tag">c++11</pre> ISO C++ 2011修正案 </li>
<li><pre class="crayon-plain-tag">gnu++98</pre> <pre class="crayon-plain-tag">gnu++03</pre> <pre class="crayon-plain-tag">gnu++11</pre> GNU的C++方言</li>
</ol>
</td>
</tr>
<tr>
<td colspan="2"><strong><em>启用/抑制警告</em></strong></td>
</tr>
<tr>
<td>-w </td>
<td>抑制所有警告信息</td>
</tr>
<tr>
<td>-Wall</td>
<td>显示所有警告信息</td>
</tr>
<tr>
<td>-Wno-unused-parameter</td>
<td>忽略未使用的变量</td>
</tr>
<tr>
<td>-Werror </td>
<td>在出现警告的地方强制停止编译</td>
</tr>
<tr>
<td>-Werror=</td>
<td>指定哪些警告被看作错误</td>
</tr>
<tr>
<td colspan="2"><strong><em>调试选项</em></strong></td>
</tr>
<tr>
<td>-g</td>
<td>
<p>使用操作系统本地格式来生成调试信息，可供GDB使用，可以指定0-3</p>
<ol>
<li><pre class="crayon-plain-tag">g0</pre> 无任何调试信息</li>
<li><pre class="crayon-plain-tag">g1</pre> 最小化信息，可以支持栈回溯，信息包括函数描述、外部变量描述、行号表，但是没有本地变量信息</li>
<li><pre class="crayon-plain-tag">g2</pre> 默认级别</li>
<li><pre class="crayon-plain-tag">g3</pre> 最大化调试信息，包括宏定义</li>
<li><pre class="crayon-plain-tag">-ggdb</pre> 生成GDB所需的调试信息</li>
<li><pre class="crayon-plain-tag">-gdwarf</pre> 如果支持的话，以DWARF格式产生调试信息</li>
</ol>
</td>
</tr>
<tr>
<td colspan="2"><strong><em>优化选项</em></strong></td>
</tr>
<tr>
<td>-O</td>
<td>
<p>指定编译优化级别，0-3，级别越大优化效果越好，编译时间越长</p>
<ol>
<li><pre class="crayon-plain-tag">O0</pre> 默认，编译时间-，执行时间+</li>
<li><pre class="crayon-plain-tag">O1</pre> 优化代码尺寸-和执行时间- ，内存消耗+，编译耗时+    导致帧指针丢失，无法进行栈backtrace，用<pre class="crayon-plain-tag">-fno-omit-frame-pointer</pre> 解决此问题</li>
<li><pre class="crayon-plain-tag">O2</pre> 进一步优化执行时间--，编译耗时++</li>
<li><pre class="crayon-plain-tag">O3</pre> 进一步优化执行时间---，编译耗时+++</li>
<li><pre class="crayon-plain-tag">Os</pre> 优化代码尺寸--，编译耗时++</li>
<li><pre class="crayon-plain-tag">Ofast O3</pre>外加非精确快速数学运算，执行时间---，编译耗时+++</li>
</ol>
</td>
</tr>
<tr>
<td colspan="2"><strong><em>预处理选项</em></strong></td>
</tr>
<tr>
<td>-D</td>
<td>定义宏，name或者name=definition，默认definition为1</td>
</tr>
<tr>
<td>-U</td>
<td>取消宏定义</td>
</tr>
<tr>
<td>-I</td>
<td>指定头文件的查找目录，<pre class="crayon-plain-tag">/usr/include</pre>自动寻找</td>
</tr>
<tr>
<td>-include</td>
<td>用来指定头文件，很少使用</td>
</tr>
<tr>
<td>-nostdinc</td>
<td>不去搜索系统的标准C头文件位置</td>
</tr>
<tr>
<td>-nostdinc++</td>
<td>不去搜索系统的标准C++头文件位置</td>
</tr>
<tr>
<td colspan="2"><strong><em>连接选项</em></strong></td>
</tr>
<tr>
<td>-L</td>
<td>指定寻找链接库的目录，<pre class="crayon-plain-tag">/lib /usr/lib</pre> <pre class="crayon-plain-tag">/usr/local/lib</pre>自动寻找</td>
</tr>
<tr>
<td>-l</td>
<td>
<p>指定需要链接的库的名称，<span style="background-color: #c0c0c0;">必须去掉前后缀，并且不留空格</span>，例如<pre class="crayon-plain-tag">-lm</pre>。<span style="background-color: #c0c0c0;">链接Windows下的库时，只需要去掉后缀</span></p>
</td>
</tr>
<tr>
<td>-Wl,-rpath-link</td>
<td>
<p>当使用ELF时，一个共享库可能依赖于另外一个。当ld -shared并且将另外一个共享库作为输入的时候就会发生</p>
<p>该选项指定查找依赖的共享库时，优先寻找的一系列目录（冒号分隔）</p>
</td>
</tr>
<tr>
<td>-Wl,-rpath</td>
<td>
<p>修改runtime linker 搜索共享库的路径，优先级高于LD_LIBRARY_PATH</p>
<p>RPATH是写入到ELF文件中的信息：</p>
<pre class="crayon-plain-tag">ldd cstudy
	linux-vdso.so.1 =&gt;  (0x00007fff56be3000)
    # 使用非标准的libc
	libc.so.6 =&gt; /home/alex/.local/lib/libc.so.6 (0x00007ffa34fee000)
	/home/alex/.local/lib/ld-linux-x86-64.so.2 =&gt; /lib64/ld-linux-x86-64.so.2 (0x00007ffa353a3000)</pre>
</td>
</tr>
<tr>
<td>-Wl,--dynamic-linker</td>
<td>
<p>使用指定的连接器：
<pre class="crayon-plain-tag"># 使用自定义的glibc
-Wl,-rpath=/home/alex/.local/lib 
-Wl,--dynamic-linker=/home/alex/.local/lib/ld-linux-x86-64.so.2 </pre>
</td>
</tr>
<tr>
<td>
<p>-shared 
</td>
<td>可以用于编译出共享库</td>
</tr>
<tr>
<td>-static</td>
<td>默认情况下，链接时GCC优先使用共享库。该选项<span style="background-color: #c0c0c0;">强制链接到静态库</span></td>
</tr>
<tr>
<td>-fmessage-length=n</td>
<td>格式化错误信息，最长n字符。对于g++默认72，；其它gcc前端0，即不换行</td>
</tr>
<tr>
<td>-Wl,option</td>
<td><span style="background-color: #c0c0c0;">向连接器传递选项，多个选项使用逗号分隔</span></td>
</tr>
<tr>
<td>-Wl,-soname=new_soname</td>
<td>设置生成的共享库的soname，等价于-Wl,-hnew_soname</td>
</tr>
<tr>
<td>-Wl,--out-implib=</td>
<td>生成的导入库的名称</td>
</tr>
<tr>
<td>-Wl,--output-def=</td>
<td>生成的DEF文件的名称</td>
</tr>
<tr>
<td>-Wl,--dynamic-linker</td>
<td>指定使用的连接器</td>
</tr>
<tr>
<td>-nostartfiles</td>
<td>连接时不使用标准的系统startup文件。标准库文件仍然被使用，除非使用 -nostdlib 或 -nodefaultlibs</td>
</tr>
<tr>
<td>-nodefaultlibs</td>
<td>连接时不使用标准系统库</td>
</tr>
<tr>
<td>-nostdlib</td>
<td>连接时不使用标准的系统startup文件、库文件</td>
</tr>
<tr>
<td colspan="2"><strong><em>未归类选项</em></strong></td>
</tr>
<tr>
<td>@file</td>
<td>从文件中读取命令行选项</td>
</tr>
<tr>
<td>outfile</td>
<td>命令输出文件</td>
</tr>
<tr>
<td>infile</td>
<td>命令输入文件的列表</td>
</tr>
<tr>
<td>-fpic</td>
<td>
<p>作用于编译阶段，告诉编译器<span style="background-color: #c0c0c0;">产生与位置无关代码（PIC）</span>，生成动态库时应该启用该选项，启用该选项后，所有代码通过全局偏移表（GOT）访问地址常量。GOT的条目将由操作系统的动态加载器负责解析。如果GOT超过机器限制的大小，你会得到错误提示，此时可以使用-fPIC 代替</p>
<p>PIC需要机器/OS的支持：例如对于i386来说GCC支持System V，Sun 386i则不支持</p>
<p>不使用PIC的时候，共享库的代码段必须被每个引用进程修改，因而无法实现共享</p>
</td>
</tr>
<tr>
<td>-fPIC</td>
<td>同上，但是不会受到GOT大小的限制</td>
</tr>
<tr>
<td>-fpie -fPIE</td>
<td>类似上面两个选项，但是生成的动态库仅仅供可执行文件链接</td>
</tr>
<tr>
<td>-fexec-charset=charset</td>
<td>设置执行字符集，用于字符串和字符常量，默认UTF-8</td>
</tr>
<tr>
<td>-fwide-exec-charset=charset</td>
<td>设置宽执行字符集，用于宽字符串和宽字符常量，默认UTF-32或UTF-16，应和wchar_t宽度对应</td>
</tr>
<tr>
<td>-finput-charset=charset</td>
<td>输入字符集，当GCC将输入文件（源代码文件）字符集转换为源字符集（GCC内部使用）时使用。默认值UTF-8，可以被该选项或者locale覆盖</td>
</tr>
<tr>
<td colspan="2"><strong><em> i386/x86-64选项</em></strong></td>
</tr>
<tr>
<td> -mthreads</td>
<td>用于在MinGW32下支持线程安全的异常处理</td>
</tr>
<tr>
<td> -m32 </td>
<td>生成32位代码，long/指针为32位，支持任意i386架构</td>
</tr>
<tr>
<td> -m64  </td>
<td>生成64位代码，long/指针为64位，支持x86-64架构</td>
</tr>
<tr>
<td colspan="2"><strong><em>i386/x86-64的Windows选项</em></strong></td>
</tr>
<tr>
<td>-mconsole</td>
<td>用于Cygwin、MinGW目标，提示生成控制台应用程序，连接器会设置PE头的subsystem，该选项是Cygwin、MinGW目标的默认行为</td>
</tr>
<tr>
<td> -mdll</td>
<td>用于Cygwin、MinGW目标，提示生成动态链接库（DLL）</td>
</tr>
<tr>
<td>-mnop-fun-dllimport</td>
<td>用于Cygwin、MinGW目标，忽略dllimport属性 </td>
</tr>
<tr>
<td>-mthread </td>
<td>用于MinGW目标，使用MinGW专有的线程支持</td>
</tr>
<tr>
<td>-mwin32</td>
<td>用于Cygwin、MinGW目标，预处理器将使用Windows典型的预定义宏</td>
</tr>
<tr>
<td>-mwindows </td>
<td>
<p>用于Cygwin、MinGW目标，提示生成GUI程序</p>
</td>
</tr>
</tbody>
</table>
<div class="blog_h3"><span class="graybg">示例</span></div>
<pre class="crayon-plain-tag"># 不指定选项，将预处理、汇编链接形成可执行文件，默认输出a.out
gcc hello.c
# 指定输出文件的名称
gcc hello.c -o hello
# 得到预处理输出文件
gcc -E hello.c -o hello.i
# 将预处理输出文件编译为汇编文件hello.s
gcc -S hello.i
# 将汇编文件编译为目标文件hello.o
gcc -c hello.s
# 不指定选项进行链接，生成可执行文件
gcc hello.o -o hello

# 多个文件一起编译，形成可执行文件
gcc hellolib.c hello.c -o hello
# 亦可逐个编译，然后链接，这样可以只编译部分文件
gcc -c hellolib.c
gcc -c hello.c
gcc hellolib.o hello.o -o hello

# 编译出共享库
gcc -shared hellolib.c -o libhello.so

# 强制使用静态hi库来链接
gcc hello.o -static -lhi -o hello</pre>
<div class="blog_h2"><span class="graybg"><a id="pkgcfg"></a>pkg-config命令</span></div>
<p>显示已经安装的软件包的元数据信息，该工具可以用于：</p>
<ol>
<li>检查库的版本号。如果所需要的库的版本不满足要求，它会打印出错误信息，避免链接错误版本的库文件</li>
<li>获得编译预处理参数，如宏定义，头文件的位置</li>
<li>获得链接参数，如库及依赖的其它库的位置，文件名及其它一些链接参数</li>
<li>自动加入所依赖的其它库的设置</li>
</ol>
<p>要使用pkg-config命令，<span style="background-color: #c0c0c0;">目标库必须提供一个包含了元数据的.pc文件</span>。对于大部分系统，这些.pc文件存放在：</p>
<ol>
<li>/usr/lib/pkgconfig</li>
<li>/usr/share/pkgconfig</li>
<li>/usr/local/lib/pkgconfig</li>
<li>/usr/local/share/pkgconfig</li>
</ol>
<p><span style="font-size: 13px;">但是可以通过环境变量</span><pre class="crayon-plain-tag">PKG_CONFIG_PATH</pre><span style="font-size: 13px;">进行自定义。</span></p>
<div class="blog_h3"><span class="graybg">元数据字段</span></div>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 20%; text-align: center;">字段</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>Name</td>
<td>该字段显示一个人类可读的包的名称</td>
</tr>
<tr>
<td>Description</td>
<td>该字段显示一个包的简短描述</td>
</tr>
<tr>
<td>URL</td>
<td>可以从其下载包的网址</td>
</tr>
<tr>
<td>Version</td>
<td>详细的软件包版本字符串</td>
</tr>
<tr>
<td>Requires</td>
<td>逗号分隔的，被当前包依赖的包的列表，来自依赖包的标记会被自动合并输出，可以指定操作符：=, &lt;, &gt;, &gt;=, &lt;= </td>
</tr>
<tr>
<td>Requires.private</td>
<td>类似于Requires，但是在进行动态链接时，这些依赖不会被考虑存放在标记中</td>
</tr>
<tr>
<td>Conflicts</td>
<td>类似于Requires，列出与当前包冲突的库</td>
</tr>
<tr>
<td>Libs</td>
<td>该字段显示当前包依赖的库</td>
</tr>
<tr>
<td>Libs.private</td>
<td>显示当前包依赖的私有库，这些库不需要暴露给当前库的使用者，但是当前库需要与之静态链接</td>
</tr>
<tr>
<td>Cflags</td>
<td>显示编译标记的列表</td>
</tr>
</tbody>
</table>
<p>下面是一个例子：</p>
<pre class="crayon-plain-tag"># This is a comment
prefix=/home/hp/unst # this defines a variable
exec_prefix=${prefix} # defining another variable in terms of the first
libdir=${exec_prefix}/lib
includedir=${prefix}/include

Name: GObject # human-readable name
Description: Object/type system for GLib # human-readable description
Version: 1.3.1
URL: http://www.gtk.org
Requires: glib-2.0 = 1.3.1
Conflicts: foobar &lt;= 4.5
Libs: -L${libdir} -lgobject-1.3
Libs.private: -lm
Cflags: -I${includedir}/glib-2.0 -I${libdir}/glib/include</pre>
<div class="blog_h3"><span class="graybg">环境变量</span></div>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 35%; text-align: center;">变量</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>PKG_CONFIG_PATH</td>
<td>用于搜索.pc文件的目录的集合，默认目录在这些目录后面搜索。默认目录为：libdir/pkgconfig:datadir/pkgconfig，其中libdir、datadir是pkg-config安装时的相关目录</td>
</tr>
<tr>
<td>PKG_CONFIG_DEBUG_SPEW</td>
<td>如果该变量被设置，pkg-config将打印所有的调试信息、错误信息</td>
</tr>
<tr>
<td>PKG_CONFIG_TOP_BUILD_DIR</td>
<td> 设置.pc文件中可能包含的pc_top_builddir变量的值，默认使用$(top_builddir)。只在依赖一个尚未安装的库时用到</td>
</tr>
<tr>
<td>PKG_CONFIG_DISABLE_UNINSTALLED</td>
<td>默认的，如果你指定一个foo，并且存在一个库：foo-uninstalled，那么后一个变体被使用，该变量禁止此行为</td>
</tr>
<tr>
<td>PKG_CONFIG_ALLOW_SYSTEM_CFLAGS</td>
<td>不从cflags中去除-I/usr/include</td>
</tr>
<tr>
<td>PKG_CONFIG_SYSROOT_DIR</td>
<td>修改-I、-L使用的系统根目录，如果该变量设置为/var/target，那么目标库的-I/usr/include/libfoo将被解释为-I/var/target/usr/include/libfoo</td>
</tr>
<tr>
<td>PKG_CONFIG_LIBDIR</td>
<td>替换默认的pkg-config搜索目录，一般设置为/usr/lib/pkgconfig</td>
</tr>
</tbody>
</table>
<div class="blog_h3"><span class="graybg">格式</span></div>
<pre class="crayon-plain-tag">pkg-config 
　[--modversion] [--help] [--print-errors] 
　[--silence-errors] [--cflags] [--libs] 
　[--libs-only-L] [--libs-only-l] [--cflags-only-I] 
　[--variable=VARIABLENAME] [--define-variable=VARIABLENAME=VARIABLEVALUE] 
　[--print-variables] [--uninstalled] [--exists] [--atleast-version=VERSION] 
　[--exact-version=VERSION] [--max-version=VERSION] [--list-all] [LIBRARIES...]
　[--print-provides] [--print-requires] [--print-requires-private] [LIBRARIES...]</pre>
<div class="blog_h3"><span class="graybg">选项</span></div>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 30%; text-align: center;">选项</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>--modversion</td>
<td>请求显示目标库的版本信息，如果指定多个库并且都成功找到，那么每个库的版本信息占一行</td>
</tr>
<tr>
<td>--print-errors</td>
<td>如果一个或者多个目标库、或者其依赖没有被找到，或者解析.pc文件失败，该选项会打印错误信息。环境变量PKG_CONFIG_DEBUG_SPEW覆盖此选项</td>
</tr>
<tr>
<td>--cflags</td>
<td>打印在编译目标库时，使用的预处理、编译标记，包括其依赖的所有标记</td>
</tr>
<tr>
<td>--cflags-only-I</td>
<td>仅打印--cflags中的 -I部分，即头文件搜索路径</td>
</tr>
<tr>
<td>--libs</td>
<td>显示链接标记</td>
</tr>
<tr>
<td>--libs-only-L</td>
<td>仅显示-- libs中的 -L/-R部分，即依赖库的搜索路径</td>
</tr>
<tr>
<td>--libs-only-l</td>
<td>仅显示-- libs中的 -l部分，即依赖库的名称</td>
</tr>
<tr>
<td>--variable=NAME</td>
<td>返回.pc文件中定义的变量</td>
</tr>
<tr>
<td>--define-variable=NAME=VALUE</td>
<td>设置某个变量的全局值，覆盖.pc中的值</td>
</tr>
<tr>
<td>--print-variables</td>
<td>打印目标库定义的变量的列表</td>
</tr>
<tr>
<td>--exists</td>
<td rowspan="4">用于判断目标包/包列表可以被检测到</td>
</tr>
<tr>
<td>--atleast-version=VERSION</td>
</tr>
<tr>
<td>--exact-version=VERSION</td>
</tr>
<tr>
<td>--max-version=VERSION</td>
</tr>
<tr>
<td>--msvc-syntax  </td>
<td>仅用于Windows，使输出格式中的-l、-L可以被MSVC编译器cl识别</td>
</tr>
<tr>
<td>--static</td>
<td>输出适合静态链接的库</td>
</tr>
<tr>
<td>--list-all</td>
<td>列出能识别的所有模块</td>
</tr>
<tr>
<td>--print-provides</td>
<td>列出目标库提供的所有模块</td>
</tr>
<tr>
<td>--print-requires</td>
<td>列出目标库需要的所有模块</td>
</tr>
<tr>
<td>--print-requires-private</td>
<td>列出目标库需要静态链接的所有依赖库</td>
</tr>
</tbody>
</table>
<div class="blog_h3"><span class="graybg">示例</span></div>
<pre class="crayon-plain-tag"># 打印glib-2.0库的prefix变量
# 检查是否存在大于1.3.4版本的glib-2.0库、以及大于1.8.3的libxml库
pkg-config --variable=prefix glib-2.0 
pkg-config --exists 'glib-2.0 &gt;= 1.3.4 libxml = 1.8.3' </pre>
<div class="blog_h2"><span class="graybg"><a id="ldconfig"></a>ldconfig命令</span></div>
<p>配置动态连接器的运行时绑定，<span style="background-color: #c0c0c0;">修改额外的链接库位置后，需要调用该命令使之生效</span></p>
<p>该命令为参数中提供的路径下找到的共享库，在：</p>
<ol>
<li>/etc/ld.so.conf（动态库的额外搜索位置）</li>
<li>受信目录（/lib /usr/lib）</li>
</ol>
<p><span style="font-size: 13px;">中<span style="background-color: #c0c0c0;">创建必要的链接、缓存</span>。修改文件/etc/ld.so.conf后应当调用ldconfig。运行时连接器ld.so、ld-linux.so会使用这些缓存</span></p>
<p>ldconfig通过检查库的文件名和头，来判断那些版本（的库）需要更新其软链接。</p>
<div class="blog_h3"><span class="graybg">格式</span></div>
<pre class="crayon-plain-tag">ldconfig [ -nNvXV ] [ -f conf ] [ -C cache ] [ -r root ] directory ...
ldconfig -l [ -v ] library ...
ldconfig -p</pre>
<div class="blog_h3"><span class="graybg">选项</span></div>
<p style="padding-left: 30px;">-v 打印当前版本号，正在处理的目录、创建的链接<br />-n 仅仅处理命令行指定的目录。不处理/etc/ld.so.conf中列出的目录，以及受信目录（/lib、/usr/lib）<br />-N 不重建缓存<br />-X 不更新软链接<br />-f 使用其它文件而非/etc/ld.so.conf<br />-C 使用其它缓存而非/etc/ld.so.cache </p>
<div class="blog_h2"><span class="graybg"><a id="ldd"></a>ldd</span></div>
<p>打印指定的程序、共享库，所依赖的共享库列表</p>
<div class="blog_h3"><span class="graybg">格式</span></div>
<pre class="crayon-plain-tag">ldd [OPTION]... FILE...</pre>
<div class="blog_h3"><span class="graybg">选项</span></div>
<p style="padding-left: 30px;">-u 打印没有使用的直接依赖<br />-d 进行重定位，报告所有缺失的对象<br />-r 同时对数据对象和函数进行重定位，报告缺失的对象或函数</p>
<div class="blog_h2"><span class="graybg"><a id="objdump"></a>objdump</span></div>
<p>显示一个或多个目标文件（object files）的信息</p>
<div class="blog_h3"><span class="graybg">格式</span></div>
<pre class="crayon-plain-tag">objdump [options] objfile...</pre>
<div class="blog_h3"><span class="graybg">选项</span></div>
<p style="padding-left: 30px;">-a 如果某个目标文件是归档文件（静态库），显示归档的头信息<br />-f 显示目标文件一般性的头<br />-p 显示特定目标文件格式的私有头<br />-h 显示目标文件section header的基本信息<br />-x 显示所有可用的头信息，包括符号表、relocation entries<br />-d 执行反汇编，仅针对可能存在代码的section<br />-D 对所有section执行反汇编<br />-S 显示混合着汇编的源码，隐含-d<br />-g 显示Debug信息<br />-e 显示Debug信息，输出格式与ctags工具兼容<br />-C  解码（demangle）低级的符号名称为用户可读名称，让C++函数名可读</p>
<div class="blog_h3"><span class="graybg">示例</span></div>
<pre class="crayon-plain-tag"># 显示共享库的soname
objdump -p /usr/lib/mysql/plugin/auth.so | grep SONAME

# 查找符号
objdump -x /home/alex/.local/lib/libc.so.6 | grep getaddrinfo</pre>
<div class="blog_h2"><span class="graybg"><a id="readelf"></a>readelf </span></div>
<p>显示ELF文件的信息。ELF是UNIX系统实验室（USL）作为应用程序二进制接口（Application Binary Interface，ABI）而开发和发布的，也是Linux的主要可执行文件格式。</p>
<div class="blog_h3"><span class="graybg">选项</span></div>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 20%; text-align: center;">选项</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>-a</td>
<td>等价于 -h -l -S -s -r -d -V -A -I</td>
</tr>
<tr>
<td>-h</td>
<td>
<p>显示ELF头信息，这是一些最基础的信息：</p>
<pre class="crayon-plain-tag"># readelf -h /home/alex/.local/lib/libc.so.6
ELF Header:
  Magic:   7f 45 4c 46 02 01 01 03 00 00 00 00 00 00 00 00 
  Class:                             ELF64
  Data:                              2's complement, little endian
  Version:                           1 (current)
  # 可以看到操作系统信息
  OS/ABI:                            UNIX - GNU
  ABI Version:                       0
  Type:                              DYN (Shared object file)
  # 可以看到体系结构信息
  Machine:                           Advanced Micro Devices X86-64
  Version:                           0x1
  # 程序入口点地址
  Entry point address:               0x21d10
  Start of program headers:          64 (bytes into file)
  Start of section headers:          12004816 (bytes into file)
  Flags:                             0x0
  Size of this header:               64 (bytes)
  Size of program headers:           56 (bytes)
  Number of program headers:         10
  Size of section headers:           64 (bytes)
  Number of section headers:         82
  Section header string table index: 79 </pre>
</td>
</tr>
<tr>
<td>-l <br />--program-headers<br />--segments</td>
<td>显示program headers信息</td>
</tr>
<tr>
<td>-S <br />--section-headers<br /> --sections</td>
<td>显示section headers信息</td>
</tr>
<tr>
<td>-g<br />--section-groups</td>
<td>显示section groups信息</td>
</tr>
<tr>
<td>-t <br />--section-details</td>
<td>
<p>显示section详细信息
<pre class="crayon-plain-tag"># 查看调试文件信息
readelf -S /lib/x86_64-linux-gnu/libc-2.23.so | grep -E 'debug|.build'
  [ 1] .note.gnu.build-i NOTE             0000000000000270  00000270
  [70] .gnu_debuglink    PROGBITS         0000000000000000  001c6dd1</pre>
</td>
</tr>
<tr>
<td>-s   --syms<br />--symbols</td>
<td>显示符号表</td>
</tr>
<tr>
<td>--dyn-syms</td>
<td>显示动态符号表</td>
</tr>
</tbody>
</table>
<div class="blog_h2"><span class="graybg">objcopy</span></div>
<p>可以将可执行文件中的调试信息抽取出来：
<pre class="crayon-plain-tag">objcopy --only-keep-debug foo foo.debug
strip -g foo </pre>
<div class="blog_h1"><span class="graybg">GDB</span></div>
<div class="blog_h2"><span class="graybg">简介</span></div>
<p>GNU的调试器，可以：</p>
<ol>
<li>启动应用程序，并指定任何参数</li>
<li>在特定情况下，让应用程序暂停执行</li>
<li>暂停指向后，检查应用程序的状态</li>
<li>改变应用程序状态</li>
</ol>
<div class="blog_h2"><span class="graybg">源码路径映射</span></div>
<p>编译器有时候不去记录源文件所被编译时的实际目录，而仅仅记录源文件的名称或相对路径。即使记录了，在你调试的时候，编译目录可能已经删除，这就让gdb面临如何将符号表中的源文件路径映射到Debugger机器的源文件的问题</p>
<p>gdb使用一系列目录，从中搜索源文件，此所谓source path。每次gdb需要源文件时，逐个搜索这些目录，直到找到一个名称匹配的文件</p>
<p>例如，假设可执行文件符号表中引用/usr/src/foo-1.0/lib/foo.c，而Debugger的source path设定为 /mnt/cross，则文件的（在Debugger上）搜索顺序为：</p>
<ol>
<li>按符号表字面值搜索：/usr/src/foo-1.0/lib/foo.c</li>
<li>添加source path作为前缀：/mnt/cross/usr/src/foo-1.0/lib/foo.c</li>
<li>添加source path作为前缀，剥离符号表的目录部分：/mnt/cross/foo.c</li>
</ol>
<p>符号表引用的相对路径../lib/foo.c类似，分别搜索 ../lib/foo.c、/mnt/cross/../lib/foo.c、/mnt/cross/foo.c</p>
<div class="blog_h2"><span class="graybg">符号表</span></div>
<p>在符号表中，程序源代码中的每个<span style="background-color: #c0c0c0;">标识符</span>都和它的声明或使用信息绑定在一起，比如<span style="background-color: #c0c0c0;">其数据类型、作用域以及内存地址</span>。</p>
<p>目标文件中通常会有一个包含了所有外部可见标识符的符号表。在链接不同的目标文件时，链接器会使用这些文件中的<span style="background-color: #c0c0c0;">符号表来解析所有未解析的符号引用</span>。</p>
<p>在调试阶段，需要通过符合表来解读各种信息。</p>
<p>对于Ubuntu来说，软件包的符号表独立分发。它们发布在 ddebs.ubuntu.com，命名为<pre class="crayon-plain-tag">&lt;package&gt;-dbgsym</pre>。相比传统的<pre class="crayon-plain-tag">&lt;package&gt;-dgb</pre>，它的优势包括：</p>
<ol>
<li>dbgsym可以自动生成、自动上传到ddebs.ubuntu.com。维护成本低</li>
</ol>
<div class="blog_h3"><span class="graybg">查找调试信息</span></div>
<p>GDB支持在独立文件中存放的符号信息，并且能够自动发现、加载。</p>
<p>GDB支持两种方式来指定调试信息文件：</p>
<ol>
<li>在可执行文件中，包含<span style="background-color: #c0c0c0;">debug link</span>，指定了独立调试文件的名称，debug link是<span style="background-color: #c0c0c0;">可执行文件中名为.gnu_debuglink的特殊section</span>。调试文件名称格式通常是executable-name.debug。debug link 还提供调试文件的CRC检查，用于验证调试文件、可执行文件来自同一次build</li>
<li>在可执行文件中，包含<span style="background-color: #c0c0c0;">build ID</span>，在对应的调试文件中，此ID也存在。build ID是可执行文件中（通常）<span style="background-color: #c0c0c0;">名为 .note.gnu.build-id的section</span></li>
</ol>
<p>对于方式debug link方式，GDB依次在以下位置寻找调试文件：</p>
<ol>
<li>在可执行文件所在目录</li>
<li>名为.debug的子目录</li>
<li>全局debug目录</li>
</ol>
<p>对于build ID方式，GDB会在全局debug目录的<span style="background-color: #c0c0c0;">.build-id子目录</span>中查找nn/nnnnnnnn.debug文件。前面的nn是build ID的最前面2个HEX字符，nnnnnnnn则是build ID的剩余部分。</p>
<p>举例来说，调试/usr/bin/ls的时候，如果debug link指定了 ls.debug，并且具有build ID是abcdef1234，并且全局debug目录包括/usr/lib/debug，那么以下位置用于寻找调试文件：</p>
<ol>
<li>/usr/lib/debug/.build-id/ab/cdef1234.debug</li>
<li>/usr/bin/ls.debug</li>
<li>/usr/bin/.debug/ls.debug</li>
<li>/usr/lib/debug/usr/bin/ls.debug</li>
</ol>
<p>全局debug目录取决于GDB的配置项  <pre class="crayon-plain-tag">--with-separate-debug-dir</pre>。</p>
<p>你也可以使用交互式命令查看、设置全局dbug目录：</p>
<pre class="crayon-plain-tag"># 查看
(gdb) show debug-file-directory
# 设置，path separator分隔
(gdb) set debug-file-directory directories  </pre>
<p>找到调试文件，是设置断点的前提条件。运行到断点之后，你可能遇到：res_send.c: No such file or directory. 这样的错误，这是因为没有找到源码。可以用下面的命令来设置源码路径：</p>
<pre class="crayon-plain-tag">(gdb) directory /glibc-2.27/resolv
# 指定多个源码路径
(gdb) directory /glibc:$cdir:$cwd</pre>
<div class="blog_h2"><span class="graybg"><a id="gdb"></a>gdb命令</span></div>
<div class="blog_h3"><span class="graybg">格式</span></div>
<pre class="crayon-plain-tag">gdb [-help] [-nh] [-nx] [-q] [-batch] [-cd=dir] [-f] [-b bps]
    [-tty=dev] [-s symfile] [-e prog] [-se prog] [-c core] [-p procID]
    [-x cmds] [-d dir] [prog|prog procID|prog core]

# 连接到指定的进程以调试
gdb -p 1234

# 调试应用程序
gdb /path/to/program</pre>
<div class="blog_h3"><span class="graybg">选项</span></div>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 150px; text-align: center;">选项</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>-s </td>
<td>从指定的文件中读取符号表</td>
</tr>
<tr>
<td>-e</td>
<td>指定可执行文件，在适合（发起run命令）的时候执行。也用于和core dump联用来检查pure data</td>
</tr>
<tr>
<td>-se</td>
<td>从文件读取符号表同时将其作为可执行文件</td>
</tr>
<tr>
<td>-c</td>
<td>指定需要检查的core dump</td>
</tr>
<tr>
<td>-x</td>
<td>从文件中读取GDB命令</td>
</tr>
<tr>
<td>
<p>-ex</p>
</td>
<td>执行指定的GDB命令</td>
</tr>
<tr>
<td>-d</td>
<td>指定搜索源码文件的目录</td>
</tr>
<tr>
<td>-cd</td>
<td>指定工作目录</td>
</tr>
<tr>
<td>-write</td>
<td>启用写入到可执行文件/core文件</td>
</tr>
<tr>
<td>-tty</td>
<td>使用指定的设备作为程序的stdio</td>
</tr>
<tr>
<td>
<p>-p</p>
</td>
<td>Attach到指定的进程</td>
</tr>
<tr>
<td>-tui</td>
<td>使用CLUI模式启动，在容器中运行的GDB，CLUI模式下显示有问题，建议从宿主机上进入容器的空间执行GDB</td>
</tr>
</tbody>
</table>
<div class="blog_h3"><span class="graybg">子命令</span></div>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 150px; text-align: center;">子命令</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td colspan="2"><strong><em>常用</em></strong></td>
</tr>
<tr>
<td>target</td>
<td>
<p>连接到目标，例如gdbserver或者进程：</p>
<pre class="crayon-plain-tag"># 连接到gdbserver
target remote localhost:2345</pre>
</td>
</tr>
<tr>
<td>attach</td>
<td>附着到进程进行调试</td>
</tr>
<tr>
<td>detach</td>
<td>解除对进程的调试</td>
</tr>
<tr>
<td>&lt;ENTER&gt;</td>
<td>再次执行上一个命令</td>
</tr>
<tr>
<td>ctrl + c</td>
<td>立即暂停正在运行中的程序，暂停在当前正在执行的行，等待后续调试指令</td>
</tr>
<tr>
<td>break</td>
<td>
<p>设置断点，注意，你可能需要设置在期望函数的上一行，<strong><span style="background-color: #c0c0c0;">否则它可能是在调用函数结束之后才暂停，而不是调用开始</span><span style="background-color: #c0c0c0;">前</span></strong>
<p>示例：</p>
<pre class="crayon-plain-tag"># 在主函数执行前暂停
b main

# 在main.cpp的55行设置断点
break main.cpp:55

# 在指定的内存地址上设置断点
b *0x08048375

# 在main函数的第7个指令上设置断点
b *main+7 


# 指定源文件绝对路径
break  /tmp/KeyDB/src/replication.cpp:2302

# 函数断点，在执行函数前暂停程序
break funcname

# 在当前文件的第N行设置断点
b N
# 在当前行后面的第N行设置断点
b +N

# 条件断点
break line-number if condition
break main.cpp:55 if x &gt; 1

# 线程断点
# 使用info threads获得线程编号
break line thread thread-number


# 列出所有断点
info break

# 根据序号删除断点
del breakpointnumber

# 禁用/启用断点
dis breakpointnumber
en  breakpointnumber

# 忽略断点，知道经过它X次
ignore breakpointnumber x

# 清除（当前暂停在的）断点
clear</pre>
</td>
</tr>
<tr>
<td>tbreak</td>
<td>一次性断点</td>
</tr>
<tr>
<td><strong>c</strong></td>
<td>
<p>continue，让程序从暂停中恢复，当遇到下一个breakpoint或者ctrl + c程序会再次暂停
<p><pre class="crayon-plain-tag">c N</pre> 继续且忽视当前断点N次</p>
</td>
</tr>
<tr>
<td>until</td>
<td><pre class="crayon-plain-tag">until line-number</pre>，一直执行，直到指定的行数</td>
</tr>
<tr>
<td><strong>s</strong></td>
<td>
<p>step，执行一行代码，如果正在执行的是函数，则进入此函数。相当于大部分调试工具的step into</p>
<p><pre class="crayon-plain-tag">s N</pre> 则执行N行代码</p>
</td>
</tr>
<tr>
<td>si</td>
<td>step的精确版，每次仅执行一个汇编指令，可能需要多次才能执行完一行</td>
</tr>
<tr>
<td><strong>n</strong></td>
<td>next，执行到下一行，相当于大部分调试工具的step over</td>
</tr>
<tr>
<td>ni</td>
<td>next的精确版，每次仅执行一个汇编指令，可能需要多次才能执行完一行</td>
</tr>
<tr>
<td><strong>f</strong></td>
<td>运行直到当前函数结束，相当于大部分调试工具的step out</td>
</tr>
<tr>
<td>l</td>
<td>list，显示当前行附近的代码。默认显示20行</td>
</tr>
<tr>
<td>bt</td>
<td>backtrace，打印栈追踪</td>
</tr>
<tr>
<td>u</td>
<td>转到栈帧的上一级别</td>
</tr>
<tr>
<td>d</td>
<td>转到栈帧的下一级别</td>
</tr>
<tr>
<td>where</td>
<td>打印所有栈帧的信息，包括指令地址、函数名、源码位置。如果在调试时源码路径映射不对（导致显示反编译代码），可以通过此命令知晓构建时的源码路径</td>
</tr>
<tr>
<td>directory</td>
<td>
<p>添加指定的目录到源码搜索路径中 </p>
<p>示例：</p>
<pre class="crayon-plain-tag"># 解决CLion中，由于源码路径不正确导致显示汇编的问题
where
# #30 0x00000000010ad0b0 in event_persist_closure (ev=&lt;optimized out&gt;, base=0x32fc000) at ../event.c:1580
# #31 event_process_active_single_queue (base=base@entry=0x32fc000, activeq=0x32260b0, max_to_process=max_to_process@entry=2147483647, endtime=endtime@entry=0x0) at ../event.c:1639
# 这里的libevent的源码映射失败，构建时，源码位于工作目录的上级目录
# #32 0x00000000010ada67 in event_process_active (base=0x32fc000) at ../event.c:1738
# #33 event_base_loop (base=0x32fc000, flags=0) at ../event.c:1961
# #34 0x0000000000a5067c in Envoy::Event::DispatcherImpl::run (this=0x322b3f0, type=Envoy::Event::Dispatcher::RunType::Block) at source/common/event/dispatcher_impl.cc:165
# #35 0x00000000009d74c5 in Envoy::Server::InstanceImpl::run (this=0x32bb200) at source/server/server.cc:466
# #36 0x0000000000425baa in Envoy::MainCommonBase::run (this=0x32ba8b0) at source/exe/main_common.cc:103
# #37 0x000000000040cd68 in Envoy::MainCommon::run (this=0x32ba480) at bazel-out/k8-dbg/bin/source/exe/_virtual_includes/envoy_main_common_lib/exe/main_common.h:86
# #38 0x000000000040a4bf in main (argc=19, argv=0x7ffe63314218) at source/exe/main.cc:37
# #39 0x00007f7407aaff45 in __libc_start_main (main=0x40a44e &lt;main(int, char**)&gt;, argc=19, argv=0x7ffe63314218, init=&lt;optimized out&gt;, fini=&lt;optimized out&gt;, rtld_fini=&lt;optimized out&gt;, stack_end=0x7ffe63314208) at libc-start.c:287
# #40 0x0000000000409e45 in _start ()
# libevent的源码目录是：/home/alex/CPP/lib/libevent/2.1.8-stable
# 而当前的工作目录是：/home/alex/CPP/projects/clion/envoy
# 将libevent源码添加到源码搜索路径
directory /home/alex/CPP/lib/libevent/2.1.8-stable</pre>
</td>
</tr>
<tr>
<td>
<p>i
<p>info</p>
</td>
<td>
<p>显示各种信息：</p>
<p style="padding-left: 30px;">info all-registers 列出所有寄存器及其值<br />info args 显示当前栈帧的参数值<br />info locals 显示当前栈帧的本地变量值<br />info variables 显示全局和静态变量的值<br />info break  显示断点列表</p>
<p>示例：</p>
<pre class="crayon-plain-tag"># 得到eax寄存器的值，通常是函数调用返回值
i r eax </pre>
</td>
</tr>
<tr>
<td>set</td>
<td>
<p>可以用来设置变量值：
<p><pre class="crayon-plain-tag">set varname=varvalue

# 设置寄存器rdx的值
set $rdx = 10000</pre>
</td>
</tr>
<tr>
<td>p</td>
<td>print，可以用来打印变量值：<pre class="crayon-plain-tag">print varname</pre></td>
</tr>
<tr>
<td>help</td>
<td>使用<pre class="crayon-plain-tag">help commandname</pre>可以查看某个命令的帮助</td>
</tr>
<tr>
<td>q</td>
<td>quit，退出GDB</td>
</tr>
<tr>
<td colspan="2"><strong><em>断点</em></strong></td>
</tr>
<tr>
<td>watch</td>
<td>为表达式设置监控点，例如<pre class="crayon-plain-tag">watch x &gt; 1</pre>表示当x大于1时暂停执行</td>
</tr>
<tr>
<td>break-range</td>
<td>为地址范围设置断点</td>
</tr>
<tr>
<td>catch</td>
<td>设置捕获点以捕获事件</td>
</tr>
<tr>
<td>commands</td>
<td>指定到达断点时执行的命令</td>
</tr>
<tr>
<td>delete</td>
<td>删除断点或自动显示表达式</td>
</tr>
<tr>
<td>skip</td>
<td>单步跟踪时，忽略指定的函数、文件</td>
</tr>
<tr>
<td>tbreak</td>
<td>设置临时断点</td>
</tr>
<tr>
<td>disable</td>
<td>禁用断点</td>
</tr>
<tr>
<td>enable</td>
<td>启用断点</td>
</tr>
<tr>
<td colspan="2"><strong><em>程序操控</em></strong></td>
</tr>
<tr>
<td>advance</td>
<td>
<p>运行到指定位置：</p>
<pre class="crayon-plain-tag"># 向前跑30行
advance +30</pre>
</td>
</tr>
<tr>
<td>finish</td>
<td>执行直到选中的栈帧返回</td>
</tr>
<tr>
<td>handle</td>
<td>指定如何处理信号</td>
</tr>
<tr>
<td>jump</td>
<td>跳转正在执行的程序到指定的行或地址</td>
</tr>
<tr>
<td>kill</td>
<td>杀死被调试的程序</td>
</tr>
<tr>
<td>queue-signal </td>
<td> 入队一个信号，在当前线程resume后传递给它</td>
</tr>
<tr>
<td>run</td>
<td>启动被调试的程序</td>
</tr>
<tr>
<td>start</td>
<td>启动被停止在主程序开始处的程序</td>
</tr>
<tr>
<td>thread</td>
<td>切换到其它线程</td>
</tr>
<tr>
<td>thread find </td>
<td>使用正则式查找线程</td>
</tr>
<tr>
<td>thread name </td>
<td>设置当前线程的名称</td>
</tr>
<tr>
<td colspan="2"><strong><em>文件和路径</em></strong></td>
</tr>
<tr>
<td>set directories</td>
<td>设置源码搜索路径</td>
</tr>
<tr>
<td>set substitute-path</td>
<td>set substitute-path from to 设置查找源码使用的路径映射</td>
</tr>
<tr>
<td>set sysroot</td>
<td>
<p>设置包含了调试目标的库的副本的目录，当连接到gdbserver进行远程调试时有用
<pre class="crayon-plain-tag"># 从指定的本地目录来寻找库
set sysroot [Directory]

# 从指定的远程目录寻找库，一般设置为 /
set sysroot remote:/
set sysroot remote:[Remote directory]
set solib-absolute-prefix [Directory]
show sysroot</pre>
<p>如果运行gdbserver的远程服务器、运行gdb的本地服务器，上面的库不匹配，设置此变量可以解决找不到调试文件的问题 </p>
</td>
</tr>
<tr>
<td>add-symbol-file  </td>
<td>从文件加载符号表</td>
</tr>
<tr>
<td>cd </td>
<td>设置调试器的工作目录</td>
</tr>
<tr>
<td>core-file </td>
<td>使用指定的文件作为coredump，用于检查内存和寄存器</td>
</tr>
<tr>
<td>generate-core-file </td>
<td>生成正在调试的程序的coredump</td>
</tr>
<tr>
<td>load</td>
<td>动态的加载文件到当前程序中</td>
</tr>
<tr>
<td>nosharedlibrary</td>
<td> 卸载所有共享库的符号</td>
</tr>
<tr>
<td colspan="2"><strong><em>状态和信息</em></strong></td>
</tr>
<tr>
<td>info address</td>
<td>
<p>描述指定的符号在何处存储</p>
</td>
</tr>
<tr>
<td>info symbol</td>
<td>显示指定地址的符号</td>
</tr>
<tr>
<td>info all-registers</td>
<td> 列出所有寄存器及其值</td>
</tr>
<tr>
<td>info args</td>
<td>显示当前栈帧的参数值</td>
</tr>
<tr>
<td>info locals</td>
<td>显示当前栈帧的本地变量值</td>
</tr>
<tr>
<td>info variables</td>
<td>显示全局和静态变量的值</td>
</tr>
<tr>
<td>info breakpoints</td>
<td>显示断点状态</td>
</tr>
<tr>
<td>info files</td>
<td>目标的名称以及被调试的文件</td>
</tr>
<tr>
<td>info frame</td>
<td>选中栈帧的信息</td>
</tr>
<tr>
<td>info functions</td>
<td>列出所有函数名</td>
</tr>
<tr>
<td>info macro</td>
<td>显示宏定义</td>
</tr>
<tr>
<td>info macros</td>
<td>显示指定 LINESPEC的所有宏定义</td>
</tr>
<tr>
<td>info program</td>
<td>显示程序的执行状态</td>
</tr>
<tr>
<td>info stack</td>
<td>显示栈的追踪</td>
</tr>
<tr>
<td>info target</td>
<td>显示正在调试的目标名</td>
</tr>
<tr>
<td>info threads</td>
<td>显示线程列表</td>
</tr>
<tr>
<td>info source</td>
<td>显示当前（被中断时断点所在文件）源文件的信息</td>
</tr>
<tr>
<td>info sources  </td>
<td>
<p>显示源文件列表。启动gdb时，其source path仅仅包含：</p>
<p style="padding-left: 30px;"><pre class="crayon-plain-tag">$cdir</pre> 编译目录<br /><pre class="crayon-plain-tag">$cwd</pre> 当前工作目录</p>
</td>
</tr>
<tr>
<td>info set</td>
<td>显示所有GDB设置</td>
</tr>
<tr>
<td>info proc cwd</td>
<td>显示当前被调试程序的工作目录</td>
</tr>
<tr>
<td>info proc exe  </td>
<td>显示当前被调试程序的可执行文件的路径</td>
</tr>
<tr>
<td>show architecture</td>
<td> 显示目标体系结构</td>
</tr>
<tr>
<td>show args</td>
<td>显示被调试目标启动时的参数</td>
</tr>
<tr>
<td>show version</td>
<td>
<p>显示GDB版本信息</p>
</td>
</tr>
<tr>
<td>show sysroot</td>
<td>显示当前的system root</td>
</tr>
<tr>
<td>show substitute-path</td>
<td>显示当前的源码映射</td>
</tr>
<tr>
<td colspan="2"><strong><em>反汇编</em></strong></td>
</tr>
<tr>
<td>disassemble</td>
<td>反汇编，打印所有指令：<pre class="crayon-plain-tag">disassemble main</pre></td>
</tr>
<tr>
<td>x/8i</td>
<td>反汇编，打印前8个指令：<pre class="crayon-plain-tag">x/8i main</pre></td>
</tr>
</tbody>
</table>
<div class="blog_h2"><span class="graybg"><a id="gdbserver"></a>gdbserver命令</span></div>
<p>这是一个控制程序，允许你通过Remote GDB连接到应用程序。要在目标系统中运行gdbserver，你<span style="background-color: #c0c0c0;">需要应用程序的二进制文件，但是不一定需要符号表</span>。</p>
<div class="blog_h3"><span class="graybg">格式</span></div>
<pre class="crayon-plain-tag">gdbserver [OPTIONS] COMM PROG [ARGS ...]
gdbserver [OPTIONS] --attach COMM PID
gdbserver [OPTIONS] --multi COMM</pre>
<p>其中：</p>
<ol>
<li>COMM：要么是一个TTY设备（串口调试），要么是HOST:PORT（用于监听GDB客户端连接），还可以指定 - 或者 stdio，表示使用gdbserver的标准输入/输出</li>
<li>PROG  ARGS：被调试的可执行文件的路径和参数</li>
<li>PID：需要附着到的进程的PID</li>
</ol>
<div class="blog_h3"><span class="graybg">选项</span></div>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 20%; text-align: center;">选项</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>--attach</td>
<td>附着到指定进程</td>
</tr>
<tr>
<td>--multi</td>
<td>不指定需要执行/Attach的应用程序，仅仅在显示给出命令时才退出</td>
</tr>
<tr>
<td>--once</td>
<td>第一个连接关闭后，关闭服务器</td>
</tr>
<tr>
<td>--wrapper WRAPPER --</td>
<td>通过运行WRAPPER来启动应用程序</td>
</tr>
<tr>
<td>--disable-randomization<br />--no-disable-randomization</td>
<td>运行应用程序是，是否禁用地址空间随机化</td>
</tr>
<tr>
<td>--startup-with-shell<br />--no-startup-with-shell</td>
<td>是否在Shell中运行应用程序</td>
</tr>
<tr>
<td>--debug</td>
<td>启用一般性的调试输出</td>
</tr>
</tbody>
</table>
<div class="blog_h3"><span class="graybg">示例</span></div>
<pre class="crayon-plain-tag"># 附着到正在运行的程序，在2345端口上等待客户端连接
gdbserver --attach 0.0.0.0:2345 449</pre>
<div class="blog_h2"><span class="graybg">GDB前端</span></div>
<div class="blog_h3"><span class="graybg">tui</span></div>
<p>这是GDB自带的前端，使用<pre class="crayon-plain-tag">gdb -tui</pre>即可启动。</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 20%; text-align: center;">命令</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>ctrl-x o</td>
<td>窗口之间切换焦点</td>
</tr>
<tr>
<td>layout</td>
<td>
<p>设置窗口布局：</p>
<p style="padding-left: 30px;">src 上面源码、下面命令<br />split 上面源码、中间汇编、下面命令</p>
</td>
</tr>
<tr>
<td>ctrl-l</td>
<td>重新绘制窗口</td>
</tr>
</tbody>
</table>
<div class="blog_h3"><span class="graybg">Emacs GDB</span></div>
<p><a href="/linux-text-editors#emacs">Emacs编辑器</a>提供的功能。按<pre class="crayon-plain-tag">M-x</pre>，输入gdb即可使用。</p>
<p>输入命令<pre class="crayon-plain-tag">gdb-many-windows</pre>可以进入多窗口模式。默认6个窗口，分别是GDB终端、本地变量列表、源码、栈帧、断点。</p>
<p>输入命令<pre class="crayon-plain-tag">gdb-display-disassembly-buffer</pre>可以将某个窗口切换为反汇编。</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="text-align: center;">命令</td>
<td style="text-align: center;">功能</td>
<td style="text-align: center;">默认快捷键</td>
<td style="text-align: center;">熟悉快捷键</td>
</tr>
</thead>
<tbody>
<tr>
<td>添加断点</td>
<td>gud-break</td>
<td>C-x C-a C-b</td>
<td> </td>
</tr>
<tr>
<td>删除断点</td>
<td>gud-remove</td>
<td>C-x C-a C-d</td>
<td> </td>
</tr>
<tr>
<td>继续运行程序</td>
<td>gud-go</td>
<td> </td>
<td> </td>
</tr>
<tr>
<td>单步执行，不进入函数</td>
<td>gud-next</td>
<td>C-x C-a C-n</td>
<td>F6</td>
</tr>
<tr>
<td>单步执行，进入函数</td>
<td>
<p>gud-step</p>
</td>
<td>C-x C-a C-s</td>
<td>F5</td>
</tr>
<tr>
<td>跳出当前函数</td>
<td>gud-finish</td>
<td>C-x C-a C-f</td>
<td>F7</td>
</tr>
<tr>
<td>运行到光标所在语句</td>
<td>gud-until</td>
<td>C-x C-a C-u</td>
<td>Ctrl+F8</td>
</tr>
<tr>
<td>继续运行程序</td>
<td>gud-cont</td>
<td>C-x  C-a  C-r</td>
<td>F8</td>
</tr>
</tbody>
</table>
<div class="blog_h3"><span class="graybg">cgdb</span></div>
<p>如果不习惯Emacs的快捷键，可以使用cgdb，它更加简洁，vim用户友好。</p>
<p>默认情况下，cgdb将控制台分为两个窗口：</p>
<ol>
<li>源码窗口：使用vim风格命令，输入:quit可以退出</li>
<li>GDB窗口：使用GDB命令，输入quit或者C-d退出</li>
</ol>
<p>可用窗口列表：</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 100px; text-align: center;">窗口</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>源码窗口</td>
<td>
<p>在这个窗口查看源码，使用快捷键进行单步跟踪。支持C, C++语法高亮，支持源码导航导航到指定的源码，然后按Space即可设置断点。支持在源码中基于正则式进行搜索</p>
<p>快捷键：</p>
<pre class="crayon-plain-tag">ESC            进入命令模式
  i              进入GDB模式（输入焦点变为GDB窗口）
  s              进入GDB模式，支持滚动
  C-t            为被调试程序打开新的TTY窗口
  C-w            切换水平/垂直布局
  num            k或者num up-arrow 光标向上移动num行
  num j          或者 num down-arrow 光标向下移动num行
  h              或者left-arrow 向左移动光标
  l              或者 right-arrow 向右移动光标
  C-b            或者 page up 向上翻页
  C-u            向上翻半页
  C-f            或者 page down 向下翻页
  C-d            向下翻半页
  m[a-zA-Z]      设置标记
  '[a-zA-Z]      跳转到标记
  ''             跳转到上一个位置
  '.             跳转到正在执行的行
  /              从当前光标处开始搜索
  ?              从当前光标处反向搜索
  n              前向下一个匹配
  N              反向下一个匹配
  o              打开文件对话框
  spacebar       在当前行设置断点
  t              在当前行设置一次性断点
  F5             发送run命令给GDB
  F6             发送continue命令给GDB
  F7             发送finish命令给GDB
  F8             发送next命令给GDB
  F10            发送step命令给GDB</pre>
<p>&nbsp;</p>
<p>命令： </p>
<pre class="crayon-plain-tag">:set asr           自动加载修改后的源文件

:set color         是否在可能的时候用彩色显示，默认no
:set dwc           是否在调试窗口中使用彩色显示

:set dis           是否显示汇编，默认off，如果on，则显示混合的汇编、源码

:set ic            搜索时是否忽略大小写，默认off
:set hls           是否高亮显示所有匹配

:set showmarks     源码窗口中是否默认显示标记

:set ts=number     TAB显示为空格的数量，默认8

:set wso=style     窗口布局方向 horizontal  或者 vertical
:set hls

:map lhs rhs       新建或者覆盖原有的按键映射</pre>
<p>命令可以写在<pre class="crayon-plain-tag">~/.cgdb/cgdbrc</pre>文件中，不需要前导的冒号。</p>
<p>map命令用于修改按键映射，例如： </p>
<p style="padding-left: 30px;"><pre class="crayon-plain-tag">map &lt;F2&gt; ip&lt;Space&gt;argc&lt;CR&gt;</pre>表示，按F2后，会自动输入i进入GDB模式，然后输入 p空格argc回车<br /><pre class="crayon-plain-tag">map &lt;F9&gt; :until&lt;CR&gt;</pre>表示，按F9后执行:unti回车</p>
<p>下面的配置修改成Eclipse风格的调试快捷键：</p>
<pre class="crayon-plain-tag">set ignorecase
set ts=4
set wso=vertical
set dis=on

map &lt;F3&gt;     isi&lt;CR&gt;&lt;ESC&gt;
map &lt;F4&gt;     ini&lt;CR&gt;&lt;ESC&gt;
map &lt;F5&gt;     :step&lt;CR&gt;
map &lt;F6&gt;     :next&lt;CR&gt;
map &lt;F7&gt;     :finish&lt;CR&gt;
map &lt;F8&gt;     :continue&lt;CR&gt;
map &lt;F9&gt;     :run&lt;CR&gt;
map &lt;F10&gt;    :until&lt;CR&gt;</pre>
<p>其中尖括号中的是keycode：</p>
<pre class="crayon-plain-tag">&lt;Esc&gt;        escape key
&lt;Up&gt;         cursor up key
&lt;Down&gt;       cursor down key
&lt;Left&gt;       cursor left key
&lt;Right&gt;      cursor right key
&lt;Home&gt;       home key
&lt;End&gt;        end key
&lt;PageUp&gt;     page up key
&lt;PageDown&gt;   page down key
&lt;Del&gt;        delete key
&lt;Insert&gt;     insert key
&lt;Nul&gt;        zero
&lt;Bs&gt;         backspace key
&lt;Tab&gt;        tab key
&lt;NL&gt;         linefeed
&lt;FF&gt;         formfeed
&lt;CR&gt;         carriage return
&lt;Space&gt;      space
&lt;Lt&gt;         less-than
&lt;Bslash&gt;     backslash
&lt;Bar&gt;        vertical bar
&lt;F1&gt;         function keys 1 to 12
&lt;C-...&gt;      control keys
&lt;S-...&gt;      shift keys</pre>
</td>
</tr>
<tr>
<td>GDB窗口</td>
<td>在这个窗口中可以使用GDB命令</td>
</tr>
<tr>
<td>文件对话框</td>
<td>
<p>用于选择需要查看的文件
<p>在源码窗口输入<pre class="crayon-plain-tag">o</pre>即可打开对话框，输入<pre class="crayon-plain-tag">q</pre>退出对话框</p>
</td>
</tr>
<tr>
<td>状态栏</td>
<td>
<p>最右侧是<pre class="crayon-plain-tag">*</pre>表示当前焦点在GDB窗口，按<pre class="crayon-plain-tag">ESC</pre>即可让源码窗口获得焦点（进入CGDB模式），输入<pre class="crayon-plain-tag">i</pre>焦点回到GDB窗口（进入GDB模式）</p>
</td>
</tr>
</tbody>
</table>
<div class="blog_h1"><span class="graybg">常用代码片段</span></div>
<div class="blog_h2"><span class="graybg">向程序传递参数</span></div>
<p>Linux或者UNIX下C语言程序入口函数的标准声明方式如下：</p>
<pre class="crayon-plain-tag">int main(int argc, char *argv[]);
//argc 表示参数的个数，其中第一个参数是程序的名称
//argv 表示参数的数组
//多个参数使用空白符区分，如果单个参数中间有空白符，使用引号包围该参数</pre>
<p>下面的代码说明如何读取程序参数：</p>
<pre class="crayon-plain-tag">#include &lt;stdio.h&gt;
#include &lt;stdlib.h&gt;
int main( int argc, char *argv[] )
{
    int arg;
    for ( arg = 0; arg &lt; argc; arg++ )
    {
        if ( argv[arg][0] == '-' ) printf( "选项: %s\n", argv[arg] + 1 );
        else printf( "参数: %s\n", argv[arg] );
    }
    exit( 0 );
}</pre>
<p>X/Open规范定义了命令行选项的标准格式，并提供了获取命令行选项的接口：getopt函数：</p>
<pre class="crayon-plain-tag">#include &lt;unistd.h&gt;
int getopt(int argc, char *const argv[], const char *optstring);
//optstring：字符列表，表示支持的选项，每个字符代表一个选项，:表示前面的选项需要一个值
//例如if:lr表示支持4个选项，其中f后面紧跟一个关联值
extern char *optarg;
//optind存放下一个待处理参数的索引
//如果遇到无法识别的选项，getopt返回一个?并将此选项保存到optopt中
//如果一个选项要求关联值，但是没有传递，则getopt返回一个?
extern int optind, opterr, optopt;</pre>
<p>反复调用getopt，即可依次得到选项，示例如下：</p>
<pre class="crayon-plain-tag">#include &lt;stdio.h&gt;
#include &lt;unistd.h&gt;
#include &lt;stdlib.h&gt;
int main( int argc, char *argv[] )
{
    int opt;
    //直到返回值是-1，依次读取
    while ( ( opt = getopt( argc, argv, ":if:lr" ) ) != -1 )
    {
        switch ( opt )
        {
            case 'i' :
            case 'l' :
            case 'r' :
                printf( "option: %c\n", opt );
                break;
            case 'f' :
                printf( "filename: %s\n", optarg ); //读取参数值
                break;
            case ':' :
                printf( "option needs a value\n" );
                break;
            case '?' :
                printf( "unknown option: %c\n", optopt ); //读取未知参数名
                break;
        }
    }
    for ( ; optind &lt; argc; optind++ )
        printf( "argument: %s\n", argv[optind] );
    exit( 0 );
}</pre>
<p>GNU C函数库支持双横线开头的长参数，函数为getopt_long，有关联值的参数使用<pre class="crayon-plain-tag">--option=value</pre> 的形式给出。该函数用法示例如下：</p>
<pre class="crayon-plain-tag">#include &lt;stdio.h&gt;
#include &lt;unistd.h&gt;
#include &lt;stdlib.h&gt;
#define _GNU_SOURCE
#include &lt;getopt.h&gt;
int main( int argc, char *argv[] )
{
    int opt;
    struct option longopts[] = {
            //此结构的定义如下：
            //name：长选项的名字，缩写亦可，只要不与其它选项混淆
            //has_arg：是否带参数，0表示不带；1表示必须有一个参数；2表示有一个可选参数
            //flag：设置为NULL，那么找到该选项时，getopt_long返回val中给定的值（对应的短选项）
            //      否则，getopt_long返回0并且将val的值写入flag指向的变量
            //val：getopt_long为该选项返回的值
            { "initialize", 0, NULL, 'i' },
            { "file", 1, NULL, 'f' },
            { "list", 0, NULL, 'l' },
            { "restart", 0, NULL, 'r' },
            { 0, 0, 0, 0 }
    };
    while ( ( opt = getopt_long( argc, argv, ":if:lr", longopts, NULL ) ) != -1 )
    {
        switch ( opt )
        {
            case 'i' :
                case 'l' :
                case 'r' :
                printf( "option: %c\n", opt );
                break;
            case 'f' :
                printf( "filename: %s\n", optarg );
                break;
            case ':' :
                printf( "option needs a value\n" );
                break;
            case '?' :
                printf( "unknown option: %c\n", optopt );
                break;
        }
    }
    for ( ; optind &lt; argc; optind++ )
        printf( "argument: %s\n", argv[optind] );
    exit( 0 );
}</pre>
<div class="blog_h2"><span class="graybg">获取环境变量</span></div>
<pre class="crayon-plain-tag">#include &lt;stdlib.h&gt;
//读取环境变量
char *getenv(const char *name);
//设置环境变量，以name=value形式的字符串作为参数
int putenv(const char *string);</pre>
<p>除了上面两个方法外，可以通过environ变量直接访问环境变量：</p>
<pre class="crayon-plain-tag">#include &lt;stdlib.h&gt;
//字符串数组
extern char **environ;</pre>
<p>示例如下：</p>
<pre class="crayon-plain-tag">#include &lt;stdlib.h&gt;
#include &lt;stdio.h&gt;
extern char **environ;
int main()
{
    char **env = environ;
    while ( *env )
    {
        printf( "%s\n", *env );
        env++;
    }
    exit( 0 );
}</pre>
<div class="blog_h2"><span class="graybg">日期与时间处理</span></div>
<p> UNIX/Linux下的时间的表示，均是以格林尼治（GMT）时间1970-01-01 00:00:00为起点进行计时的，表示为从这一时间点流逝的时间。时间使用预定义类型：<pre class="crayon-plain-tag">time_t</pre> 表示，通过以下函数可以得到当前时间值（流逝的秒数）：</p>
<pre class="crayon-plain-tag">#include &lt;time.h&gt;
//如果tloc不为NULL，那么返回值同时写入到该指针
time_t time( time_t *tloc );
//把结构还原为时间值
time_t mktime(struct tm *timeptr);</pre>
<p> difftime函数可以返回两个时间点之间的差值：</p>
<pre class="crayon-plain-tag">#include &lt;time.h&gt;
//返回time1 - time2
double difftime(time_t time1, time_t time2);</pre>
<p>使用gmtime函数可以把time_t分解为一个结构：</p>
<pre class="crayon-plain-tag">#include &lt;time.h&gt;
struct tm *gmtime( const time_t timeval );
//结构成员包括：
//int tm_sec，秒0-61
//int tm_min，分0-59
//int tm_hour，时0-23
//int tm_mday，月份中的日期1-31
//int tm_mon，月份0-11
//int tm_year，年度，从1900开始算
//int tm_wday，星期几，0-6，周日为0
//int tm_yday，年份中的日期，0-365
//int tm_isdst，是否夏令时</pre>
<p>以上函数处理的都是GMT标准时间，如果需要得到当地时间，可以使用：</p>
<pre class="crayon-plain-tag">#include &lt;time.h&gt;
struct tm *localtime(const time_t *timeval);</pre>
<p>以人类易于阅读的方式输出日期，可以使用：</p>
<pre class="crayon-plain-tag">#include &lt;time.h&gt;
char *asctime( const struct tm *timeptr );
//输出格式为固定26字符：Sun Jun 9 12:34:56 2007\n\0
//下面这个函数等价于：asctime(localtime(timeval))
char *ctime( const time_t *timeval );</pre>
<p>Linux提供了strftime函数，可以对日期格式进行细致的格式化：</p>
<pre class="crayon-plain-tag">#include &lt;time.h&gt;
size_t strftime( char *s, size_t maxsize, const char *format, struct tm *timeptr );
/**
 * 其中，format为格式化字符串，类似于printf：
 * %a 星期几的缩写
 * %A 星期几的全称
 * %b 月份的缩写
 * %B 月份的全称
 * %c 日期和时间
 * %d 月份中的日期01-31
 * %H 小时0-23
 * %I 12小时制中的小时01-12
 * %j 年份中的日期，001-366
 * %m 年份中的月份，01-12
 * %M 分钟，00-59
 * %p a.m表示上午，p.m.下午
 * %S 秒，00-61
 * %u 星期几，1-7，周一为1
 * %U 一年中的第几周，01-53（周日算第一天）
 * %V 一年中的第几周，01-53（周一算第一天）
 * %w 星期几，0-6，周日为9
 * %x 本地格式的日期
 * %X 本地格式的时间
 * %y 年份-1900
 * %Y 年份
 * %Z 时区名
 * %% 字符%
 */</pre>
<div class="blog_h2"><span class="graybg">使用临时文件</span></div>
<pre class="crayon-plain-tag">#include &lt;stdio.h&gt;
//创建一个唯一的文件名，缓冲区至少L_tmpnam长
char *tmpnam( char *s );
//创建一个唯一的临时文件，以读写方式打开
FILE *tmpfile( void );

//来自UNIX的临时文件函数，字符串模板必须以XXXXXX结尾，这6个字符会被替换
#include &lt;stdlib.h&gt;
char *mktemp( char *template );
int mkstemp( char *template );</pre>
<div class="blog_h2"><span class="graybg">获取用户信息</span></div>
<p>除了init之外，所有Linux进程均是由其它程序或者用户启动的。每个程序都是<span style="background-color: #c0c0c0;">以某个用户的名义</span>去运行的，用户的内部识别方式是UID。</p>
<p>一般情况下，启动程序的用户的UID，即是程序的有效UID；如果程序的SUID位被设置，那么程序的所有者将作为其有效UID，不论谁启动该程序。</p>
<p>UID的类型是<pre class="crayon-plain-tag">uid_t</pre> ，定义在头文件<pre class="crayon-plain-tag">sys/types.h</pre> 中，是一个小整数，某些UID是系统预定义的，其它则是系统管理员添加的，一般情况下UID大于100。</p>
<pre class="crayon-plain-tag">#include &lt;sys/types.h&gt;
#include &lt;unistd.h&gt;
//得到程序关联的UID，通常是启动程序的用户的ID
uid_t getuid( void );
//返回当前用户关联的登录名
char *getlogin( void );

#include &lt;sys/types.h&gt;
#include &lt;pwd.h&gt;
//获取用户信息的标准接口，出错时返回空指针并设置errno
struct passwd *getpwuid( uid_t uid );
struct passwd *getpwnam( const char *name );
/**
 * passwd结构成员：
 * char *pw_name 用户登录名
 * uid_t pw_uid UID号
 * gid_t pw_gid GID号
 * char *pw_dir 家目录
 * char *pw_gecos 用户全名
 * char *pw_shell 默认Shell
 */</pre>
<div class="blog_h2"><span class="graybg">获取主机信息</span></div>
<p>uname命令可以获取主机的若干信息，例如主机名等。</p>
<pre class="crayon-plain-tag">#include &lt;unistd.h&gt;
//获取主机名，字符串被假设最少有namelen长，成功返回0否则返回-1
int gethostname( char *name, size_t namelen );

#include &lt;sys/utsname.h&gt;
//使用uname系统调用，可以获取更多的信息
int uname( struct utsname *name );
struct utsname
{
    char sysname[]; //操作系统名称
    char nodename[]; //主机名称
    char release[]; //操作系统的发布级别
    char version[]; //操作系统的版本号
    char machine[]; //硬件类型
};
//得到唯一的机器ID
#include &lt;unistd.h&gt;
long gethostid( void );</pre>
<div class="blog_h2"><span class="graybg">使用系统日志功能</span></div>
<p>Linux系统文件一般被存放到专用的目录，例如/usr/adm或者/var/log目录。UNIX规范通过syslog函数为所有程序产生日志信息提供了统一接口：</p>
<pre class="crayon-plain-tag">#include &lt;syslog.h&gt;
//该函数向系统的日志设施（facility）发送一条日志消息
//priority：严重级别与实施值的位或
//LOG_EMERG   紧急情况，该信息可能广播给所有用户
//LOG_ALERT   高优先级故障，例如数据库崩溃，该信息可能EMAIL管理员
//LOG_CRIT    严重错误，例如硬件故障
//LOG_ERR     错误
//LOG_WARNING 警告
//LOG_NOTICE  需要注意的特殊情况
//LOG_INFO    一般信息
//LOG_DEBUG   调试信息，该信息可能被忽略，其它级别的一般均记录日志
//
//message：基于printf风格的消息模板，%m表示当前errno
//arguments：模板参数
void syslog( int priority, const char *message, arguments...);

//其它函数
//关闭openlog打开的文件描述符
void closelog( void );
//该函数用于改变日志的表示方式，该函数会打开一个文件描述符，通过它写日志，使用syslog前并不是必须调用该函数
//ident：添加在日志信息前面的前缀

//logopt：对后续syslog调用的行为进行配置，是以下四个值的位或：
//LOG_PID    在日志信息中包含进程标识符
//LOG_CONS   如果无法记录到文件，信息被发送到控制台
//LOG_ODELAY 第一次调用syslog时才打开日志设施
//LOG_NDELAY 立即打开日志设施

//facility：后续syslog调用的默认设施值
void openlog( const char *ident, int logopt, int facility );
//设置日志掩码，来控制日志的优先级，没有在此掩码中置位的优先级，在后续syslog调用中，全部被丢弃
int setlogmask( int maskpri );</pre>
<p>syslog创建的日志包含消息头和消息体，消息体由设施值和时间导出，消息体由调用者提供。syslog使用示例如下：</p>
<pre class="crayon-plain-tag">#include &lt;syslog.h&gt;
#include &lt;stdio.h&gt;
#include &lt;unistd.h&gt;
#include &lt;stdlib.h&gt;
int main()
{
    int logmask;
    openlog( "logmask", LOG_PID | LOG_CONS, LOG_USER );
    syslog( LOG_INFO, "informative message, pid = %d", getpid() );
    syslog( LOG_DEBUG, "debug message, should appear" );
    logmask = setlogmask( LOG_UPTO( LOG_NOTICE ) ); //设置日志掩码，改变优先级
    syslog( LOG_DEBUG, "debug message, should not appear" );
    exit( 0 );
}</pre>
<div class="blog_h2"><span class="graybg">资源和限制</span></div>
<p>Linux系统上运行的程序会受到资源限制的影响，这些限制包括硬件方面的物理性限制、系统策略的限制、具体实现的限制（例如整数的长度、文件名允许的最大字符数）。</p>
<p>头文件limits.h包含了一些操作系统方面限制的显式常量：</p>
<table style="width: 100%;" border="1" cellspacing="0" cellpadding="5">
<thead>
<tr>
<td style="width: 150px; text-align: center;"> 常量</td>
<td style="text-align: center;">说明 </td>
</tr>
</thead>
<tbody>
<tr>
<td>NAME_MAX</td>
<td>文件名最大字符数</td>
</tr>
<tr>
<td>CHAR_BIT</td>
<td>char类型值的最大位数</td>
</tr>
<tr>
<td>CHAR_MAX</td>
<td>char类型的最大值</td>
</tr>
<tr>
<td>INT_MAX</td>
<td>int类型的最大值</td>
</tr>
</tbody>
</table>
<p>头文件sys/resource.h提供了资源操作方面的定义：</p>
<pre class="crayon-plain-tag">#include &lt;sys/resource.h&gt;
#include &lt;sys/time.h&gt;
/**
 * 获取优先级。默认的优先级是0，正数用于后台任务，负数使一个程序运行的更加频繁，获得更多CPU时间
 * 优先级的有效范围：-20 ~ +20
 * @param which 指明who参数的含义：PRIO_PROCESS 进程标识符；PRIO_PGRP 进程组；PRIO_USER 用户
 * @param who 标识符
 * @return 成功时返回优先级，失败返回-1并设置errno，因为-1本身是有效的优先级，所有调用函数前应当置errno为0
 */
int getpriority( int which, id_t who );
//设置优先级。普通用户只能降低优先级，不能提高
int setpriority( int which, id_t who, int priority );
/**
 * 获取资源限制
 * @param resource 资源类型，RLIMIT_开头的若干常量
 */
int getrlimit( int resource, struct rlimit *r_limit );
struct rlimit
{
    rlim_t rlim_cur;   //当前软限制，建议性的最好不要超过的限制，如果超过可能导致库函数返回错误
    rlim_t rlim_max;   //硬限制，如果被超越，系统可能发送信号终止程序
};
int setrlimit( int resource, const struct rlimit *r_limit );
/**
 * 将CPU时间信息写入到r_usage中
 * @param who RUSAGE_SELF 仅返回当前程序使用的信息；RUSAGE_CHILDREN 包括子进程的使用信息
 */
int getrusage( int who, struct rusage *r_usage );
struct rusage
{
    struct timeval ru_utime; //使用的用户时间（程序自身的指令消耗的时间）
    struct timeval ru_stime; //使用的系统时间（操作系统为程序执行所消耗的时间，包括I/O等系统调用、其它系统函数调用）
};</pre>
<div class="blog_h2"><span class="graybg">内存管理</span></div>
<p>C标准库函数可以用来分配内存：</p>
<pre class="crayon-plain-tag">#include &lt;stdlib.h&gt;
//注意，所有这些内存分配函数，都不保证两次调用分配的内存是连续的，不能假设并进行连续寻址
/**
 * 分配指定大小的内存
 */
void *malloc( size_t size );
/**
 * 用于为数组分配内存，所分配的内存全部初始化为0
 * @param number_of_elements 数组元素个数
 * @param element_size 单个元素的大小
 * @return 指向第一个元素的指针
 */
void *calloc( size_t number_of_elements, size_t element_size );
/**
 * 扩大或者减小已分配内存块的长度，可能需要移动整个内存块的位置
 * 注意，一定要使用新的指针，而不是existing_memory的指针进行后续访问
 * 如果无法调整内存块的大小，该函数返回空指针
 */
void *realloc( void *existing_memory, size_t new_size );</pre>
<p>当物理内存耗尽时，内存分配会启用交换空间（Swap space），交换空间与Windows的虚拟内存交换文件类似，但是没有需要代码处理的局部堆、全局堆、可丢弃内存段等内容——Linux内核会管理所有工作。</p>
<p>Linux实现了“按需换页的虚拟内存系统”，程序看到的内存全部是虚拟的，Linux将内存按页划分，通常每页4096字节。</p>
<p>Linux内存管理系统能保护系统的其它部分免受错误使用内存的程序的影响，每个程序只能看到属于自己的内存映像，尝试访问不属于自己的内存空间，会导致段错误（Segmentation fault）。</p>
<p>空指针操作：glibc可能允许某些空指针读操作，输出“(null)\0”；空指针写、直接空指针读都是禁止的，会导致程序终止。</p>
<p>动态分配的内存在使用完毕后，应当调用free，将内存归还系统：</p>
<pre class="crayon-plain-tag">#include &lt;stdlib.h&gt;
void free(void *ptr_to memory);</pre>
<p>free函数接受的指针必须指向由malloc、calloc、realloc调用所分配的内存。 </p>
<div class="blog_h2"><span class="graybg">操控MySQL</span></div>
<p>下面的几个函数用于连接的初始化与关闭，以及读写数据：</p>
<pre class="crayon-plain-tag">/**
 * 首先，需要使用该函数初始化MySQL连接句柄
 * @param MYSQL* 使用既有的句柄，一般NULL
 */
MYSQL *mysql_init( MYSQL * );
/**
 * 再正式连接之前，可以调用该函数提供一些选项，每次调用只能设置一个选项
 * @param option_to_set 选项，支持：
 *     const unsigned int * MYSQL_OPT_CONNECT_TIMEOUT  连接超时时间
 *     NULL MYSQL_OPT_COMPRESS 启用网络流量压缩
 *     const char * MYSQL_INIT_COMMAND 连接成功后指向的命令
 *     
 * @return 成功后返回0
 */
int mysql_options( MYSQL *connection, enum option_to_set, const char *argument );
/**
 * 然后，提供参数进行连接
 * @return 如果无法连接，返回NULL，可以通过mysql_error函数获取细节
 */
MYSQL *mysql_real_connect(
        MYSQL *connection, //指向已经由mysql_init初始化的结构
        const char *server_host, //主机名或者IP地址
        const char *sql_user_name, //MySQL用户名，为空则使用当前Linux的登录ID
        const char *sql_password, //密码，为空则只能访问不需要密码的资源
        const char *db_name, //使用的MySQL数据库
        unsigned int port_number, //如果使用默认端口，传递0
        const char *unix_socket_name, //一般传递NULL
        unsigned int flags ); //位或标记
/**
 * 执行SQL语句
 * @return 如果成功返回0
 */
int mysql_query( MYSQL *connection, const char *query );
//检查语句影响的行数
my_ulonglong mysql_affected_rows( MYSQL *connection );
//逐行提取结果集
MYSQL_RES *mysql_use_result( MYSQL *connection );
/**
 * 返回全部结果集
 * @return 如果成功，返回结果集指针，否则返回NULL
 */
MYSQL_RES *mysql_store_result( MYSQL *connection );
//上述函数调用成功后，应当调用下面的函数来获取结果集的行数
my_ulonglong mysql_num_rows( MYSQL_RES *result );
//读取下一行数据，如果没有更多的数据返回NULL
MYSQL_ROW mysql_fetch_row( MYSQL_RES *result );
//读取某个字段
MYSQL_FIELD *mysql_fetch_field( MYSQL_RES *result );
//可以在结果集中跳转到指定的偏移量，如果指定为0，那么下一次fetch_row返回第一行
void mysql_data_seek( MYSQL_RES *result, my_ulonglong offset );
//获取当前游标在结果集中的偏移量
MYSQL_ROW_OFFSET mysql_row_tell( MYSQL_RES *result );
//根据偏移量移动游标，返回先前的偏移量
MYSQL_ROW_OFFSET mysql_row_seek( MYSQL_RES *result, MYSQL_ROW_OFFSET offset );
/**
 * 返回结果集中字段的个数
 */
unsigned int mysql_field_count( MYSQL *connection );
//释放结果集相关资源
void mysql_free_result( MYSQL_RES *result );

/**
 * 最后使用完毕后，需要关闭MySQL连接，该函数调用后，MySQL结构被释放，指针失效无法再次使用
 */
void mysql_close( MYSQL *connection );

/**
 * 获取错误代码
 */
unsigned int mysql_errno( MYSQL *connection );
/**
 * 获取错误文本，这些文本存放在MySQL内部静态空间
 */
char *mysql_error( MYSQL *connection );</pre>
<p>示例代码：</p>
<pre class="crayon-plain-tag">setvbuf( stdout, NULL, _IONBF, 0 );
MYSQL* conn = mysql_init( NULL );
const unsigned int timeout = 5;
mysql_options( conn, MYSQL_OPT_CONNECT_TIMEOUT, ( char* ) &amp;timeout );
if ( !mysql_real_connect( conn, "127.0.0.1", "root", "root", "test", 0, NULL, 0 ) )
{
    printf( "Failed to connect: %s\n", mysql_error( conn ) );
}
if ( !mysql_query( conn, "INSERT INTO T_CORP (CORP_NAME, CAPI) VALUES ('Gmem Inc.', 1000.20)" ) )
{
    printf( "Inserted %lu rows", ( unsigned long ) mysql_affected_rows( conn ) );
    mysql_query( conn, "SELECT LAST_INSERT_ID()" );
    MYSQL_RES *res_ptr = mysql_use_result( conn ); //使用结果集
    MYSQL_ROW sqlrow; //代表行的字符串数组
    while ( ( sqlrow = mysql_fetch_row( res_ptr ) ) ) //循环读取结果集
    {
        printf( ", Last Corp ID: %s", sqlrow[0] );
    }
    mysql_free_result( res_ptr ); //释放结果集
}
else
{
    printf( "Failed to execute SQL: %s\n", mysql_error( conn ) );
}
mysql_close( conn );</pre>
<div class="blog_h1"><span class="graybg">内核编程</span></div>
<p>参考：<a href="linux-kernel-programming-faq">Linux内核编程知识集锦</a></p>
</div><p>The post <a rel="nofollow" href="https://blog.gmem.cc/linux-programming-faq">Linux编程知识集锦</a> appeared first on <a rel="nofollow" href="https://blog.gmem.cc">绿色记忆</a>.</p>
]]></content:encoded>
			<wfw:commentRss>https://blog.gmem.cc/linux-programming-faq/feed</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>Linux IO编程</title>
		<link>https://blog.gmem.cc/linux-io-programming</link>
		<comments>https://blog.gmem.cc/linux-io-programming#comments</comments>
		<pubDate>Fri, 19 Jun 2009 05:55:48 +0000</pubDate>
		<dc:creator><![CDATA[Alex]]></dc:creator>
				<category><![CDATA[C]]></category>
		<category><![CDATA[Linux]]></category>
		<category><![CDATA[IO编程]]></category>
		<category><![CDATA[Linux编程]]></category>

		<guid isPermaLink="false">http://blog.gmem.cc/?p=6830</guid>
		<description><![CDATA[<p>文件访问 Linux系统中每个运行的进程，具有与之关联的文件描述符，通过这些描述符可以访问打开的文件或者设备。当一个进程打开时，一般会有三个已经打开的文件描述符：  描述符 说明  0 代表标准输入 1  代表标准输出  2 代表标准错误 系统调用 下表是与文件访问有关的系统调用 系统调用  说明  write 将缓冲区buf的前n个字节写入到文件描述符fildes关联的文件中，返回实际写入的字节数，如果底层设备对数据块长度比较敏感，返回值可能小于n。如果出现错误，返回-1，可以通过全局变量errno访问错误代码。 函数原型：[crayon-69e74a6c83193582129390-i/]  示例代码： [crayon-69e74a6c83198032362265/] read  从文件描述符fildes关联的文件中读取n字节数据，并把它们放入缓冲区中，返回实际读入的字节数。如果返回0表示未读入任何数据，已经到达文件结尾；如果返回-1表示出错。 函数原型：[crayon-69e74a6c8319a692442954-i/]  <a class="read-more" href="https://blog.gmem.cc/linux-io-programming">[...]</a></p>
<p>The post <a rel="nofollow" href="https://blog.gmem.cc/linux-io-programming">Linux IO编程</a> appeared first on <a rel="nofollow" href="https://blog.gmem.cc">绿色记忆</a>.</p>
]]></description>
				<content:encoded><![CDATA[<div class="wri_content_clear_both"><div class="blog_h2"><span class="graybg">文件访问</span></div>
<p>Linux系统中每个运行的进程，具有与之关联的<span style="background-color: #c0c0c0;">文件描述符</span>，通过这些描述符可以<span style="background-color: #c0c0c0;">访问打开的文件或者设备</span>。当一个进程打开时，一般会有三个已经打开的文件描述符：</p>
<table style="width: 100%;" border="1" cellspacing="0" cellpadding="5">
<thead>
<tr>
<td style="width: 100px; text-align: center;"> 描述符</td>
<td style="text-align: center;">说明 </td>
</tr>
</thead>
<tbody>
<tr>
<td>0</td>
<td>代表标准输入</td>
</tr>
<tr>
<td>1 </td>
<td>代表标准输出 </td>
</tr>
<tr>
<td>2</td>
<td>代表标准错误</td>
</tr>
</tbody>
</table>
<div class="blog_h3"><span class="graybg">系统调用</span></div>
<p>下表是与文件访问有关的系统调用</p>
<table style="width: 100%;" border="1" cellspacing="0" cellpadding="5">
<thead>
<tr>
<td style="width: 100px; text-align: center;">系统调用 </td>
<td style="text-align: center;">说明 </td>
</tr>
</thead>
<tbody>
<tr>
<td>write</td>
<td>
<p>将缓冲区buf的前n个字节写入到文件描述符fildes关联的文件中，返回实际写入的字节数，如果底层设备对数据块长度比较敏感，返回值可能小于n。如果出现错误，返回-1，可以通过全局变量errno访问错误代码。</p>
<p>函数原型：<pre class="crayon-plain-tag">size_t write( int fildes, const void *buf, size_t nbytes );</pre> </p>
<p>示例代码：</p>
<pre class="crayon-plain-tag">#include &lt;unistd.h&gt;
write( 1, "stdout", 6 );
write( 2, "stderr", 6 );</pre>
</td>
</tr>
<tr>
<td>read </td>
<td>
<p>从文件描述符fildes关联的文件中读取n字节数据，并把它们放入缓冲区中，返回实际读入的字节数。如果返回0表示未读入任何数据，已经到达文件结尾；如果返回-1表示出错。
<p>函数原型：<pre class="crayon-plain-tag">size_t read( int fildes, void *buf, size_t nbytes );</pre> </p>
<p>示例代码：</p>
<pre class="crayon-plain-tag">char buf[128];
int nread;
//阻塞的从标准输入中读取数据
nread = read( 0, buf, 16 );
write( 2, buf, nread );</pre>
</td>
</tr>
<tr>
<td>open</td>
<td>
<p>该函数用于创建一个新的文件描述符。该调用会返回一个文件描述符，其它进程即使打开同一个文件，也不会使用相同的描述符。如果两个进程同时写一个文件，那么写入的内容可能会相互覆盖（两个进程读写偏移量独立维护）。
<p>函数原型：</p>
<pre class="crayon-plain-tag">#include &lt;fcntl.h&gt;
//严格的说， 在遵守POSIX规范的系统上，下面两个头文件不需要包含
#include &lt;sys/types.h&gt;
#include &lt;sys/stat.h&gt;
//oflags可以指定打开模式
//O_RDONLY   只读
//O_WRONLY   只写
//O_RDWR     读写
//oflags还可以按位或以下选项：
//O_APPEND   写入数据追加在文件结尾
//O_TRUNC    设置文件长度为0，丢弃已有内容
//O_CREAT    如果需要，根据mode中给出的模式创建文件
//O_EXCL     与O_CREAT联用，防止其它进程创建同一个文件
//           如果其它进程创建同一文件，该调用将失败
int open(const char *path, int oflags);
//mode在使用O_CREAT选项时有意义，代表文件的模式（权限）
//模式由S_开头的若干常量来表示，使用时按位或
// S_ISUID 04000 文件的 (set user-id on execution)位
// S_ISGID 02000 文件的 (set group-id on execution)位
// S_ISVTX 01000 文件的sticky 位
// S_IRUSR (S_IREAD) 00400 文件所有者具可读取权限
// S_IWUSR (S_IWRITE)00200 文件所有者具可写入权限
// S_IXUSR (S_IEXEC) 00100 文件所有者具可执行权限
// S_IRGRP 00040 用户组具可读取权限
// S_IWGRP 00020 用户组具可写入权限
// S_IXGRP 00010 用户组具可执行权限
// S_IROTH 00004 其他用户具可读取权限
// S_IWOTH 00002 其他用户具可写入权限
// S_IXOTH 00001 其他用户具可执行权限
int open(const char *path, int oflags, mode_t mode);</pre>
<p>open调用在成功时返回一个非负整数，作为文件描述符。失败时返回-1并设置全局变量errno。新文件描述符总是使用<span style="background-color: #c0c0c0;">未用描述符的最小值</span>。该特性被用于重定向：关闭标准输出后，再次调用open，则文件描述符1被重新使用，从而实现重定向。</p>
<p>任何运行中的程序能够打开的文件数量是有限制的，该限制在头文件limits.h中的OPEN_MAX定义，POSIX要求最少可以打开16个，在Linux中，该限制可以在运行时动态调整，因此OPEN_MAX是变量</p>
</td>
</tr>
<tr>
<td>close</td>
<td>
<p>可以终止文件描述符与对应文件的关联，文件描述符被释放，可以重新使用。成功关闭返回0，否则返回-1</p>
</td>
</tr>
<tr>
<td>lseek</td>
<td>
<p>可以对文件描述符的读写指针进行设置，即设置文件的读写位置。返回文件头到指针设置处的字节偏移量，失败时返回-1</p>
<p>函数原型：</p>
<pre class="crayon-plain-tag">#include &lt;unistd.h&gt;
#include &lt;sys/types.h&gt;
//whence可以为：
// SEEK_SET: 表示偏移量为绝对位置
// SEEK_CUR: 表示偏移量是相对于当前位置的位置
// SEEK_END: 表示偏移量是相对于文件结尾的位置
off_t lseek( int fildes, off_t offset, int whence );</pre>
</td>
</tr>
<tr>
<td>fstat<br />stat<br />lstat</td>
<td>
<p>返回与打开文件描述符关联的文件的状态信息
<p>函数原型：</p>
<pre class="crayon-plain-tag">#include &lt;unistd.h&gt;
#include &lt;sys/stat.h&gt;
#include &lt;sys/types.h&gt;
//该函数通过描述符定位文件
int fstat( int fildes, struct stat *buf );
//下面两个函数通过文件路径定位
int stat( const char *path, struct stat *buf );
//该函数与stat类似，但是如果文件是符号链接，它会
//返回符号链接本身的信息，而stat返回链接目标的信息
int lstat( const char *path, struct stat *buf );</pre>
</td>
</tr>
<tr>
<td>dup<br />dup2</td>
<td>
<p>用于复制文件描述符，可以让多个文件描述符指向同一文件，从而允许在文件的不同位置进行读写。通过管道在多个进程间进行通信时，这些调用很有用
<p>函数原型：</p>
<pre class="crayon-plain-tag">#include &lt;unistd.h&gt;
//复制并返回新的文件描述符
int dup( int fildes );
//复制文件描述符为fildes2
int dup2( int fildes, int fildes2 );</pre>
</td>
</tr>
</tbody>
</table>
<div id="stdio" class="blog_h3"><span class="graybg">标准I/O库</span></div>
<p>标准I/O库（stdio）及其头文件stdio.h为底层I/O系统提供通用的对外接口，现在这个库已经是ANSI C标准库的一部分。标准I/O库提供多种复杂的函数用于格式化输出、扫描输入，还负责设备缓冲的处理。
<p>在标准I/O库中，操控文件的方式与系统调用类似，其中与底层文件描述符对应的是流（Stream），被实现为指向结构体FILE的指针。</p>
<p>在程序启动时，stdin、stdout、stderr这三个文件已经打开。</p>
<p>标准I/O库关于文件读写API的使用，参考<a href="/c-study-note#file-io">Linux I/O编程</a></p>
<div class="blog_h2"><span class="graybg">目录管理</span></div>
<p>相关系统调用或函数：</p>
<table style="width: 100%;" border="1" cellspacing="0" cellpadding="5">
<thead>
<tr>
<td style="width: 150px; text-align: center;"> 系统调用</td>
<td style="text-align: center;">说明 </td>
</tr>
</thead>
<tbody>
<tr>
<td>chmod</td>
<td>修改目录或者文件的模式（权限），是chmod命令的基础：<br />
<pre class="crayon-plain-tag">#include &lt;sys/stat.h&gt;
int chmod(const char *path, mode_t mode);</pre>
</td>
</tr>
<tr>
<td>chown</td>
<td>超级用户可以修改文件的所有者：<br />
<pre class="crayon-plain-tag">#include &lt;sys/types.h&gt;
#include &lt;unistd.h&gt;
int chown(const char *path, uid_t owner, gid_t group);
//支持数字格式的uid、gid，这些ID可以通过getuid、getgid获得</pre>
</td>
</tr>
<tr>
<td>unlink<br />link<br />symlink</td>
<td>
<p>unlink 可以用来删除文件，该调用减少文件的链接数，成功返回0否则返回-1，要求用户具有文件所属目录的写和执行权限。如果链接数为0且没有进程打开之，则文件就会被删除<br />link 创建指向已有文件的新链接，新的目录项由path2参数给出<br />symlink 创建指向已有文件的符号链接，该系统调用不会增加目标文件的链接数</p>
<pre class="crayon-plain-tag">#include &lt;unistd.h&gt;
int unlink(const char *path);
int link(const char *path1, const char *path2);
int symlink(const char *path1, const char *path2);

//创建临时文件的技巧，创建后立即unlink
open(...);
unlink(...);
//该文件会在当前进程退出后自动清理删除</pre>
</td>
</tr>
<tr>
<td>mkdir<br />rmdir</td>
<td> 分别用于创建和删除目录，rmdir只有在目录为空的时候才能删除<br />
<pre class="crayon-plain-tag">#include &lt;sys/types.h&gt;
#include &lt;sys/stat.h&gt;
int mkdir(const char *path, mode_t mode);

#include &lt;unistd.h&gt;
int rmdir(const char *path);</pre>
</td>
</tr>
<tr>
<td>chdir</td>
<td>修改当前目录：<br />
<pre class="crayon-plain-tag">#include &lt;unistd.h&gt;
int chdir(const char *path);</pre>
</td>
</tr>
<tr>
<td style="text-align: center;"><strong>函数</strong></td>
<td style="text-align: center;"><strong>说明</strong></td>
</tr>
<tr>
<td>getcwd</td>
<td>获取当前工作目录，如果缓冲区长度不够，返回NULL，否则返回缓冲区<br />
<pre class="crayon-plain-tag">#include &lt;unistd.h&gt;
char *getcwd(char *buf, size_t size);</pre>
</td>
</tr>
</tbody>
</table>
<div class="blog_h2"><span class="graybg">目录扫描</span></div>
<p>虽然可以将目录作为普通文件一样打开读写，但是这种方式不具有可移植性。Linux下用于目录访问的标准库函数定义在<span style="background-color: #c0c0c0;">dirent.h</span>头中，这些函数使用结构体<span style="background-color: #c0c0c0;">DIR</span>作为目录操作的基础，DIR*被称为<span style="background-color: #c0c0c0;">目录流</span>。这些函数中常用的包括：
<table style="width: 100%;" border="1" cellspacing="0" cellpadding="5">
<thead>
<tr>
<td style="width: 150px; text-align: center;"> 函数</td>
<td style="text-align: center;">说明 </td>
</tr>
</thead>
<tbody>
<tr>
<td>opendir</td>
<td>
<p>opendir打开一个目录并建立目录流，如果失败，返回NULL，在底层，目录流打开目录的文件描述符</p>
<pre class="crayon-plain-tag">#include &lt;sys/types.h&gt;
#include &lt;dirent.h&gt;
DIR *opendir(const char *name);</pre>
</td>
</tr>
<tr>
<td>readdir</td>
<td>
<p>返回一个包含了下一个目录项信息的dirent指针，每次调用返回下一个，遇到目录结尾则返回NULL。如果在迭代期间，其他进程创建/删除了文件，那么readdir不能保证所有文件被列出。
<p>dirent指针包含两个数据项：</p>
<p><pre class="crayon-plain-tag">ino_t d_ino;/*文件Inode节点号*/char d_name[];/*文件的名字*/</pre> </p>
<pre class="crayon-plain-tag">#include &lt;sys/types.h&gt;
#include &lt;dirent.h&gt;
struct dirent *readdir(DIR *dirp);</pre>
</td>
</tr>
<tr>
<td>telldir</td>
<td>
<p>返回当前目录流迭代的位置，可以供后续seekdir调用进行重置
<pre class="crayon-plain-tag">#include &lt;sys/types.h&gt;
#include &lt;dirent.h&gt;
long int telldir(DIR *dirp);</pre>
</td>
</tr>
<tr>
<td>seekdir</td>
<td>
<p>设置目录流指针的位置
<pre class="crayon-plain-tag">#include &lt;sys/types.h&gt;
#include &lt;dirent.h&gt;
void seekdir(DIR *dirp, long int loc);</pre>
</td>
</tr>
<tr>
<td>closedir</td>
<td>
<p> 关闭目录流，并释放与之相关的资源
<pre class="crayon-plain-tag">#include &lt;sys/types.h&gt;
#include &lt;dirent.h&gt;
int closedir(DIR *dirp);</pre>
</td>
</tr>
</tbody>
</table>
<div class="blog_h2"><span class="graybg">错误处理</span></div>
<p>很多文件I/O函数在失败后会设置外部变量errno的值，以提示失败的原因，程序必须在函数报告出错后立即检查errno，因为它可能被下一个函数调用覆盖。常见的错误代码包括：
<table style="width: 100%;" border="1" cellspacing="0" cellpadding="5">
<thead>
<tr>
<td style="width: 150px; text-align: center;">错误代码</td>
<td style="text-align: center;">说明 </td>
</tr>
</thead>
<tbody>
<tr>
<td> EPERM</td>
<td>操作不允许</td>
</tr>
<tr>
<td> ENOENT</td>
<td>文件或目录不存在</td>
</tr>
<tr>
<td>EINTR</td>
<td>系统调用被中断</td>
</tr>
<tr>
<td>EIO</td>
<td>I/O错误</td>
</tr>
<tr>
<td>EBUSY</td>
<td>设备或资源忙</td>
</tr>
<tr>
<td>EEXIST</td>
<td>文件存在</td>
</tr>
<tr>
<td>EINVAL</td>
<td>无效参数</td>
</tr>
<tr>
<td>EMFILE</td>
<td>打开的文件过多</td>
</tr>
<tr>
<td>ENODEV</td>
<td>设备不存在</td>
</tr>
<tr>
<td>EISDIR</td>
<td>是一个目录</td>
</tr>
<tr>
<td>ENOTDIR</td>
<td>不是一个目录</td>
</tr>
</tbody>
</table>
<p>下面两个函数用于错误代码的处理：</p>
<table style="width: 100%;" border="1" cellspacing="0" cellpadding="5">
<thead>
<tr>
<td style="width: 150px; text-align: center;">函数</td>
<td style="text-align: center;">说明 </td>
</tr>
</thead>
<tbody>
<tr>
<td>strerror</td>
<td>将整型错误代码转换为字符串表示：<br />
<pre class="crayon-plain-tag">#include &lt;string.h&gt;
char *strerror(int errnum);</pre>
</td>
</tr>
<tr>
<td>perror</td>
<td>将当前错误的字符串形式添加到缓冲区后面，在前缀后面添加冒号和空格：<br />
<pre class="crayon-plain-tag">#include &lt;stdio.h&gt;
void perror(const char *s);</pre>
</td>
</tr>
</tbody>
</table>
<div class="blog_h2"><span class="graybg">其它主题</span></div>
<div class="blog_h3"><span class="graybg">fcntl系统调用</span></div>
<p>该系统调用允许对底层的文件描述符进行更加细致的控制：</p>
<pre class="crayon-plain-tag">#include &lt;fcntl.h&gt;
int fcntl(int fildes, int cmd);
int fcntl(int fildes, int cmd, long arg);

//cmd表示需要执行的动作：
//F_DUPFD 复制并返回一个新的文件描述符
//F_GETFD 返回fcntl.h中定义的文件描述符标记
//F_SETFD 设置文件描述符标记
//F_GETFL 获取文件状态标记、访问模式</pre>
<div class="blog_h3"><span class="graybg">mmap函数</span></div>
<p>内存映射。该函数用于创建一段供多个程序共享的内存，其中一个程序对其的修改，另外一个程序会立即看到。</p>
<p>该函数创建了一个指向了一段内存区域的指针，该内存区域与文件描述符指向的文件的内容关联：</p>
<pre class="crayon-plain-tag">#include &lt;sys/mman.h&gt;
void *mmap(void *addr, size_t len, int prot, int flags, int fildes, off_t off);
//addr 起始内存地址，如果设置为0，则自动分配
//len 共享内存的长度
//prot设置共享内存的访问权限，以下位或：
//PROT_READ 该内存段可读
//PROT_WRITE 该内存段可写
//PROT_EXEC 该内存段可执行
//PROT_NONE 该内存段不能被访问
//flags控制程序对内存段的改变造成的影响：
//MAP_PRIVATE 内存段是私有的，修改是本地的，仅对当前进程有效
//MAP_SHARED 对内存段的修改被保留到磁盘
//MAP_FIXED 该段必须位于addr指定的地址
//fildes 共享内存关联的文件描述符
//off 共享内存访问文件内容的偏移值</pre>
<p>使用msync函数可以把内存段中的部分或者全部写回到被映射的文件中，或者从文件中读出：</p>
<pre class="crayon-plain-tag">#include &lt;sys/mman.h&gt;
int msync(void *addr, size_t len, int flags);
//addr 起始地址
//len 长度
//flags 标记位：
//MS_ASYNC 执行异步写
//MS_SYNC 执行同步写
//MS_INVALIDATE 把数据读回到内存段</pre>
<p>使用munmap函数可以释放内存段：</p>
<pre class="crayon-plain-tag">#include &lt;sys/mman.h&gt;
int munmap(void *addr, size_t len);</pre>
<div class="blog_h3"><span class="graybg">select系统调用</span></div>
<p>该系统调用在Linux下不仅仅可以用于网络I/O，普通文件I/O也被支持：</p>
<pre class="crayon-plain-tag">#include &lt;sys/types.h&gt;
#include &lt;sys/time.h&gt;
#include &lt;stdio.h&gt;
#include &lt;fcntl.h&gt;
#include &lt;sys/ioctl.h&gt;
#include &lt;unistd.h&gt;
#include &lt;stdlib.h&gt;
int main()
{
    char buffer[128];
    int result, nread;
    fd_set inputs, testfds;
    struct timeval timeout;
    FD_ZERO( &amp;inputs ); //初始化为空白文件描述符集
    FD_SET( 0, &amp;inputs ); //设置标准输入
    while ( 1 )
    {
        testfds = inputs;
        timeout.tv_sec = 2;
        timeout.tv_usec = 500000;
        //等待标准输入上具有数据可读，超时2.5秒
        result = select( FD_SETSIZE, &amp;testfds, ( fd_set * ) NULL, ( fd_set * ) NULL, &amp;timeout );
        switch ( result )
        {
            case 0 :
                printf( "超时\n" );
                break;
            case -1 :
                perror( "出错" );
                exit( 1 );
            default :
                if ( FD_ISSET( 0, &amp;testfds ) )
                {
                    //标准输入已经就绪，可以读取
                }
                break;
        }
    }
}</pre>
</div><p>The post <a rel="nofollow" href="https://blog.gmem.cc/linux-io-programming">Linux IO编程</a> appeared first on <a rel="nofollow" href="https://blog.gmem.cc">绿色记忆</a>.</p>
]]></content:encoded>
			<wfw:commentRss>https://blog.gmem.cc/linux-io-programming/feed</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>Linux网络编程</title>
		<link>https://blog.gmem.cc/linux-network-programming</link>
		<comments>https://blog.gmem.cc/linux-network-programming#comments</comments>
		<pubDate>Thu, 18 Jun 2009 11:53:21 +0000</pubDate>
		<dc:creator><![CDATA[Alex]]></dc:creator>
				<category><![CDATA[C]]></category>
		<category><![CDATA[Linux]]></category>
		<category><![CDATA[Linux编程]]></category>
		<category><![CDATA[网络编程]]></category>

		<guid isPermaLink="false">https://blog.gmem.cc/?p=25263</guid>
		<description><![CDATA[<p>系统编程接口 错误码 代码 名字 说明 0   Success 1 EPERM Operation not permitted 2 ENOENT No such file or directory 3 ESRCH <a class="read-more" href="https://blog.gmem.cc/linux-network-programming">[...]</a></p>
<p>The post <a rel="nofollow" href="https://blog.gmem.cc/linux-network-programming">Linux网络编程</a> appeared first on <a rel="nofollow" href="https://blog.gmem.cc">绿色记忆</a>.</p>
]]></description>
				<content:encoded><![CDATA[<div class="wri_content_clear_both"><div class="blog_h1"><span class="graybg">系统编程接口</span></div>
<div class="blog_h2"><span class="graybg"><a id="skt-enos"></a>错误码</span></div>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 50px; text-align: center;">代码</td>
<td style="width: 100px; text-align: center;">名字</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>0</td>
<td> </td>
<td>Success</td>
</tr>
<tr>
<td>1</td>
<td>EPERM</td>
<td>Operation not permitted</td>
</tr>
<tr>
<td>2</td>
<td>ENOENT</td>
<td>No such file or directory</td>
</tr>
<tr>
<td>3</td>
<td>ESRCH</td>
<td>No such process</td>
</tr>
<tr>
<td>4</td>
<td>EINTR</td>
<td>Interrupted system call</td>
</tr>
<tr>
<td>5</td>
<td>EIO</td>
<td>Input/output error</td>
</tr>
<tr>
<td>6</td>
<td>ENXIO</td>
<td>No such device or address</td>
</tr>
<tr>
<td>7</td>
<td>E2BIG</td>
<td>Argument list too long</td>
</tr>
<tr>
<td>8</td>
<td>ENOEXEC</td>
<td>Exec format error</td>
</tr>
<tr>
<td>9</td>
<td>EBADF</td>
<td>Bad file descriptor</td>
</tr>
<tr>
<td>10</td>
<td>ECHILD</td>
<td>No child processes</td>
</tr>
<tr>
<td>11</td>
<td>EAGAIN</td>
<td>
<p>Try again  / Resource temporarily unavailable</p>
<p>提示操作将被阻塞，但是请求了非阻塞操作</p>
<p>对于send()，可能原因包括：</p>
<ol>
<li>使用<pre class="crayon-plain-tag">fcntl()</pre>显式的将文件描述符标记为非阻塞的</li>
<li>传递<pre class="crayon-plain-tag">MSG_DONTWAIT</pre>标记给<pre class="crayon-plain-tag">send()</pre></li>
<li>使用套接字选项<pre class="crayon-plain-tag">SO_SNDTIMEO</pre>设置了发送超时</li>
<li>发送的消息无法放入（满了）套接字缓冲时，send()默认会阻塞。但是如果套接字被设置为non-blocking I/O模式，则会返回EAGAIN</li>
</ol>
</td>
</tr>
<tr>
<td>12</td>
<td>ENOMEM</td>
<td>Cannot allocate memory</td>
</tr>
<tr>
<td>13</td>
<td>EACCES</td>
<td>Permission denied</td>
</tr>
<tr>
<td>14</td>
<td>EFAULT</td>
<td>Bad address</td>
</tr>
<tr>
<td>15</td>
<td>ENOTBLK</td>
<td>Block device required</td>
</tr>
<tr>
<td>16</td>
<td>EBUSY</td>
<td>Device or resource busy</td>
</tr>
<tr>
<td>17</td>
<td>EEXIST</td>
<td>File exists</td>
</tr>
<tr>
<td>18</td>
<td>EXDEV</td>
<td>Invalid cross-device link</td>
</tr>
<tr>
<td>19</td>
<td>ENODEV</td>
<td>No such device</td>
</tr>
<tr>
<td>20</td>
<td>ENOTDIR</td>
<td>Not a directory</td>
</tr>
<tr>
<td>21</td>
<td>EISDIR</td>
<td>Is a directory</td>
</tr>
<tr>
<td>22</td>
<td>EINVAL</td>
<td>Invalid argument</td>
</tr>
<tr>
<td>23</td>
<td>ENFILE</td>
<td>Too many open files in system</td>
</tr>
<tr>
<td>24</td>
<td>EMFILE</td>
<td>Too many open files</td>
</tr>
<tr>
<td>25</td>
<td>ENOTTY</td>
<td>Inappropriate ioctl for device</td>
</tr>
<tr>
<td>26</td>
<td>ETXTBSY</td>
<td>Text file busy</td>
</tr>
<tr>
<td>27</td>
<td>EFBIG</td>
<td>File too large</td>
</tr>
<tr>
<td>28</td>
<td>ENOSPC</td>
<td>No space left on device</td>
</tr>
<tr>
<td>29</td>
<td>ESPIPE</td>
<td>Illegal seek</td>
</tr>
<tr>
<td>30</td>
<td>EROFS</td>
<td>Read-only file system</td>
</tr>
<tr>
<td>31</td>
<td>EMLINK</td>
<td>Too many links</td>
</tr>
<tr>
<td>32</td>
<td>EPIPE</td>
<td>Broken pipe</td>
</tr>
<tr>
<td>33</td>
<td>EDOM</td>
<td>Numerical argument out of domain</td>
</tr>
<tr>
<td>34</td>
<td>ERANGE</td>
<td>Numerical result out of range</td>
</tr>
<tr>
<td>35</td>
<td> </td>
<td>Resource deadlock avoided</td>
</tr>
<tr>
<td>36</td>
<td> </td>
<td>File name too long</td>
</tr>
<tr>
<td>37</td>
<td> </td>
<td>No locks available</td>
</tr>
<tr>
<td>38</td>
<td> </td>
<td>Function not implemented</td>
</tr>
<tr>
<td>39</td>
<td> </td>
<td>Directory not empty</td>
</tr>
<tr>
<td>40</td>
<td> </td>
<td>Too many levels of symbolic links</td>
</tr>
<tr>
<td>41</td>
<td> </td>
<td>Unknown error 41</td>
</tr>
<tr>
<td>42</td>
<td> </td>
<td>No message of desired type</td>
</tr>
<tr>
<td>43</td>
<td> </td>
<td>Identifier removed</td>
</tr>
<tr>
<td>44</td>
<td> </td>
<td>Channel number out of range</td>
</tr>
<tr>
<td>45</td>
<td> </td>
<td>Level 2 not synchronized</td>
</tr>
<tr>
<td>46</td>
<td> </td>
<td>Level 3 halted</td>
</tr>
<tr>
<td>47</td>
<td> </td>
<td>Level 3 reset</td>
</tr>
<tr>
<td>48</td>
<td> </td>
<td>Link number out of range</td>
</tr>
<tr>
<td>49</td>
<td> </td>
<td>Protocol driver not attached</td>
</tr>
<tr>
<td>50</td>
<td> </td>
<td>No CSI structure available</td>
</tr>
<tr>
<td>51</td>
<td> </td>
<td>Level 2 halted</td>
</tr>
<tr>
<td>52</td>
<td> </td>
<td>Invalid exchange</td>
</tr>
<tr>
<td>53</td>
<td> </td>
<td>Invalid request descriptor</td>
</tr>
<tr>
<td>54</td>
<td> </td>
<td>Exchange full</td>
</tr>
<tr>
<td>55</td>
<td> </td>
<td>No anode</td>
</tr>
<tr>
<td>56</td>
<td> </td>
<td>Invalid request code</td>
</tr>
<tr>
<td>57</td>
<td> </td>
<td>Invalid slot</td>
</tr>
<tr>
<td>58</td>
<td> </td>
<td>Unknown error 58</td>
</tr>
<tr>
<td>59</td>
<td> </td>
<td>Bad font file format</td>
</tr>
<tr>
<td>60</td>
<td> </td>
<td>Device not a stream</td>
</tr>
<tr>
<td>61</td>
<td> </td>
<td>No data available</td>
</tr>
<tr>
<td>62</td>
<td> </td>
<td>Timer expired</td>
</tr>
<tr>
<td>63</td>
<td> </td>
<td>Out of streams resources</td>
</tr>
<tr>
<td>64</td>
<td> </td>
<td>Machine is not on the network</td>
</tr>
<tr>
<td>65</td>
<td> </td>
<td>Package not installed</td>
</tr>
<tr>
<td>66</td>
<td> </td>
<td>Object is remote</td>
</tr>
<tr>
<td>67</td>
<td> </td>
<td>Link has been severed</td>
</tr>
<tr>
<td>68</td>
<td> </td>
<td>Advertise error</td>
</tr>
<tr>
<td>69</td>
<td> </td>
<td>Srmount error</td>
</tr>
<tr>
<td>70</td>
<td> </td>
<td>Communication error on send</td>
</tr>
<tr>
<td>71</td>
<td> </td>
<td>Protocol error</td>
</tr>
<tr>
<td>72</td>
<td> </td>
<td>Multihop attempted</td>
</tr>
<tr>
<td>73</td>
<td> </td>
<td>RFS specific error</td>
</tr>
<tr>
<td>74</td>
<td> </td>
<td>Bad message</td>
</tr>
<tr>
<td>75</td>
<td> </td>
<td>Value too large for defined data type</td>
</tr>
<tr>
<td>76</td>
<td> </td>
<td>Name not unique on network</td>
</tr>
<tr>
<td>77</td>
<td> </td>
<td>File descriptor in bad state</td>
</tr>
<tr>
<td>78</td>
<td> </td>
<td>Remote address changed</td>
</tr>
<tr>
<td>79</td>
<td> </td>
<td>Can not access a needed shared library</td>
</tr>
<tr>
<td>80</td>
<td> </td>
<td>Accessing a corrupted shared library</td>
</tr>
<tr>
<td>81</td>
<td> </td>
<td>.lib section in a.out corrupted</td>
</tr>
<tr>
<td>82</td>
<td> </td>
<td>Attempting to link in too many shared libraries</td>
</tr>
<tr>
<td>83</td>
<td> </td>
<td>Cannot exec a shared library directly</td>
</tr>
<tr>
<td>84</td>
<td> </td>
<td>Invalid or incomplete multibyte or wide character</td>
</tr>
<tr>
<td>85</td>
<td> </td>
<td>Interrupted system call should be restarted</td>
</tr>
<tr>
<td>86</td>
<td> </td>
<td>Streams pipe error</td>
</tr>
<tr>
<td>87</td>
<td> </td>
<td>Too many users</td>
</tr>
<tr>
<td>88</td>
<td> </td>
<td>Socket operation on non-socket</td>
</tr>
<tr>
<td>89</td>
<td> </td>
<td>Destination address required</td>
</tr>
<tr>
<td>90</td>
<td> </td>
<td>Message too long</td>
</tr>
<tr>
<td>91</td>
<td> </td>
<td>Protocol wrong type for socket</td>
</tr>
<tr>
<td>92</td>
<td> </td>
<td>Protocol not available</td>
</tr>
<tr>
<td>93</td>
<td> </td>
<td>Protocol not supported</td>
</tr>
<tr>
<td>94</td>
<td> </td>
<td>Socket type not supported</td>
</tr>
<tr>
<td>95</td>
<td> </td>
<td>Operation not supported</td>
</tr>
<tr>
<td>96</td>
<td> </td>
<td>Protocol family not supported</td>
</tr>
<tr>
<td>97</td>
<td> </td>
<td>Address family not supported by protocol</td>
</tr>
<tr>
<td>98</td>
<td> </td>
<td>Address already in use</td>
</tr>
<tr>
<td>99</td>
<td> </td>
<td>Cannot assign requested address</td>
</tr>
<tr>
<td>100</td>
<td> </td>
<td>Network is down</td>
</tr>
<tr>
<td>101</td>
<td> </td>
<td>Network is unreachable</td>
</tr>
<tr>
<td>102</td>
<td> </td>
<td>Network dropped connection on reset</td>
</tr>
<tr>
<td>103</td>
<td> </td>
<td>Software caused connection abort</td>
</tr>
<tr>
<td>104</td>
<td> </td>
<td>Connection reset by peer</td>
</tr>
<tr>
<td>105</td>
<td> </td>
<td>No buffer space available</td>
</tr>
<tr>
<td>106</td>
<td> </td>
<td>Transport endpoint is already connected</td>
</tr>
<tr>
<td>107</td>
<td> </td>
<td>Transport endpoint is not connected</td>
</tr>
<tr>
<td>108</td>
<td> </td>
<td>Cannot send after transport endpoint shutdown</td>
</tr>
<tr>
<td>109</td>
<td> </td>
<td>Too many references: cannot splice</td>
</tr>
<tr>
<td>110</td>
<td> </td>
<td>Connection timed out</td>
</tr>
<tr>
<td>111</td>
<td> </td>
<td>Connection refused</td>
</tr>
<tr>
<td>112</td>
<td> </td>
<td>Host is down</td>
</tr>
<tr>
<td>113</td>
<td> </td>
<td>No route to host</td>
</tr>
<tr>
<td>114</td>
<td> </td>
<td>Operation already in progress</td>
</tr>
<tr>
<td>115</td>
<td> </td>
<td>Operation now in progress</td>
</tr>
<tr>
<td>116</td>
<td> </td>
<td>Stale NFS file handle</td>
</tr>
<tr>
<td>117</td>
<td> </td>
<td>Structure needs cleaning</td>
</tr>
<tr>
<td>118</td>
<td> </td>
<td>Not a XENIX named type file</td>
</tr>
<tr>
<td>119</td>
<td> </td>
<td>No XENIX semaphores available</td>
</tr>
<tr>
<td>120</td>
<td> </td>
<td>Is a named type file</td>
</tr>
<tr>
<td>121</td>
<td> </td>
<td>Remote I/O error</td>
</tr>
<tr>
<td>122</td>
<td> </td>
<td>Disk quota exceeded</td>
</tr>
<tr>
<td>123</td>
<td> </td>
<td>No medium found</td>
</tr>
<tr>
<td>124</td>
<td> </td>
<td>Wrong medium type</td>
</tr>
</tbody>
</table>
<div class="blog_h2"><span class="graybg">套接字选项</span></div>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 20%; text-align: center;">选项</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>SO_REUSEADDR</td>
<td>
<p>当绑定源地址的时候，可以通过绑定到0.0.0.0:port来<span style="background-color: #c0c0c0;">绑定到所有本地网络地址的对应端口</span>上，也可以绑定到10.0.0.1:port来绑定到特定本地网络地址的端口</p>
<p>在默认设置下，没有socket能够绑定到同一地址的同一端口。比如在Socket A已经绑定了0.0.0.0:80以后，Socket B若是想要绑定10.0.0.0.1:80，那就会报<pre class="crayon-plain-tag">EADDRINUSE</pre>。因为Socket A已经绑定了所有ip地址的80端口，包括10.0.0.1:8000</p>
<p>如果Socket B设置了选项SO_REUSEADDR，那么：</p>
<ol>
<li>它不可以绑定0.0.0.0:80</li>
<li>它可以绑定10.0.0.0.1:80， 也就是说，除非有<span style="background-color: #c0c0c0;">Socket已经绑定到和它字面上一模一样的地址，才报错</span></li>
</ol>
<p>此外SO_REUSEADDR还可以<span style="background-color: #c0c0c0;">允许绑定<a href="/network-faq#connstate">TIME_WAIT</a>状态的连接占据的源地址</span></p>
</td>
</tr>
<tr>
<td>SO_REUSEPORT</td>
<td>允许多个Socket绑定到完全相同的IP和端口</td>
</tr>
</tbody>
</table>
<div class="blog_h2"><span class="graybg">套接字简介</span></div>
<p>Berkeley sockets（BSD sockets，来自BSD 4.2，1983），是一套用于操控因特网套接字、UNIX域套接字的接口标准，是进程间通信的一种方式。</p>
<p>POSIX sockets与Berkeley sockets的差别很小。大部分的现代操作系统实现了Berkeley sockets接口，甚至包括Microsoft的Winsock。</p>
<div class="blog_h3"><span class="graybg">五元组</span></div>
<p>一个套接字（连接）由五元组来标识：<pre class="crayon-plain-tag">&lt;protocol&gt;, &lt;src addr&gt;, &lt;src port&gt;, &lt;dest addr&gt;, &lt;dest port&gt;</pre></p>
<p>其中：</p>
<ol>
<li>protocol在创建<pre class="crayon-plain-tag">socket()</pre>时设置</li>
<li>src addr / src port 在<pre class="crayon-plain-tag">bind()</pre>时设置</li>
<li>dest addr / dest port在<pre class="crayon-plain-tag">connect()</pre>时设置</li>
</ol>
<p>虽然UDP不需要connect()，但是dest addr / dest port会在第一次发送数据数据时由系统隐式设置。</p>
<div class="blog_h2"><span class="graybg">套接字API</span></div>
<p>这套API主要包含了以下头文件：</p>
<table class="fixed-word-wrap full-width" style="width: 100%;" border="1" cellspacing="0" cellpadding="5">
<thead>
<tr>
<td style="width: 150px; text-align: center;">头文件</td>
<td style="text-align: center;">说明 </td>
</tr>
</thead>
<tbody>
<tr>
<td class="blog_h3">sys/socket.h</td>
<td>包括BSD套接字的核心函数、数据结构</td>
</tr>
<tr>
<td class="blog_h3">netinet/in.h</td>
<td>与AF_INET、AF_INET6地址族相关的内容 </td>
</tr>
<tr>
<td class="blog_h3">sys/un.h</td>
<td>与PF_UNIX/PF_LOCAL地址族相关的内容</td>
</tr>
<tr>
<td class="blog_h3">arpa/inet.h</td>
<td>包含一些操控IP地址的函数</td>
</tr>
<tr>
<td class="blog_h3">netdb.h</td>
<td>用来转换协议名称、主机名称为数字化的地址</td>
</tr>
<tr>
<td class="blog_h3"><span style="color: #252525;">unistd.h</span></td>
<td>UNIX标准头，包含很多系统调用（fork、pipe）的封装和I/O原语，例如read、write、close</td>
</tr>
</tbody>
</table>
<p>包含的主要函数有：</p>
<table class="full-width fixed-word-wrap" style="width: 100%;" border="1" cellspacing="0" cellpadding="5">
<thead>
<tr>
<td style="width: 150px; text-align: center;"> 函数</td>
<td style="text-align: center;">说明 </td>
</tr>
</thead>
<tbody>
<tr>
<td class=" blog_h3">socket()</td>
<td>创建以整数（描述符）来识别的、指定类型的套接字，并为其分配系统资源<br />
<pre class="crayon-plain-tag">/**
 * 创建一个通信端点（endpoint）并返回套接字的文件描述符，如果出错返回-1
 * domain 
 *   指定协议族：AF_INET、AF_INET6、AF_UNIX
 * type 
 *   套接字类型：SOCK_STREAM、SOCK_DGRAM、SOCK_SEQPACKET、SOCK_RAW 
 * protocol 
 *   传输协议：IPPROTO_TCP、IPPROTO_SCTP、IPPROTO_UDP、IPPROTO_DCCP
 *   这些协议定义在netinet/in.h，如果指定0，则根据domain、type推导默认值
 */
int socket(int domain, int type, int protocol);</pre>
</td>
</tr>
<tr>
<td class=" blog_h3">bind()</td>
<td>一般用于服务器端，将一个socket与socket address结构体（包含本地IP、端口信息）关联<br />
<pre class="crayon-plain-tag">/**
 * 为套接字分配地址，成功返回0否则返回-1
 * sockfd
 *   被分配地址的套接字的描述符
 * my_addr
 *   代表地址的sockaddr结构的指针
 * addrlen
 *   指定sockaddr结构的长度
 */
int bind(int sockfd, const struct sockaddr *my_addr, socklen_t addrlen);</pre>
</td>
</tr>
<tr>
<td class=" blog_h3">listen()</td>
<td>用于服务器端，导致绑定的TCP socket进入监听（LISTENING）状态<br />
<pre class="crayon-plain-tag">/**
 * 仅用于面向流（面向连接）的套接字服务端
 * sockfd
 *   监听的套接字描述符
 * backlog
 *   排队入站请求数量，一旦请求被接受即移出队列
 *   超过队列限制的请求被直接拒绝
 */
int listen(int sockfd, int backlog);</pre>
</td>
</tr>
<tr>
<td class=" blog_h3">connect()</td>
<td>用于客户端，将本地空闲端口分配给套接字，对于TCP，该函数将尝试建立TCP连接<br />
<pre class="crayon-plain-tag">/**
 * 该系统调用通过自己指定的套接字，连接到服务器端套接字
 * 对于无连接的套接字类型，该调用不建立连接，只是用于说明数据报的默认目标
 * sockfd
 *   本地套接字描述符
 * serv_addr
 *   远程套接字监听地址
 * addrlen
 *   远程套接字地址的长度  
 */
int connect(int sockfd, const struct sockaddr *serv_addr, socklen_t addrlen);</pre>
</td>
</tr>
<tr>
<td class=" blog_h3">accept()</td>
<td>用于服务器端，接受一个入站连接请求，创建并返回关联了socket pair的socket<br />
<pre class="crayon-plain-tag">/**
 * 为连接请求创建一个新的套接字，并移出队列
 * sockfd
 *   监听套接字的描述符
 * cliaddr
 *   用于接收客户端地址信息的结构
 * addrlen
 *   sockaddr类型的长度
 */
int accept(int sockfd, struct sockaddr *cliaddr, socklen_t *addrlen);</pre>
</td>
</tr>
<tr>
<td class=" blog_h3">send()</td>
<td>TCP Socket的send()是一个异步调用，当数据送入socket send buffer以后就会返回。也就是说，在send()返回以后，数据仍然需要经历TCP拥塞控制等过程，才能被成功发送</td>
</tr>
<tr>
<td class=" blog_h3">recv()</td>
<td>read返回值：0表示对端关闭了连接；-1表示发生错误</td>
</tr>
<tr>
<td class=" blog_h3">write()</td>
<td> </td>
</tr>
<tr>
<td class=" blog_h3">read()</td>
<td> </td>
</tr>
<tr>
<td class=" blog_h3">sendto()</td>
<td>用于UDP场景下的发送数据</td>
</tr>
<tr>
<td class=" blog_h3">recvfrom()</td>
<td>用于UDP场景下的接收数据</td>
</tr>
<tr>
<td class=" blog_h3">close()</td>
<td>
<p>关闭套接字的输入/输出通道，导致系统释放与套接字相关的资源，对于TCP，连接被关闭。对于客户端，即使connect()失败也要关闭套接字</p>
<p>该调用仅仅销毁了套接字的接口，套接字本身由OS内核负责销毁，某些情况下，套接字可能进入TIME_WAIT状态</p>
</td>
</tr>
<tr>
<td class=" blog_h3">shutdown()</td>
<td>
<p>类似于close()，但是可以实现“半关闭”，即只关闭输出通道或者输入通道</p>
</td>
</tr>
<tr>
<td class=" blog_h3">gethostbyname()</td>
<td>用于解析IPv4的主机名、IP地址<br />
<pre class="crayon-plain-tag">/**
 * 通过DNS系统或者/etc/hosts来查找主机名对应的Internet地址信息
 * 如果调用成功返回代表因特网地址的结构hostent，否则返回NULL指针
 * 出错时通过h_errno可以检查具体原因
 * name
 *   主机名，例如gmem.cc
 */
struct hostent *gethostbyname(const char *name);</pre>
</td>
</tr>
<tr>
<td class=" blog_h3">gethostbyaddr()</td>
<td>
<pre class="crayon-plain-tag">/**
 * 类似于上面的函数，通过地址来查询
 * addr
 *   in_addr类型的指针，代表了主机的地址
 * len
 *   上面地址的长度
 * type
 *   地址族类型，例如AF_INET
 */
struct hostent *gethostbyaddr(const void *addr, int len, int type);</pre>
</td>
</tr>
<tr>
<td class=" blog_h3">select()</td>
<td>
<p>轮询，等待所提供列表中一个或者多个套接字可读、可写或者有错误发生。具有以下缺点：</p>
<ol>
<li>每次调用时，列表中所有套接字的文件描述符需要拷贝到内核空间</li>
<li>每次调用时，需要在内核态遍历文件描述符</li>
<li>支持的文件描述符数量较小，默认1024</li>
</ol>
</td>
</tr>
<tr>
<td class=" blog_h3">poll()</td>
<td>与select()类似，但是描述文件描述符的方式不同，select()使用fd_set结构，而poll()使用pollfd结构</td>
</tr>
<tr>
<td class=" blog_h3">epoll()</td>
<td>Linux内核为处理大批量文件描述符而作了改进的poll</td>
</tr>
<tr>
<td class=" blog_h3">getsockopt()</td>
<td>获取某个套接字选项当前的值</td>
</tr>
<tr>
<td class=" blog_h3">setsockopt()</td>
<td>设置某个套接字选项的值</td>
</tr>
</tbody>
</table>
<div class="blog_h2"><span class="graybg">Linux代码示例</span></div>
<div class="blog_h3"><span class="graybg">Echo示例 </span></div>
<pre class="crayon-plain-tag">#include &lt;stdio.h&gt;
#include &lt;string.h&gt;
#include &lt;sys/socket.h&gt;
#include &lt;arpa/inet.h&gt;
#include &lt;unistd.h&gt;

#define DEFAULT_PORT 1918

const char* ip2txt( struct sockaddr_in addr, char* buf );

int main( void )
{
    int sock_desc, client_sock, read_size;
    struct sockaddr_in server, client;
    socklen_t socklen = sizeof ( server );
    char client_message[2000], ipv4_buf[INET_ADDRSTRLEN];
    struct timeval timeout;
    timeout.tv_sec = 3;
    timeout.tv_usec = 0;

    setvbuf( stdout, NULL, _IONBF, 0 );

    //创建套接字，返回一个描述符
    sock_desc = socket( AF_INET, SOCK_STREAM, 0 );
    if ( sock_desc == -1 )
    {
        puts( "Failed to create socket" );
        return 1;
    }
    //监听套接字结构
    server.sin_family = AF_INET;
    server.sin_addr.s_addr = INADDR_ANY;
    server.sin_port = htons( DEFAULT_PORT );
    //绑定本地监听地址到描述符
    if ( bind( sock_desc, ( struct sockaddr * ) &amp;server, socklen ) &lt; 0 )
    {
        puts( "Binding failed" );
        return 1;
    }
    else
    {
        printf( "Binding to local socket address: %s:%d\n", ip2txt( server, ipv4_buf ), DEFAULT_PORT );
    }
    //开始监听，等待队列容量10
    listen( sock_desc, 10 );
    for ( ;; )
    {
        //接收一个客户端连接，返回代表此套接字的描述符
        client_sock = accept( sock_desc, ( struct sockaddr * ) &amp;client, &amp;socklen );
        printf( "Connection from %s:%d accepted\n", ip2txt( client, ipv4_buf ), ntohs( client.sin_port ) );
        //设置读取超时，此方法用于Linux，Cygwin无效
        if ( setsockopt( client_sock, SOL_SOCKET, SO_RCVTIMEO, &amp;timeout, sizeof(struct timeval) ) == 0 )
        {
            printf( "recv() timeout set to %ds\n", ( int ) timeout.tv_sec );
        }
        //循环读取数据，该方法会立即返回
        while ( ( read_size = recv( client_sock, client_message, sizeof ( client_message ), 0 ) ) &gt; 0 )
        {
            //把数据原样写给客户端
            printf( "Received message: %s\n", client_message );
            write( client_sock, client_message, strlen( client_message ) );
        }

        //如果超时后仍然读取不到数据、或者出现其他错误，会返回-1
        puts( "Error recv()." );
    }
    return 0;
}

const char* ip2txt( struct sockaddr_in addr, char* buf )
{
    //该函数用来将网络字节序的整数转换为点号分隔的IP地址格式
    return inet_ntop( AF_INET, &amp;addr.sin_addr, buf, INET_ADDRSTRLEN );
}</pre>
<p>客户端代码示例：</p>
<pre class="crayon-plain-tag">void client()
{
    int client_sock_desc;
    struct sockaddr_in server;
    char client_message[2000], server_message[2000];
    client_sock_desc = socket( AF_INET, SOCK_STREAM, 0 );
    server.sin_family = AF_INET;
    server.sin_addr.s_addr = inet_addr( "192.168.0.90" );
    server.sin_port = htons( DEFAULT_PORT );
    socklen_t socklen = sizeof ( server );
    //连接到服务器端
    if ( connect( client_sock_desc, ( struct sockaddr* ) &amp;server, socklen ) &lt; 0 )
    {
        printf( "Connect failed.\n" );
        return;
    }
    while ( 1 )
    {
        printf( "Enter message: \n" );
        scanf( "%s", client_message );
        //发送消息
        if ( send( client_sock_desc, client_message, strlen( client_message ), 0 ) &lt; 0 )
        {
            printf( "Send failed.\n" );
            return;
        }
        //接收回应
        if ( recv( client_sock_desc, server_message, sizeof ( server_message ), 0 ) &lt; 0 )
        {
            printf( "Recv failed.\n" );
            return;
        }
        printf( "Message from server:%s\n", server_message );
    }
}</pre>
<div class="blog_h3"><span class="graybg">多进程服务器示例</span></div>
<p>一般情况下，对于网络服务器不适用“多进程”方式处理，因为网络服务器通常需要很多内存资源，fork()却会复制这些资源。这里只是做一个简单示例：</p>
<pre class="crayon-plain-tag">bind( server_sockfd, ( struct sockaddr * ) &amp;server_address, server_len );
listen( server_sockfd, 5 );
signal( SIGCHLD, SIG_IGN ); //忽略子进程的退出
while ( 1 )
{
    client_len = sizeof ( client_address );
    //接受一个连接请求
    client_sockfd = accept( server_sockfd, ( struct sockaddr * ) &amp;client_address, &amp;client_len );
    //创建子进程处理请求
    if ( fork() == 0 )
    {
        //如果是子进程，读取消息并处理
        //注意子进程继承父进程打开的文件描述符
        read( client_sockfd, &amp;ch, 1 );
        write( client_sockfd, &amp;ch, 1 );
        close( client_sockfd );
        exit( 0 );
    }
    else
    {
        //如果是父进程，只需要关闭描述符
        close( client_sockfd );
    }
}</pre>
<div class="blog_h3"><span class="graybg">基于Select的异步I/O</span></div>
<p>select系统调用允许同时在<span style="background-color: #c0c0c0;">多个底层文件描述符</span>上等待<span style="background-color: #c0c0c0;">输入的到达或输出的完成</span>，避免在单个输入输出上的忙等待。下面是相关的函数说明：</p>
<pre class="crayon-plain-tag">#include &lt;sys/types.h&gt;
#include &lt;sys/time.h&gt;
//下面这组宏用于对文件描述符集合进行操作
void FD_ZERO( fd_set *fdset );//将文件描述符集初始化为空集合
int FD_ISSET( int fd, fd_set *fdset ); //判断文件描述符fd是否存在于fdset中
void FD_CLR( int fd, fd_set *fdset ); //清除一个文件描述符
void FD_SET( int fd, fd_set *fdset ); //设置一个文件描述符
/**
 * 测试文件描述符集中是否至少有一个文件描述符已经处于可读、可写、错误状态。
 * 该函数在以下情况下返回：
 * 1、readfds具有可读、writefds具有可写、errorfds存在描述符遇到错误条件
 * 2、如果上述三个情况都没有发生，该调用在timeout后返回
 * 当返回时，描述符集合被修改为指示哪些描述符可读、可写或者处于错误状态
 * 
 * @param nfds 需要测试的文件描述符数量，从0到nfds-1
 * 下面三个参数都可以被设置为空指针，表示不进行相应的测试
 * @param readfds  读文件描述符
 * @param writefds 写文件描述符
 * @param errorfds 错误文件描述符
 * @param timeout 超时时间，如果是空指针将一直阻塞
 * @return 状态无变化的描述符总数，失败时返回-1并设置errno：
 *         EBADF 无效文件描述符
 *         EINTR 因中断而返回
 *         EINVAL ndfs或者timeout取值错误
 */
int select( int nfds, fd_set *readfds, fd_set *writefds, fd_set *errorfds, struct timeval *timeout );
struct timeval
{
    time_t tv_sec; /* 毫秒 */
    long tv_usec; /* 微秒 */
};</pre>
<p>下面是一段示例代码，通过单进程服务多个客户端，并将请求值加1返回给客户端：</p>
<pre class="crayon-plain-tag">#include &lt;sys/types.h&gt;
#include &lt;sys/socket.h&gt;
#include &lt;stdio.h&gt;
#include &lt;netinet/in.h&gt;
#include &lt;sys/time.h&gt;
#include &lt;sys/ioctl.h&gt;
#include &lt;unistd.h&gt;
#include &lt;stdlib.h&gt;

#define DEFAULT_PORT 1918

int main()
{
    int server_sockfd, client_sockfd;
    int server_len, client_len;
    struct sockaddr_in server_address;
    struct sockaddr_in client_address;
    int result;
    fd_set readfds, testfds;
    //创建客户端TCP套接字
    server_sockfd = socket( AF_INET, SOCK_STREAM, 0 );
    server_address.sin_family = AF_INET;
    server_address.sin_addr.s_addr = htonl( INADDR_ANY );
    server_address.sin_port = htons( DEFAULT_PORT );
    server_len = sizeof ( server_address );
    //绑定套接字描述符到本地地址
    bind( server_sockfd, ( struct sockaddr * ) &amp;server_address, server_len );
    //在套接字描述符上监听
    listen( server_sockfd, 5 );
    FD_ZERO( &amp;readfds );
    FD_SET( server_sockfd, &amp;readfds ); //将监听描述符添加到集合中
    while ( 1 )
    {
        char ch;
        int fd;
        int nread;
        testfds = readfds;
        //一直等待，直到输入（连接请求）到达
        result = select( FD_SETSIZE, &amp;testfds, ( fd_set * ) 0, ( fd_set * ) 0, ( struct timeval * ) 0 );
        for ( fd = 0; fd &lt; FD_SETSIZE; fd++ )
        {
            //遍历所有就绪的可读描述符
            if ( FD_ISSET( fd, &amp;testfds ) )
            {
                if ( fd == server_sockfd )
                {
                    //这是一个服务器监听套接字
                    //服务器端套接字就绪，说明有连接请求
                    client_len = sizeof ( client_address );
                    //生成客户套接字，并加入到读套接字集中
                    client_sockfd = accept( server_sockfd, ( struct sockaddr * ) &amp;client_address, &amp;client_len );
                    FD_SET( client_sockfd, &amp;readfds );
                }
                else
                {
                    //这是一个客户套接字
                    ioctl( fd, FIONREAD, &amp;nread ); //得到可读数据字节数
                    if ( nread == 0 )
                    {
                        close( fd );
                        FD_CLR( fd, &amp;readfds ); //从读集合中移除该客户端
                    }
                    else
                    {
                        read( fd, &amp;ch, 1 ); //读取一个字节
                        ch++;
                        write( fd, &amp;ch, 1 ); //写回一个字节
                    }
                }
            }
        }
    }
}</pre>
<div class="blog_h3"><span class="graybg">UDP示例</span></div>
<pre class="crayon-plain-tag">sockfd = socket( AF_INET, SOCK_DGRAM, 0 );
address.sin_family = AF_INET;
address.sin_port = servinfo-&gt;s_port;
address.sin_addr = *( struct in_addr * ) *hostinfo-&gt;h_addr_list;
len = sizeof(address);
//向address发送一个数据报
sendto( sockfd, buffer, 1, 0, ( struct sockaddr * ) &amp;address, len );
//从address接收数据报
recvfrom( sockfd, buffer, sizeof(buffer), 0, ( struct sockaddr * ) &amp;address, &amp;len );</pre>
<div class="blog_h3"><span class="graybg">TCP服务器示例</span></div>
<pre class="crayon-plain-tag">#include &lt;sys/socket.h&gt;
#include &lt;netinet/in.h&gt;
#include &lt;arpa/inet.h&gt;
#include 
#include 
#include 
#include 
#include 
#include &lt;sys/types.h&gt;
#include  

int main(int argc, char *argv[])
{
    int listenfd = 0, connfd = 0;
    struct sockaddr_in serv_addr; 

    char sendBuff[1025];
    time_t ticks; 
    // 下面的调用在内核中创建一个未命名套接字，返回一个整数 —— 套接字描述符
    // AF_INET 地址族，对于IPv4使用AF_INET
    // SOCK_STREAM 传输层协议类型，流式表示需要确认机制
    // 0 让内核决定默认协议，AF_INET + SOCK_STREAM -&gt; TCP
    listenfd = socket(AF_INET, SOCK_STREAM, 0);

    memset(&amp;serv_addr, '0', sizeof(serv_addr));
    memset(sendBuff, '0', sizeof(sendBuff)); 
    serv_addr.sin_family = AF_INET;
    serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
    serv_addr.sin_port = htons(5000); 
    // 将serv_addr所指定的细节信息，绑定到listenfd。bind调用时可选的，如果不调用，内核可以随机选取监听端口
    bind(listenfd, (struct sockaddr*)&amp;serv_addr, sizeof(serv_addr)); 

    // 在此套接字上监听新连接请求，最多10个排队
    listen(listenfd, 10); 

    while(1)
    {
        // accept导致当前线程休眠，当有新的客户端请求进入，并且完成3次握手后，线程醒来
        // 获得代表客户端套接字的套接字描述符
        connfd = accept(listenfd, (struct sockaddr*)NULL, NULL); 

        ticks = time(NULL);
        snprintf(sendBuff, sizeof(sendBuff), "%.24s\r\n", ctime(&amp;ticks));
        // 可以向套接字描述符中写数据
        write(connfd, sendBuff, strlen(sendBuff)); 
        // 关闭描述符
        close(connfd);
        sleep(1);
     }
}</pre>
<div class="blog_h3"><span class="graybg">TCP客户端示例</span></div>
<pre class="crayon-plain-tag">#include &lt;sys/socket.h&gt;
#include &lt;sys/types.h&gt;
#include &lt;netinet/in.h&gt;
#include 
#include 
#include 
#include 
#include 
#include 
#include &lt;arpa/inet.h&gt; 

int main(int argc, char *argv[])
{
    int sockfd = 0, n = 0;
    char recvBuff[1024];
    struct sockaddr_in serv_addr; 

    if(argc != 2)
    {
        printf("\n Usage: %s  \n",argv[0]);
        return 1;
    } 

    memset(recvBuff, '0',sizeof(recvBuff));
    // 创建一个套接字，客户端服务器没有区别
    if((sockfd = socket(AF_INET, SOCK_STREAM, 0)) &lt; 0)
    {
        printf("\n Error : Could not create socket \n");
        return 1;
    } 

    memset(&amp;serv_addr, '0', sizeof(serv_addr)); 

    serv_addr.sin_family = AF_INET;
    serv_addr.sin_port = htons(5000); 

    if(inet_pton(AF_INET, argv[1], &amp;serv_addr.sin_addr)&lt;=0)
    {
        printf("\n inet_pton error occured\n");
        return 1;
    } 

    // 客户端套接字一般都不需要绑定，由内核自由分配一个端口就足够了

    // 尝试连接到远程套接字（IP+端口）
    if( connect(sockfd, (struct sockaddr *)&amp;serv_addr, sizeof(serv_addr)) &lt; 0)
    {
       printf("\n Error : Connect Failed \n");
       return 1;
    } 
    // 读取套接字，就像读取一个普通文件一样
    while ( (n = read(sockfd, recvBuff, sizeof(recvBuff)-1)) &gt; 0)
    {
        recvBuff[n] = 0;
        if(fputs(recvBuff, stdout) == EOF)
        {
            printf("\n Error : Fputs error\n");
        }
    } 

    if(n &lt; 0)
    {
        printf("\n Read error \n");
    } 

    return 0;
}</pre>
<div class="blog_h1"><span class="graybg">轮询机制对比</span></div>
<div class="blog_h2"><span class="graybg">read</span></div>
<p>应用程序周期性的调用read来检查I/O状态，以完成数据的读取，性能最差。</p>
<div class="blog_h2"><span class="graybg">select</span></div>
<p>维护一个最长1024的数组，保存文件描述符的状态，一次select可以遍历很多文件描述符。</p>
<pre class="crayon-plain-tag">int select (int n, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);</pre>
<p>select 函数监视的文件描述符分 3 类，分别是 writefds、readfds 和 exceptfds。<span style="background-color: #c0c0c0;">调用后 select 函数会阻塞，直到有描述符就绪</span>（有数据 可读、可写、或者有 except），<span style="background-color: #c0c0c0;">或者超时</span>（timeout 指定等待时间，如果立即返回设为 null 即可）。当 <span style="background-color: #c0c0c0;">select 函数返回后，通过遍历 fd_set，来找到就绪的描述符</span>。<br />select 目前<span style="background-color: #c0c0c0;">几乎在所有的平台上支持</span>，其良好跨平台支持是它的一大优点。select 的一个<span style="background-color: #c0c0c0;">缺点在于单个进程（Apache使用多进程方式解决此问题，尽管Linux进程很轻量，也是有代价的。而且进程间数据同步远比不上线程间同步的高效）能够监视的文件描述符的数量存在最大限制，在 Linux 上一般为 1024</span>，可以通过修改宏定义甚至重新编译内核的方式提升这一限制，但是这样也会造成效率的降低。</p>
<div class="blog_h2"><span class="graybg">poll</span></div>
<p>类似于select，但是使用链表解决1024数组长度限制。</p>
<pre class="crayon-plain-tag">int poll (struct pollfd *fds, unsigned int nfds, int timeout);
struct pollfd {
    int fd; /* file descriptor */
    short events; /* requested events to watch */
    short revents; /* returned events witnessed */
};</pre>
<p>pollfd 结构包含了要监视的 event 和发生的 event，不再使用 select参数-值传递的方式。同时，<span style="background-color: #c0c0c0;">pollfd 并没有最大数量限制（但是数量过大后性能也是会下降）</span>。和 select 函数一样，<span style="background-color: #c0c0c0;">poll 返回后，需要轮询 pollfd 来获取就绪的描述符</span>。<br />从上面看，select 和 poll 都需要在返回后，通过遍历文件描述符来获取已经就绪的 socket。事实上，同时连接的<span style="background-color: #c0c0c0;">大量客户端在同一时刻可能只有很少的处于就绪状态，因此随着监视的描述符数量的增长，其效率也会线性下降</span>。</p>
<div class="blog_h2"><span class="graybg">epoll</span></div>
<p>效率最高，调用epoll后，如果没有发现可用的I/O事件，调用线程将会自动休眠，直到有事件发生后，内核将其唤醒。</p>
<pre class="crayon-plain-tag">// 创建 epoll 文件描述符，参数 size 并不是限制了 epoll 所能监听的描述符最大个数，只是对内核初始分配内部数据结构的一个建议
int epoll_create(int size)；
// 对指定描述符 fd 执行 op 操作控制，event 是与 fd 关联的监听事件
// op有三种：添加 EPOLL_CTL_ADD，删除 EPOLL_CTL_DEL，修改 EPOLL_CTL_MOD
//          分别添加、删除和修改对 fd 的监听事件
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event)；

typedef union epoll_data {
    void *ptr;
    int fd;
    __uint32_t u32;
    __uint64_t u64;
} epoll_data_t;

struct epoll_event {
    __uint32_t events;      /* Epoll events */
    epoll_data_t data;      /* User data variable */
};
//  等待 epfd 上的 IO 事件，最多返回 maxevents 个事件
int epoll_wait(int epfd, struct epoll_event * events,  int maxevents, int timeout);</pre>
<p>在 select/poll 中，进程只有在调用一定的方法后，<span style="background-color: #c0c0c0;">内核才对所有监视的文件描述符进行扫描</span>，而 epoll 事先通过 epoll_ctl() 来注册一个文件描述符，<span style="background-color: #c0c0c0;">一旦某个文件描述符就绪时，内核会采用类似 callback 的回调机制，迅速激活这个文件描述符</span>，当进程调用 epoll_wait 时便得到通知</p>
<p>epoll 的优点主要是一下几个方面：</p>
<ol>
<li>监视的描述符数量不受限制，它所支持的 fd 上限是最大可以打开文件的数目，这个数字一般远大于 2048, 举个例子, 在 1GB 内存的机器上大约是 10 万左右，具体数目可以 <pre class="crayon-plain-tag">cat /proc/sys/fs/file-max</pre> 察看, 一般来说这个数目和系统内存关系很大</li>
<li>IO 的效率不会随着监视 fd 的数量的增长而下降。epoll 不同于 select 和 poll 轮询的方式，而是通过每个 fd 定义的回调函数来实现的。只有就绪的 fd 才会执行回调函数</li>
<li>支持水平触发和边沿触发两种模式：
<ol>
<li>水平触发模式，文件描述符状态发生变化后，如果没有采取行动，它将后面反复通知，这种情况下编程相对简单，libevent 等开源库很多都是使用的这种模式</li>
<li>边沿触发模式，只告诉进程哪些文件描述符刚刚变为就绪状态，只说一遍，如果没有采取行动，那么它将不会再次告知。理论上边缘触发的性能要更高一些，但是代码实现相当复杂（Nginx 使用的边缘触发）</li>
</ol>
</li>
<li>mmap 加速内核与用户空间的信息传递。epoll 是通过<span style="background-color: #c0c0c0;">内核与用户空间 mmap 同一块内存，避免了无谓的内存拷贝</span></li>
</ol>
</div><p>The post <a rel="nofollow" href="https://blog.gmem.cc/linux-network-programming">Linux网络编程</a> appeared first on <a rel="nofollow" href="https://blog.gmem.cc">绿色记忆</a>.</p>
]]></content:encoded>
			<wfw:commentRss>https://blog.gmem.cc/linux-network-programming/feed</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>Linux网络知识集锦</title>
		<link>https://blog.gmem.cc/network-faq</link>
		<comments>https://blog.gmem.cc/network-faq#comments</comments>
		<pubDate>Fri, 22 Sep 2006 12:21:20 +0000</pubDate>
		<dc:creator><![CDATA[Alex]]></dc:creator>
				<category><![CDATA[Linux]]></category>
		<category><![CDATA[Network]]></category>
		<category><![CDATA[Linux知识]]></category>
		<category><![CDATA[Linux编程]]></category>
		<category><![CDATA[网络编程]]></category>

		<guid isPermaLink="false">http://blog.gmem.cc/?p=6337</guid>
		<description><![CDATA[<p>网络编程 参考： Linux网络编程 Linux编程知识集锦 Linux知识集锦 Bonding 网络接口绑定（Network Interface Bonding）是Linux下的一项技术，它能够将多块物理网卡绑定为单一的逻辑网卡，从而实现： 带宽增加 提供容错能力，防止一根网线损坏的情况 也叫Teaming、 Link Aggregation Groups（LAG）。 启用 你需要先安装bonding内核模块，并且用modprobe查看bonding驱动是否被加载： [crayon-69e74a6c844bb607766926/] 创建 你需要先安装两块物理NIC，然后使用下面的命令将它们bond为新的逻辑接口：  [crayon-69e74a6c844bf099054087/] 或者永久化的配置： <a class="read-more" href="https://blog.gmem.cc/network-faq">[...]</a></p>
<p>The post <a rel="nofollow" href="https://blog.gmem.cc/network-faq">Linux网络知识集锦</a> appeared first on <a rel="nofollow" href="https://blog.gmem.cc">绿色记忆</a>.</p>
]]></description>
				<content:encoded><![CDATA[<div class="wri_content_clear_both"><div class="blog_h1"><span class="graybg">网络编程</span></div>
<p>参考：</p>
<ol>
<li><a href="/linux-network-programming">Linux网络编程</a></li>
<li><a href="/linux-programming-faq">Linux编程知识集锦</a></li>
<li><a href="/linux-faq">Linux知识集锦</a></li>
</ol>
<div class="blog_h1"><span class="graybg">Bonding</span></div>
<p>网络接口绑定（Network Interface Bonding）是Linux下的一项技术，它能够将多块物理网卡绑定为单一的逻辑网卡，从而实现：</p>
<ol>
<li>带宽增加</li>
<li>提供容错能力，防止一根网线损坏的情况</li>
</ol>
<p>也叫Teaming、 Link Aggregation Groups（LAG）。</p>
<div class="blog_h2"><span class="graybg">启用</span></div>
<p>你需要先安装bonding内核模块，并且用modprobe查看bonding驱动是否被加载：</p>
<pre class="crayon-plain-tag">sudo modprobe bonding
lsmod | grep bond</pre>
<div class="blog_h2"><span class="graybg">创建</span></div>
<p>你需要先安装两块物理NIC，然后使用下面的命令将它们bond为新的逻辑接口： </p>
<pre class="crayon-plain-tag">sudo ip link add bond0 type bond mode 802.3ad
sudo ip link set eth0 master bond0
sudo ip link set eth1 master bond0</pre>
<p>或者永久化的配置：</p>
<pre class="crayon-plain-tag">auto bond0
iface bond0 inet static
	address 192.168.1.150
	netmask 255.255.255.0	
	gateway 192.168.1.1
	dns-nameservers 192.168.1.1 8.8.8.8
	dns-search domain.local
		slaves eth0 eth1
		bond_mode 0
		bond-miimon 100
		bond_downdelay 200
		bound_updelay 200</pre>
<div class="blog_h1"><span class="graybg">TCP协议</span></div>
<p>参考：<a href="/tcp-ip-study-note">TCP/IP协议栈学习笔记</a></p>
<div class="blog_h2"><span class="graybg"><a id="connstate"></a>连接状态</span></div>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 20%; text-align: center;">状态</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td class="blog_h3">LISTEN</td>
<td>仅仅对于服务器端存在，正在指定的端口上监听</td>
</tr>
<tr>
<td class="blog_h3">ESTABLISHED</td>
<td>三次握手完成，连接建立</td>
</tr>
<tr>
<td class="blog_h3">FIN_WAIT1</td>
<td> </td>
</tr>
<tr>
<td class="blog_h3">CLOSE_WAIT</td>
<td> </td>
</tr>
<tr>
<td class="blog_h3">FIN_WAIT2</td>
<td>
<p>在FIN_WAIT_2状态，本端已经发送FIN，对端已经ACK。除非进行半关闭，否则对端的应用层已经意识到需要进行关闭，并向本端发送FIN来关闭另一方向的连接，只有对端完成这个关闭，本端才从FIN_WAIT_2进入TIME_WAIT状态。</p>
<p>这也意味着，本端可能一直处于FIN_WAIT_2，对端则一直处于CLOSE_WAIT，为了防止处于FIN_WAIT_2的无限等待，TCP实现中使用定时器进行处理</p>
</td>
</tr>
<tr>
<td class="blog_h3">TIME_WAIT</td>
<td>
<p>TIME_WAIT又称2MSL等待状态。</p>
<p>每个TCP实现必须选择一个报文段最大生存时间（Maximum Segment Lifetime，MSL）—— 是任何报文段被丢弃前在网络内的最长时间。此外，由于IP数据报具有TTL，因此在网络上存在是有限制的。后者是基于跳数，而不是定时器。MSL的值通常是 30秒、1分钟或者2分钟。</p>
<p>对一个具体实现所给定的MSL值，处理的原则是：当TCP执行一个主动关闭，并发回最后一个ACK，该连接必须在TIME_WAIT状态停留的时间为2倍的MSL，这样可让TCP再次发送最后的ACK以防这个ACK丢失（另一端超时并重发最后的FIN）。</p>
<p>2MSL等待的结果是，2MSL期间定义连接的Socket（即：唯一标识Socket的四元组）不能再被使用。但是某些实现允许重用处于2MSL状态的端口（指定：SO_REUSEADDR）</p>
<p>服务器通常是被动关闭，不会进入TIME_WAIT状态</p>
</td>
</tr>
<tr>
<td class="blog_h3">CLOSED</td>
<td>不是一个真实的状态，作为状态图假想的起终点</td>
</tr>
</tbody>
</table>
<div class="blog_h1"><span class="graybg">网络地址转换</span></div>
<p>网络地址转换也称为网络掩蔽或者<span style="background-color: #c0c0c0;">IP掩蔽（Masquerading）</span>，是一种在IP封包通过路由器或防火墙时<span style="background-color: #c0c0c0;">重写源IP地址或目的IP地址</span>的技术。该技术普遍用于只有一个公有IP的局域网中，允许局域网中多台主机与公网上的主机进行通信。NAT 功能通常被集成到路由器、防火墙、ISDN路由器或者单独的NAT设备中。</p>
<div class="blog_h2"><span class="graybg">NAT工作机制</span></div>
<p>一个典型的局域网会使用一个专用网络来指定子网，最常用的是192.168.x.x，这个局域网中包含一个路由器，它占用一个IP地址（例如192.168.0.1）。路由器同时通过一个ISP提供的公有IP地址连接到因特网上。</p>
<p>当局域网上的主机需要和公网主机通信时，其发送IP封包，路由器将IP封包的源地址从专有地址192.168.0.x转换为公有地址（例如106.185.46.7），路由记住专有地址[:端口]与公有地址[:端口]的映射关系，当公网主机的IP封包到达时，利用此映射关系改写目的地址，进而转发给局域网主机。</p>
<p>上述的源地址、目的地址改写的过程即为NAT。</p>
<div class="blog_h2"><span class="graybg">NAT的优缺点</span></div>
<p>NAT的优势：</p>
<ol>
<li>在IPv4短缺的情况下，可以使多台主机共享一个IP地址接入因特网</li>
<li>隐藏内网计算机，避免受到来自外部网络的攻击</li>
</ol>
<p>NAT的缺点：</p>
<ol>
<li>某些协议无法正常工作，例如主动模式的FTP。这些协议需要引入<span style="background-color: #c0c0c0;">应用层网关（Application Layer Gateway，ALG）</span>才能正常工作</li>
</ol>
<div class="blog_h2"><span class="graybg">NAT的分类</span></div>
<div class="blog_h3"><span class="graybg">基本网络地址转换</span></div>
<p>亦可简称NAT或者静态NAT，<span style="background-color: #c0c0c0;">仅支持地址转换，不支持端口映射</span>。需要每一个连接对应一个公网IP地址，因此需要维护一个公网IP地址池。某些宽带路由器使用这种方式去指定一台局域网主机去接受所有外部连接，并称该机器为<span style="background-color: #c0c0c0;">DMZ主机</span>（并不是真正意义上的，因为DMZ主机必须与内网隔离）。</p>
<div class="blog_h3"><span class="graybg">网络地址端口转换</span></div>
<p>NAPT，该方式<span style="background-color: #c0c0c0;">支持端口映射</span>，允许多台内网主机共享一个公网IP地址，其包括两类转换功能：</p>
<ol>
<li>源地址转换：发起连接的内网计算机的IP地址将会被重写，使得内网主机发出的数据包能够到达外网主机</li>
<li>目的地址转换：被连接内网计算机的IP地址将被重写，使得外网主机发出的数据包能够到达内网主机</li>
</ol>
<p>这两种转换一般会一起使用，以支持双向通信。</p>
<p>NAPT需要维护一个NAT表，来基于内网IP/端口与公网IP/端口的对应关系，例如：</p>
<table style="width: 100%;" border="1" cellspacing="0" cellpadding="5">
<thead>
<tr>
<td style="text-align: center;"> 内网IP/端口</td>
<td style="text-align: center;"> 外网IP/端口</td>
</tr>
</thead>
<tbody>
<tr>
<td>192.168.0.89:6443</td>
<td>106.185.46.7:9200</td>
</tr>
<tr>
<td>192.168.0.90:8897</td>
<td>106.185.46.7:9201 </td>
</tr>
</tbody>
</table>
<p> <a href="/webrtc-server-basedon-kurento#glossary-stun">STUN</a>标准将NAPT分为以下几个子类型：</p>
<table style="width: 100%;" border="1" cellspacing="0" cellpadding="5">
<thead>
<tr>
<td style="width: 150px; text-align: center;">类型 </td>
<td style="text-align: center;">说明 </td>
</tr>
</thead>
<tbody>
<tr>
<td>完全圆锥型NAT </td>
<td>一旦一个内部地址（iAddr:port1）映射到外部地址（eAddr:port2），<span style="background-color: #c0c0c0;">所有</span>发自iAddr:port1的包<span style="background-color: #c0c0c0;">都经由</span>eAddr:port2向外发送。<span style="background-color: #c0c0c0;">任意外部主机</span>（hAddr:*）都能通过给eAddr:port2发送封包到达iAddr:port1</td>
</tr>
<tr>
<td>地址受限圆锥型NAT</td>
<td>
<p>与完全圆锥形 NAT类似，但是外部主机（hAddr:*）能通过eAddr:port2给iAddr:port1发送封包的前提是，之前iAddr:port1<span style="background-color: #c0c0c0;">曾经发送</span>封包到hAddr:*。星号表示任意端口</p>
<p>在应用受限圆锥形NAT时，外网主机不能主动发起最初的通信，内网主机为了让外网主机能与之通信，主动发起连接的行为，被称为“打洞”</p>
</td>
</tr>
<tr>
<td>端口受限圆锥型NAT</td>
<td>与地址受限圆锥形NAT类似，但是附加了一个端口限制，即：外部主机（hAddr:port3）能通过eAddr:port2给iAddr:port1发送封包的前提是，之前iAddr:port1曾经发送封包到hAddr:port3</td>
</tr>
<tr>
<td>对称NAT（symmetric）</td>
<td>
<p>内部地址（iAddr:port1）向外部主机（hAddr:port3）发送封包，总是映射到同一个外部地址（eAddr:port2），不同的内部地址、外部主机地址组合总是映射到不同的外部地址。只有曾经收到过内部主机封包的外部主机，才能够把封包发回</p>
<p>一般注重安全性的大公司会启用对称NAT，以禁止P2P通信</p>
</td>
</tr>
</tbody>
</table>
<div class="blog_h2"><span class="graybg"><a id="traversal"></a>NAT穿透</span></div>
<p>NAT-T技术主要解决两台同时处于NAT设备后面的局域网主机建立网络连接的问题。</p>
<p>两台都处于NAT后面的主机，无法知道对方映射的公网地址/端口，因此，一般的NAT-T技术都需要一个公共服务器作为媒介：要么在建立连接的时候需要用到该服务器，要么所有的数据都通过此公共服务器中继。</p>
<p>假设：</p>
<ol>
<li>位于NAT后的主机A（局域网IP：192.168.0.89，公网IP：106.185.46.7）</li>
<li>主机B（局域网IP：192.168.1.90，公网IP：106.185.46.10），以公网服务器S（106.185.46.1）为媒介（所谓<span style="color: #383838;">STUN服务器）</span>，进行NAT-T的过程可能如下（A与B是对称的）：</li>
<li>A连接S，获得映射地址（192.168.0.89:35330  =&gt;106.185.46.7:2250），将此地址信息告知S</li>
<li>B连接S，获得映射地址（192.168.1.90:35330 =&gt;106.185.46.10:3210），将此地址信息告知S</li>
<li>A和B分别获取对方的两个地址</li>
<li>A尝试连接B
<ol>
<li>如果B是完全圆锥型NAT，那么不论A是圆锥型、对称NAT，连接都立刻建立</li>
<li>如果B是地址或端口受限NAT，由于A之前从未与B通信过，因此无法建立连接
<ol>
<li>如果A是地址或端口受限NAT，与B的连接无法建立，但是A尝试发包到B的记录已经被A侧NAT记录。只要该记录没有过期，B就可以反过来向A发送数据，该数据 A必然收到，随后A和B即可双向通信</li>
<li>如果A是对称型NAT，B利用从S得到的关于A的信息（假设为192.168.0.89:35330 =&gt;106.185.46.7:2250 =&gt; 106.185.46.1:3387），尝试向106.185.46.7:2250发送信息，由于A的对称NAT的限制，此尝试必然失败。但是，此尝试在B侧留下了NAT记录。如果
<ol>
<li>B是地址受限NAT，那么A可以再次向B发送数据，这一次B必然收到</li>
<li>B是端口受限NAT，则无法通信</li>
</ol>
</li>
</ol>
</li>
<li><span style="background-color: #c0c0c0;">B是对称NAT的情况下，如果A是对称NAT或者端口受限圆锥型NAT</span>，则双方无法通信</li>
</ol>
</li>
</ol>
<div id="bridging" class="blog_h1"><span class="graybg">桥接</span></div>
<p><a id="bridging"></a>网桥（Bridge）是一种（可以是软件虚拟的）设备，该设备有<span style="background-color: #c0c0c0;">两个或者多个</span>网络接口，分别连接到多个局域网（不同网段）中。网桥将它连接的某个局域网发送的数据帧，统一转发到其它网络接口对应的局域网中，从而将多个网络在数据链路层无缝连接起来，就<span style="background-color: #c0c0c0;">好像是一个局域网</span>一样。任何真实设备（例如eth0）和虚拟设备（例如tap0）都可链接到网桥。</p>
<p>网桥与路由器不同，后者允许多个网络在保持独立的情况下能够相互通信。网桥的行为更像是一台虚拟的交换机，它能够透明的工作，其它主机不需要知道其存在。</p>
<p>Vmware中的桥接，和一般意义上的桥接原理是一样的，它让虚拟机网卡连接到一个虚拟的以太网交换机，虚拟以太网交换机与宿主机器的以太网卡通过一个“虚拟网桥”相连。</p>
<div class="blog_h2"><span class="graybg">Linux中的桥接</span></div>
<p>当你将一个网络接口（例如eth0）添加到Linux虚拟网桥（例如br0）时，你需要把IP地址从eth0移除，并添加到br0，这样才能保证网络正常工作。</p>
<p>这是因为，网桥br0负责处理本机eth0的入站流量，它应该响应ARP请求——这样它<span style="background-color: #c0c0c0;">才能把流量转发给网桥中的其它网络接口</span>，因而它拥有eth0的IP就很合理了（把网桥的MAC和IP关联）。</p>
<div class="blog_h3"><span class="graybg">Hairpin</span></div>
<p>默认情况下，Bridge<span style="background-color: #c0c0c0;">不允许包从收到包的端口发出</span>。例如，当Bridge从一个端口接收到广播报文后，它会将报文<span style="background-color: #c0c0c0;">向其它端口全部发出</span>，而不包括接收的那个端口。</p>
<p>你可以<span style="background-color: #c0c0c0;">在端口级别打开Hairpin模式</span>，这样，<span style="background-color: #c0c0c0;">从该端口进入网桥的包，还可以从此端口出去</span>。在NAT场景下，例如Docker的NAT网络，从容器访问它映射（80）到的主机端口（8080）时：</p>
<ol>
<li>容器访问hostip:8080</li>
<li>请求到达网桥，来自端口A</li>
<li>进入宿主机协议栈</li>
<li>DNAT转换为continerip:80</li>
<li>协议包又要从A端口发出</li>
</ol>
<p>为什么叫Hairpin，这是形象的描述流量的走向，就像最简单的U形发卡一样，封包经过网桥/路由处理后，从它来自的地方发回去。</p>
<div class="blog_h1"><span class="graybg">以太网监听</span></div>
<p>当一张网卡被设置为混杂（Promicuous）模式，它就会接收网络上所有的数据帧，从而可以实现监听。</p>
<div class="blog_h2"><span class="graybg">混杂模式网卡检测</span></div>
<p>有多种方式可以检测处于混杂模式的网卡，假设现在怀疑硬件地址为38-83-45-09-0A-60、IP地址为192.168.1.5的网卡处于混杂模式，可以：</p>
<ol>
<li>伪造ICMP报文：ECHO_REQUEST，即ping命令使用的报文，将其IP首部的目的地址设置为192.168.1.5，以太网帧首部的目的地址设置为虚假的地址00-00-00-00-00-00。如果192.168.1.5是正常的网卡，它将会检测硬件地址，发现与自己的地址不同，因而忽略此帧；反之，如果192.168.1.5处于混杂模式，则它不会做硬件地址的比较，报文直接交由上层处理，导致回复ICMP报文</li>
<li>以非广播方式在局域网内发送ARP请求，如果某个网卡回复之，则其可能处于混杂模式</li>
</ol>
<div class="blog_h2"><span class="graybg">监听防范</span></div>
<p>首先要保证局域网的安全，因此这种监听只能出现在局域网内，因此必须有以太局域网主机被攻破。</p>
<p>另外数据加密也是比较好的手段，如果监听到的报文是加密的，没有什么用。</p>
<p>使用交换机也是一种常见的方式，交换机工作在数据链路层，与工作在物理层的HUB不同。交换机通常会维护一个ARP数据库，记录每个交换机端口绑定的MAC地址，当报文到达交换机时，它<span style="background-color: #c0c0c0;">只会将其从匹配的端口发送出去</span>。交换机只在两种情况下广播报文：</p>
<ol>
<li>以太网帧的目的地址在本地ARP数据库中不存在</li>
<li>报文本身就是广播的</li>
</ol>
<p>交换机可以在很大程度上解决监听问题，但是不能防止ARP欺骗。</p>
<div class="blog_h1"><span class="graybg">隧道</span></div>
<p>所谓隧道（Tunneling），是指<span style="background-color: #c0c0c0;">使用一种网络协议，并将另外一个网络协议封装在其载荷（或称负载，Payload）部分</span>的技术。前者叫做隧道协议，后者叫做负载协议。使用隧道技术的目的一般是：</p>
<ol>
<li>在不兼容的网络上传送网络协议数据报</li>
<li>在不安全的网络上提供安全性保证</li>
<li>规避防火墙，被防火墙阻挡的协议可以封装在不被阻挡的协议中，例如HTTP</li>
</ol>
<p>通常，隧道协议往往位于负载协议的高层（例如PPTP，即点对点隧道协议，可以通过TCP来封装PPP，前者工作在传输层，而PPP是链路层协议），或者与负载协议处于同一层。</p>
<p>Linux L3的隧道技术，主要基于TUN设备实现。</p>
<p>常见的隧道协议包括：</p>
<table style="width: 100%;" border="1" cellspacing="0" cellpadding="5">
<thead>
<tr>
<td style="width: 100px; text-align: center;">协议 </td>
<td style="text-align: center;"> 说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>IPsec</td>
<td>即互联网安全协定（Internet Protocol Security）。该协议通过对IP协议的分组进行加密、认证，来保护TCP/IP协议族的安全性</td>
</tr>
<tr>
<td>GRE</td>
<td>即通用路由封装（Generic Routing Encapsulation）。可以在虚拟的点对点链路中封装多种网络层协议</td>
</tr>
<tr>
<td>IP in IP</td>
<td>一种IP隧道协定，可以将IP封包封装进另外一个IP封包中</td>
</tr>
<tr>
<td>L2TP</td>
<td>
<p>即第二层隧道协议（Layer Two Tunneling Protocol）。是一种VPN的实现方式。L2TP本身不提供加密和验证功能，需要和某种安全协议搭配使用（例如IPsec）</p>
<p>L2TP封包使用UDP来传送。每个高层协议，例如PPP，都可以在L2TP隧道中建立一个L2TP会话，一个隧道里面可以包含多个会话</p>
</td>
</tr>
<tr>
<td>PPTP</td>
<td>即点对点隧道协议（Point to Point Tunneling Protocol）。是一种VPN的实现方式。PPTP使用TCP来创建控制通道，来发送控制命令，并使用GRE通道来封装PPP协议数据报，以发送数据。该协议的加密方式容易被破解</td>
</tr>
<tr>
<td>PPPoE</td>
<td>即基于以太网的点对点隧道协议（Point-to-Point Protocol over Ethernet）。是将PPP协议封装在以太网中的一种隧道协议</td>
</tr>
<tr>
<td>PPPoA</td>
<td>即基于异步传输模式的点对点隧道协议（Point-to-Point Protocol over ATM）</td>
</tr>
<tr>
<td>SSH</td>
<td>即安全外壳协议（Secure Shell Protocol）。是一项跨越多个层次的协议，为Shell提供安全的传输和使用环境。SSH提供了数据压缩的功能。</td>
</tr>
<tr>
<td>SOCKS</td>
<td>即套接字安全协议（Socket Secure）。用于通过一个代理服务器在客户端和服务器之间路由数据包</td>
</tr>
</tbody>
</table>
<div class="blog_h1"><span class="graybg">网关</span></div>
<p>在传统TCP/IP术语中，网络设备只分为网关、主机（亦称终端系统）两种，<span style="background-color: #c0c0c0;">前者能够在网络间传递数据报</span>。网关的数据处理一般只到达第三层（IP层）。网关与路由器在传统TCP/IP术语中往往是一个概念。</p>
<p>现代网络术语中，网关与路由器不同，<span style="background-color: #c0c0c0;">网关能够在不同协议之间移动数据</span>；而路由器则是在不同网络之间移动数据。例如，语音网关可以连接公共交换电话网（PSTN）与以太网，实现网络电话。</p>
<div class="blog_h1"><span class="graybg">多播</span></div>
<p>多播（MultiCast）也称为组播，包括链路层、网络层、应用层等多播技术。多播通常指IP多播（网络层）。</p>
<p>由于在IPv4网络中，<span style="background-color: #c0c0c0;">多播包可能不被路由</span>，因此你只能在局域网中使用多播技术。DVMRP、MOSPF 之类的技术解决此问题，但是你需要能够对客户端 - 服务器之间所有路由器进行配置，或者创建隧道。IPv6网络是强制支持IP多播的。</p>
<div class="blog_h1"><span class="graybg">网络虚拟化</span></div>
<p>网络虚拟化是一个很大的技术方向，可以包括硬件、软件或者相结合的实现方式。</p>
<p>Linux内核支持的网络虚拟化，主要包括两个方面：</p>
<ol>
<li>软交换机：Linux Bridge, OpenVSwitch</li>
<li>虚拟网络适配器：tun、tap、veth等</li>
</ol>
<div class="blog_h2"><span class="graybg">VLAN</span></div>
<p>VLAN（Virtual Local Area Network）是广泛应用的<span style="background-color: #c0c0c0;">网络虚拟化（在一套物理网络设备上虚拟出多个二层网络）</span>技术，它直接在Ethernet帧的头部加上<span style="background-color: #c0c0c0;">4个字节的VLAN Tag</span>，此Tag用于标识不同的二层网络。VLAN已经在大部分的网络设备和操作系统中得到了支持，它处理起来也比较简单，在读取Ethernet数据的时候，只需要根据EtherType相应的偏移4个字节就行。利用VLAN可以：</p>
<ol>
<li>将主机进行分组，即使主机不是连接到同一交换机上</li>
<li>可以对连接到相同交换机/网桥上的主机/客户机进行隔离，划分到不同子网。这样一个交换机就可以表现的像是多个独立交换机一样</li>
</ol>
<p>VLAN ID的范围是1 - 4095，对于规模较大的IT组织，需要谨慎规划子网，否则VLAN ID可能不够用。</p>
<div class="blog_h3"><span class="graybg">理解VLAN</span></div>
<p>假设你的IT环境下有一个物理交换机，连接着一系列物理主机，现在需要划分为三个独立网络。管理员配置交换机端口，将它们的VLAN ID分别设置为10 11 12，不同网络的主机分别连接到对应的端口。这种仅仅配置针对单个VLAND的端口，叫做access port。</p>
<p>随着规模的增大，一个交换机不够用了，管理员又引入一个交换机。这样，两个交换机必须都有一个端口，它允许任何以太网帧通过，而不管帧的VLAN ID，否则就无法联通了。这种不过滤VLAN ID的端口叫做trunk port。</p>
<p>在<span style="background-color: #c0c0c0;">虚拟化环境下，交换机的所有端口都会配置为trunk port。这样，每个端口（也就是主机）上都可以运行不同虚拟网络的VM</span>。</p>
<div class="blog_h2"><span class="graybg">VXLAN</span></div>
<p>VXLAN（Virtual eXtensible Local Area Network）是目前最热门的网络虚拟化/Overlay/虚拟隧道技术之一，VXLAN协议将<span style="background-color: #c0c0c0;">Ethernet帧封装在底层网络（Underlay）的三层报文（UDP）内，再加上8个字节的VXLAN header</span>，用来标识不同的二层网络：</p>
<p><a href="/wp-content/uploads/2019/04/106_net_005_vxlan.png"><img style="width: 100%;" src="/wp-content/uploads/2019/04/106_net_005_vxlan.png" alt="" /></a></p>
<p><span style="color: #1a1a1a;">VXLAN数据是经过<span style="background-color: #c0c0c0;">VTEP（VXLAN Tunnel EndPoint）封装和解封装</span>的，相应的VXLAN数据的外层UDP封包的IP（源、目的）地址就是VTEP（本机、对方）的IP地址，端口就是VTEP设备的端口（默认4789）。最外层的MAC地址用来实现VTEP之间的数据传递。每个物理节点上的所有虚拟机可以共享VTEP。VTEP可以由软件（例如OVS）或硬件实现。</span></p>
<p>VXLAN因为提出的较晚，在设备上的支持率不如VLAN，而且，VXLAN数据的封装解封装，要比VLAN复杂的多。但是它具有以下优势：</p>
<ol>
<li>VLAN ID数量限制：8个字节的VXLAN Header。其中的24bit用来标识（VNI，VXLAN Network Identifier）不同的二层网络，这样总共可以<span style="background-color: #c0c0c0;">标识1600多万个不同的二层网络</span></li>
<li>TOR交换机MAC地址表限制：在网络虚拟化之前，TOR交换机的一个端口连接一个物理主机对应一个MAC地址，但现在交换机的一个端口虽然还是连接一个物理主机但是可能进而连接<span style="background-color: #c0c0c0;">几十个甚至上百个虚拟机和相应数量的MAC地址</span>，MAC地址表记录在交换机的内存中，而交换机的内存是有限的。如果使用VXLAN，虚拟机的以太网帧被VTEP封装在UDP里面，<span style="background-color: #c0c0c0;">一个VTEP可以被一个物理主机上的所有虚拟机共用</span></li>
<li>灵活的虚机部署：采用VLAN网络的虚拟环境，不存在Overlay网络。虚拟机的网络数据，被打上VLAN Tag之后，直接在物理网络上传输，与物理网络上的VLAN是融合在一起的。这种实现机制的好处是：虚拟机能直接访问到物理网络的设备。而坏处是，无法突破物理网络的限制，通常<span style="background-color: #c0c0c0;">不同的VLAN网络，会被分配不同的IP地址段</span>，通过路由器或者其他的三层设备连接在一起，不同VLAN的虚拟机不能方便的进行L2通信。使用VXLAN后，由于基于UDP进行封装，因此可以在L2或L3网络上构建L2网络，这是一个独立于物理网络的Overlay network</li>
</ol>
<p>和普通隧道（例如ipip）不同，VXLAN是一对多的，而不是1：1的隧道协议。VXLAN设备可以像网桥那样，动态添加对端IP地址。严格来说，VXLAN模型中并没有隧道的物理实体。</p>
<p>配置和管理VXLAN，需要内核版本3.7以上的iproute2包，ip命令即此包的用户空间工具。</p>
<div class="blog_h3"><span class="graybg">工作原理</span></div>
<p><span style="color: #1a1a1a;">VXLAN报文的转发过程如下：</span></p>
<ol>
<li>原始报文经过源主机上的VTEP设备，被Linux内核添加VXLAN包头、外层UDP头，发送出去</li>
<li>对端VTEP设备接收到VXLAN报文，移除UDP头，然后根据VXLAN头决定发给哪个虚拟机</li>
</ol>
<p>在通信之前，需要回答以下三个问题：</p>
<ol>
<li>哪些VTEP需要加到同一VNI组</li>
<li>源虚拟机如何知道目的虚拟机的MAC地址</li>
<li>如何知道目的虚拟机在哪个节点</li>
</ol>
<p>问题1，通常由管理员进行配置。问题2/3本质上是一个问题 —— VXLAN通信双方如何感知彼此：</p>
<ol>
<li>内层报文，双方IP地址可以认为是已知</li>
<li>内层报文，对方MAC地址，需要实现一种ARP机制</li>
<li>VXLAN头，只需要知道<span style="background-color: #c0c0c0;">VNI，通常直接配置在VTEP上</span> —— 要么提前规划，要么根据内层报文自动生成</li>
<li>UDP头，需要知道源、目的地址/端口。源地址端口自动获取，目的端口一般默认4789，目的IP地址亦即目标主机的VTEP地址，可以通过两种方式得到：
<ol>
<li>组播：同一个VXLAN网络的VTEP加入到同一组播网络中，通过组播同步信息</li>
<li>控制中心：集中式保存需要的信息</li>
</ol>
</li>
</ol>
<div class="blog_h3"><span class="graybg">点对点VXLAN</span></div>
<p>这种配置下，两台机器构成一个VXLAN网络，每个机器上配置一个VTEP，VTEP之间通过它们的IP地址进行通信。</p>
<p>首先，需要添加VXLAN类型的接口：</p>
<pre class="crayon-plain-tag"># 接口类型  VNI  对端端口
ip link add vxlan0 type vxlan id 1 dstport 4789 
  # 对端Underlay地址  本地Underlay地址   物理网络接口
  remote 192.168.1.3 local 192.168.1.2 dev ens33</pre>
<p>为VTEP分配IP地址并启用之：</p>
<pre class="crayon-plain-tag"># 自动添加路由 172.17.1.0      0.0.0.0         255.255.255.0   U     0      0        0 vxlan0
ip addr add 172.17.1.2/24 dev vxlan0
ip link set vxlan0 up</pre>
<p>此时你可以看到如下FDB（Forwarding information base，也叫MAC表）表项： </p>
<pre class="crayon-plain-tag">bridge fdb
00:00:00:00:00:00 dev vxlan0 dst 192.168.1.3 via ens33 self permanent</pre>
<p>该项的含义是， 默认的VTEP对端地址为192.168.1.3。原始报文经过vxlan0接口后，内核会为其添加VXLAN头部，外部封装的UDP包的目的IP地址会被设置为192.168.1.3。</p>
<p>在对端，你需要进行对应的配置，VNI必须保持一致。 </p>
<div class="blog_h3"><span class="graybg">多播VXLAN</span></div>
<p>该模式需要Underlay网络支持组播。首先创建VTEP：</p>
<pre class="crayon-plain-tag"># VNI                    # 使用ens33上的多播组239.1.1.1进行信息交换
ip link add vxlan0 type vxlan id 1  local 192.168.1.2  group 239.1.1.1 dev ens33 dstport 4789</pre>
<p>和点对点模式类似的，你需要为VTEP配置IP地址并且启用之。此时，你可以看到如下FDB表项：</p>
<pre class="crayon-plain-tag">00:00:00:00:00:00 dev vxlan0 dst 239.1.1.1 via ens33 self permanent</pre>
<p>和点对点模式不同的是，dst字段的值设置为组播地址，而非对端Underlay IP地址。这个条目会导致默认情况下UDP头的目的地址被设置为组播地址239.1.1.1。</p>
<p>在所有参与到VXLAN的节点上，你需要进行对应的配置，VNI必须保持一致。</p>
<p>该模式下VTEP之间通信过程如下：</p>
<ol>
<li>主机1通过vxlan0发起ping 172.17.1.3报文到主机2的vxlan0</li>
<li>主机1内核发现目的地址和源地址在同一二层网络中，需要获取对方MAC地址，但是本地没有缓存，因此发送ARP查询请求</li>
<li>ARP报文源MAC地址为主机1的vxlan0的MAC地址，目的地址为广播地址255.255.255.255</li>
<li>内核将ARP报文封装到UDP中，设置VXLAN头。由于VTEP配置了多播组，同时<span style="background-color: #c0c0c0;">不知道目标VTEP在哪台主机上，因此会从239.1.1.1发送组播</span>报文</li>
<li>多播组中所有主机都会收到UDP报文，并根据VXLAN头发送给对应的VTEP</li>
<li>VTEP去掉VXLAN头，得到ARP请求报文，并<span style="background-color: #c0c0c0;">将源VTEP的MAC地址+源主机IP地址信息记录到FDB表</span>中</li>
<li>主机2发现ARP请求是针对自己的，因此生成ARP应答，并且单播（因为已经知道主机1的IP-MAC对应关系）给主机1的VTEP</li>
<li>ARP应答通过底层网络发送给主机1，解析后发送给vxlan0，VTEP解析ARP报文并更新VTEP缓存，同时根据报<span style="background-color: #c0c0c0;">文学习得到目的VTEP所在的主机地址，添加到自己的FDB表</span></li>
</ol>
<div class="blog_h3"><span class="graybg">VXLAN+桥接</span></div>
<p>实际情况下，每个主机都可能有几十甚至上百个虚拟机/容器，需要加入到同一个VLAN中，而<span style="background-color: #c0c0c0;">每个VLAN在一台主机上仅仅有一个VTEP</span>。</p>
<p>一个简单的解决办法是桥接。对于容器场景，可以用VETH Pair将容器连接到网桥，然后将VTEP也连接到网桥。VTEP通过物理网络相互联系。</p>
<div class="blog_h3"><span class="graybg">分布式控制中心</span></div>
<p>由于某些网络设备不支持多播，而且多播导致的不必要流量，<span style="background-color: #c0c0c0;">在生产环境下VXLAN多播模式用的很少</span>。</p>
<p>生产环境主要使用分布式控制中心架构，<span style="background-color: #c0c0c0;">在每个VTEP节点部署Agent，Agent联系控制中心，获取通信所需要的信息（FDB+ARP）</span>。</p>
<div class="blog_h2"><span class="graybg">Linux Bridge</span></div>
<p>L2设备，一种虚拟的网桥（Bridge），它是：</p>
<ol>
<li>一种<span style="background-color: #c0c0c0;">网络设备，因此可以配置IP地址、MAC地址</span></li>
<li>虚拟交换机，具有和物理交换机类似的能力</li>
</ol>
<p>普通网络设备只有两个Port，一端的数据会从另外一端出去。例如物理网卡，会从外部接收数据，发往内核协议栈，或者从内核协议栈接收数据发送到外部。</p>
<p>Bridge则不同，它具有多个Port，数据可以从任何端口进来，至于从哪个端口出去，原理类似于物理交换机 —— 要看MAC地址。</p>
<p>Bridge被广泛用于KVM/QEMU、容器技术。</p>
<p>下面的命令创建并启用一个网桥：</p>
<pre class="crayon-plain-tag">ip link add name br0 type bridge
ip link set br0 up

# 或者使用brctl命令
brctl addbr br0</pre>
<p>刚创建的网桥，一端连接着网络协议栈，其余什么都没有连接，因此没有任何功能。</p>
<p>下面我们创建veth对：</p>
<pre class="crayon-plain-tag">ip link add veth0 type veth peer name veth1
ip addr add 1.2.3.101/24 dev veth0
ip addr add 1.2.3.102/24 dev veth1
ip link set veth0 up
ip link set veth1 up


# 接受源自本地IP的ARP包
echo 1 &gt; /proc/sys/net/ipv4/conf/all/accept_local 
# 不进行源地址校验
echo 0 &gt; /proc/sys/net/ipv4/conf/all/rp_filter 
echo 0 &gt; /proc/sys/net/ipv4/conf/veth0/rp_filter 
echo 0 &gt; /proc/sys/net/ipv4/conf/veth1/rp_filter


# 现在测试是可以通的
#         由于目的地址是本地地址，默认会走lo，因此用 -I 强制指定PING请求的源地址/源接口
ping -c 1 -I veth0 1.2.3.102</pre>
<p>并且将其一端连接到网桥： </p>
<pre class="crayon-plain-tag">ip link set dev veth0 master br0
# 或者使用brctl命令
brctl addif br0 veth0</pre>
<p>使用下面的命令查看网桥上连接了哪些设备：</p>
<pre class="crayon-plain-tag">bridge link
# 9: veth0 state UP : &lt;BROADCAST,MULTICAST,UP,LOWER_UP&gt; mtu 1500 master br0 state forwarding priority 32 cost 2 

# 或者使用brctl命令
brctl show
# bridge name	bridge id		   STP enabled	interfaces
# br0		    8000.4a4c11893317	no		     veth0
# docker0		8000.02428b446c5b	no		
# virbr0		8000.deadbeef0000	no		     virbr0-nic</pre>
<p>现在：</p>
<ol>
<li>br0和veth0连接起来，<span style="background-color: #c0c0c0;">双向通道</span></li>
<li>协议栈和veth0是单向通道，<span style="background-color: #c0c0c0;">协议栈可以发数据给veth0</span>，<span style="background-color: #c0c0c0;">veth0从外部接受的数据不能发送给协议栈</span></li>
<li>br0的MAC地址变成veth0的<span style="background-color: #c0c0c0;">MAC地址 </span></li>
</ol>
<p>相当于<span style="background-color: #c0c0c0;">br0在veth0和协议栈之间做了拦截</span> —— 本来veth0发给协议栈的数据，全部转发给br0处理了。 </p>
<p>而且，veth0和veth1之间也不通了：</p>
<pre class="crayon-plain-tag">ping -c 1 -I veth0 1.2.3.102
# From 1.2.3.101 icmp_seq=1 Destination Host Unreachable</pre>
<p>原因是什么呢？下面在veth1、veth0、br0上分别抓包：</p>
<pre class="crayon-plain-tag">tcpdump -n -i veth0
# tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
# listening on veth0, link-type EN10MB (Ethernet), capture size 65535 bytes
# 22:37:16.291125 ARP, Request who-has 1.2.3.102 tell 1.2.3.101, length 28
# 22:37:16.291165 ARP, Reply 1.2.3.102 is-at ce:97:9b:c2:d1:ee, length 28

tcpdump -n -i veth1
# tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
# listening on veth1, link-type EN10MB (Ethernet), capture size 65535 bytes
# 22:37:16.291140 ARP, Request who-has 1.2.3.102 tell 1.2.3.101, length 28
# 22:37:16.291163 ARP, Reply 1.2.3.102 is-at ce:97:9b:c2:d1:ee, length 28


tcpdump -n -i br0
# tcpdump: WARNING: br0: no IPv4 address assigned
# tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
# listening on br0, link-type EN10MB (Ethernet), capture size 65535 bytes
# 22:37:16.291165 ARP, Reply 1.2.3.102 is-at ce:97:9b:c2:d1:ee, length 28
# 22:37:17.289325 ARP, Reply 1.2.3.102 is-at ce:97:9b:c2:d1:ee, length 28</pre>
<p>可以看到，veth0、veth1正常进行了ARP<span style="background-color: #ff9900;">请求</span>、<span style="background-color: #99cc00;">响应</span>，br0则仅仅监控到<span style="background-color: #cc99ff;">响应</span>。但是veth0却不能将ARP应答发给协议栈，因此无法通信。 具体分析：</p>
<ol>
<li>Ping命令发起ICMP请求，设置IP封包的源接口为veth0</li>
<li>内核接受到上述封包，发送到目的地址1.2.3.102之前，发现没有ARP缓存，无法生成L2帧</li>
<li>内核发送ARP请求，从veth0<span style="background-color: #ff9900;">发出</span></li>
<li>veth1接口接收到ARP请求，并正常<span style="background-color: #99cc00;">响应</span>之</li>
<li>veth0接收到<span style="background-color: #ff9900;">响应</span></li>
<li>veth0将响应<span style="background-color: #cc99ff;">通过交换机br0</span>发送，而非内核</li>
<li>内核没有接收到ARP响应，L2帧发送失败，Ping失败</li>
</ol>
<p>下面，把veth0的IP地址转让给br0：</p>
<pre class="crayon-plain-tag">ip addr del 1.2.3.101/24 dev veth0
ip addr add 1.2.3.101/24 dev br0</pre>
<p>现在，veth0没有IP地址了，因此协议栈在路由的时候，不会将数据包发送给veth0。这意味着，协议栈到veth0的单向通道也断了，<span style="background-color: #c0c0c0;">veth0单纯变成了连接br0和veth1的网线</span>。 </p>
<p>从br0 Ping veth1现在可以收到ICMP应答报文。报文流转路线：内核 - br0 - veth0 - veth1 - 内核 - veth1 - veth0 - br0 - 内核，但是却还不能联系到外部网络。另外，veth0仍然无法Ping通veth1，原因还是无法将ARP应答返回给内核。 </p>
<p>现在，再把物理网卡添加到网桥：</p>
<pre class="crayon-plain-tag">ip link set dev eth0 master br0</pre>
<p>现在eth0也和veth0一样，接受到的封包直接转发给br0，而不发给协议栈，自己变为一根网线。eth0、veth0无法Ping通网关，而br0则通过eth0这根网线连接到网关，veth1也可以通过br0 ping通网关。</p>
<p>这时，eth0已经成为一根网线，它上面配置IP没有意义，反而会影响路由表：</p>
<pre class="crayon-plain-tag">ip addr del 192.169.1.105/24 dev eth0</pre>
<p>eth0被删除后默认路由消失，重新添加默认路由后，可以从veth1连接外部网络。</p>
<div class="blog_h3"><span class="graybg">网桥和虚拟化</span></div>
<p>对于虚拟机来说，通常<span style="background-color: #c0c0c0;">通过tun/tap等网络设备，将虚拟机内的网卡连接到网桥</span>，再由网桥转发给出去。</p>
<p>对于容器来说，每个则通常使用veth对。容器的网关被桥接到br0，且网关设置为br0。容器IP封包发到br0后，进入宿主机协议栈。宿主机需要配置IP转发功能，可以把容器IP封包转发出去。由于容器、宿主机物理网络通常不是一个网段，因此转发出去之前通常需要NAT。</p>
<div class="blog_h3"><span class="graybg">网桥和混杂模式</span></div>
<p>混杂模式下，网卡会把所有接收到的流量交给协议栈处理，不管目的MAC地址是否匹配。</p>
<p>使用下面的命令，使网卡进入混杂模式：</p>
<pre class="crayon-plain-tag">ifconfig eth0 promisc</pre>
<p>使用下面的命令则退出混杂模式：</p>
<pre class="crayon-plain-tag">ifconfig eth0 -promisc</pre>
<p>需要注意的时，<span style="background-color: #c0c0c0;">加入到网桥后，设备自动进入混杂模式</span>。退出网桥后则自动退出混杂模式。另外加入网桥期间，设备无法退出混杂模式。  </p>
<div class="blog_h2"><span class="graybg">Bridge+VLAN</span></div>
<p>通过网桥，可以协助处理虚拟化/命名空间下的基于多VLAN的网络虚拟化。</p>
<div class="blog_h3"><span class="graybg">VLAN filtering</span></div>
<p>VLAN filtering是在3.8引入的特性，可以简化基于VLAN的网络虚拟化的配置复杂度。利用VLAN filtering，管理员不需要创建大量的VLAN、Bridge。<span style="background-color: #c0c0c0;">使用一个网桥，你就能够控制所有VLAN</span>。</p>
<p>这里我们看一个在虚拟化中广泛使用的网络拓扑：不同子网的虚拟机通过网桥，通过bond做负载均衡，最后连接到物理网卡。</p>
<p>没有VLAN filtering之前，拓扑图如下：</p>
<p><a href="https://cdn.gmem.cc/wp-content/uploads/2020/06/bridge_original.png"><img class="size-full wp-image-32969 aligncenter" src="https://cdn.gmem.cc/wp-content/uploads/2020/06/bridge_original.png" alt="bridge_original" width="538" height="370" /></a></p>
<p>创建此拓扑的步骤：</p>
<ol>
<li>创建bond设备，作为两张物理网卡的master：<br />
<pre class="crayon-plain-tag">ip link add bond0 type bond
ip link set bond0 type bond miimon 100 mode balance-alb

ip link set eth0 down
ip link set eth0 master bond0

ip link set eth1 down
ip link set eth1 master bond0

ip link set bond0 up </pre>
</li>
<li>在bond0上创建VLAN子接口：<br />
<pre class="crayon-plain-tag">ip link add link bond0 name bond0.2 type vlan id 2
ip link set bond0.2 up

ip link add link bond0 name bond0.3 type vlan id 3
ip link set bond0.3 up</pre>
</li>
<li>
<p>将VLAN子接口分别连接到一个网桥：</p>
<pre class="crayon-plain-tag">ip link add br0 type bridge
ip link set bond0.2 master br0
ip link set br0 up

ip link add br1 type bridge
ip link set bond0.3 master br1
ip link set br1 up </pre>
</li>
<li>
<p>将代表虚拟机的TAP设备也连接到网桥：
<pre class="crayon-plain-tag">ip link set guest_1_tap_0 master br0
ip link set guest_2_tap_0 master br0

ip link set guest_2_tap_1 master br1
ip link set guest_3_tap_0 master br1 </pre>
</li>
</ol>
<p>如果使用了VLAN filtering，就不需要创建bond的VLAN子接口，也仅需要一个网桥：
<p><a href="https://cdn.gmem.cc/wp-content/uploads/2020/06/bridge_current.png"><img class="size-full wp-image-32971 aligncenter" src="https://cdn.gmem.cc/wp-content/uploads/2020/06/bridge_current.png" alt="bridge_current" width="541" height="374" /></a></p>
<p>创建此拓扑的步骤：</p>
<ol>
<li>
<p> 创建bond设备：</p>
<pre class="crayon-plain-tag">ip link add bond0 type bond
ip link set bond0 type bond miimon 100 mode balance-alb
ip link set eth0 down
ip link set eth0 master bond0
ip link set eth1 down
ip link set eth1 master bond0
ip link set bond0 up</pre>
</li>
<li>
<p>创建网桥，启用VLAN Filtering： 
<pre class="crayon-plain-tag">ip link add br0 type bridge
ip link set br0 up
ip link set br0 type bridge vlan_filtering 1

ip link set bond0 master br0</pre>
</li>
<li>
<p> 将TAP连接到网桥：
<pre class="crayon-plain-tag">ip link set guest_1_tap_0 master br0
ip link set guest_2_tap_0 master br0

ip link set guest_2_tap_1 master br0
ip link set guest_3_tap_0 master br0</pre>
</li>
<li>为这些TAP接口、Bond接口设置VLAN filter：<br />
<pre class="crayon-plain-tag">bridge vlan add dev guest_1_tap_0 vid 2 pvid untagged master
bridge vlan add dev guest_2_tap_0 vid 2 pvid untagged master

bridge vlan add dev guest_2_tap_1 vid 3 pvid untagged master
bridge vlan add dev guest_3_tap_0 vid 3 pvid untagged master

bridge vlan add dev bond0 vid 2 master
bridge vlan add dev bond0 vid 3 master </pre>
</li>
</ol>
<div class="blog_h2"><span class="graybg">TUN/TAP</span></div>
<p>简单的理解：TUN/TAP就是针对用户空间程序而不是物理媒体的网络接口，这种网络接口的<span style="background-color: #c0c0c0;">一端连接着网络协议栈，另一端连接着用户空间程序</span>。不同于普通靠硬件板卡实现的设备，TUN/TAP全部用软件实现，并向运行于操作系统上的<span style="background-color: #c0c0c0;">软件提供与硬件的网络设备完全相同的功能</span>。
<p>TUN/TAP为用户空间程序提供数据包的收发功能，它可以呈现为简单的点对点设备或者以太网设备，与物理设备不同的是，它<span style="background-color: #c0c0c0;">从用户空间程序</span>接收数据包<span style="background-color: #c0c0c0;">而不是物理媒体</span>；同样的，它将数据包发送给用户空间程序，而不是物理媒体。操作系统通过TUN/TAP设备发送的数据包，被分发给关联到这些设备的用户空间程序中。用户空间程序也可以将数据包发给TUN/TAP设备，后者把数据包注入到操作系统的网络栈之中，模拟为来自外部的数据。</p>
<p>要使用TUN驱动，必须打开/dev/net/tun文件，并发起相应的ioctl()调用，来<span style="background-color: #c0c0c0;">向内核注册一个网络设备</span>，这些网络设备可以通过ifconfig看到，名字为tunXXX或者tapXXX（依据调用时的选项）。当关闭文件描述符时，这些网络设备连同相应的路由会消失。用户程序写入/dev/net/tun文件的数据，会写到内核网络协议栈；用户程序从/dev/net/tun文件读取时，则拿到内核发送给tun设备的IP封包。</p>
<p>要使用TAP驱动，基本和TUN完全相同，区别有几点：</p>
<ol>
<li>tun设备的/dev/tunX工作在L3，可以通过IP转发和物理网卡连通</li>
<li>tap设备的/dev/tapX工作在L2，可以和物理网卡进行桥接</li>
</ol>
<div class="blog_h3"><span class="graybg">TUN</span></div>
<p><span style="background-color: #c0c0c0;">TUN模拟了网络层（点对点）设备</span>，操作第三层数据包比如IP数据封包。普通的网卡通过网线收发数据包，但是 TUN 设备通过一个文件收发数据包。所有对这个文件的<span style="background-color: #c0c0c0;">写操作会通过 TUN 设备转换成一个数据包送给内核</span>；当内核发送一个包给 TUN 设备时，通过<span style="background-color: #c0c0c0;">读这个文件可以拿到数据包的内容</span>：</p>
<p><a href="https://blog.gmem.cc/wp-content/uploads/2010/05/tun.png"><img class="aligncenter size-full wp-image-26513" src="https://blog.gmem.cc/wp-content/uploads/2010/05/tun.png" alt="tun" width="590" height="266" /></a></p>
<p>TUN 设备的 /dev/tunX 文件收发的是 IP 层数据包，只能工作在 IP 层，<span style="background-color: #c0c0c0;">无法与物理网卡做 bridge</span>，但是可以通过三层交换（如 ip_forward）与物理网卡连通。</p>
<p>TUN的含义即隧道，实际上TUN的确和隧道技术有关系。tun/tap设备将协议栈的部分数据转发给用户空间程序，使之有处理数据包的机会，例如加密、压缩。VPN是最常见的应用场景。下面是基于TUN设备实现VPN的过程：</p>
<ol>
<li>本地应用构造一个数据包，发送给tun0所在网段（虚拟网络）的IP地址192.168.1.3</li>
<li>数据包到达内核协议栈后，根据目的IP地址判断需要从tun0发出</li>
<li>tun0的一端连接着内核协议栈，另一端则连接着用户空间应用 —— VPN程序</li>
<li>VPN程序对数据包进行再次封装，源地址设置为eth0，目的地址设置为eth0所在网段的VPN对端物理机IP地址</li>
<li>封装后数据包发送到协议栈，走eth0发送到VPN对端</li>
</ol>
<p>Linux的L3隧道技术都是基于TUN设备，包括ipip、GRE、sit、ISATAP、VTI等。</p>
<div class="blog_h3"><span class="graybg">TAP </span></div>
<p>TAP 等同于一个以太网设备，它操作第二层数据包如以太网数据帧。TAP的工作方式和TUN完全相同。</p>
<p>TAP 设备的 /dev/tapX 文件收发的是 MAC 层数据包，拥有 MAC 层功能，<span style="background-color: #c0c0c0;">可以与物理网卡做 bridge</span>，支持 MAC 层广播。</p>
<p>Libvirt创建的VM，会在宿主机上对应一个vnet*接口，这个接口桥接到网桥设备virbr*：</p>
<pre class="crayon-plain-tag">virbr0		8000.100000000000	no		virbr0-nic
							vnet0
							vnet1</pre>
<p>这些vnet接口，就是TAP设备，它关联到一个进程，即运行qemu-kvm模拟器的那个进程。qemu-kvm写入到此接口的数据，在宿主机看来，就好像从vnet这个网卡接收到的封包似的。反之亦然，qemu-kvm从此接口读取的数据，会写入到虚拟机的eth0，在虚拟机看来，就好像是从网卡接收到封包似的。</p>
<div class="blog_h2"><span class="graybg">VETH</span></div>
<p>也是虚拟的以太网设备，可以作为<span style="background-color: #c0c0c0;">网络命名空间的隧道，让两个网络命名空间可以通信</span>。VETH也能够作为独立网络设备使用。从效果上说，VETH就像是两个网络接口通过RJ45网线连接在一起似的。</p>
<p>VETH设备总是成对形式创建的：</p>
<pre class="crayon-plain-tag"># 创建一对veth，相互连接起来
ip link add veth0 type veth peer name veth1

# 查看veth对
ip link list
# 13: veth1@veth0: &lt;BROADCAST,MULTICAST,M-DOWN&gt; mtu 1500 qdisc noop state DOWN mode DEFAULT group default qlen 1000
#     link/ether 72:a4:a7:eb:e8:db brd ff:ff:ff:ff:ff:ff
# 14: veth0@veth1: &lt;BROADCAST,MULTICAST,M-DOWN&gt; mtu 1500 qdisc noop state DOWN mode DEFAULT group default qlen 1000
#     link/ether 46:ba:9b:5e:3a:27 brd ff:ff:ff:ff:ff:ff</pre>
<p><span style="background-color: #c0c0c0;">从一端发送的数据，会立即在另一端接收到</span>。任何一端Down掉或删除，则veth对的状态被破坏。</p>
<p>可以把veth放到其他网络命名空间：</p>
<pre class="crayon-plain-tag"># 创建一个新的网络命名空间
ip netns add test

# 移动veth1
ip link set veth1 netns test</pre>
<p>移动了以后，从当前命名空间看到的，Peer的名字会变化：</p>
<pre class="crayon-plain-tag"># 默认命名空间看veth1变为if13，表示Peer是test命名空间序号为13的网络接口
ip link show veth0
# 14: veth0@if13: &lt;BROADCAST,MULTICAST&gt; mtu 1500 qdisc noop state DOWN mode DEFAULT group default qlen 1000
#     link/ether 46:ba:9b:5e:3a:27 brd ff:ff:ff:ff:ff:ff link-netnsid 3

# test命名空间看veth0变为if14，和上面的序号14匹配
ip netns exec test ip link show veth1
# 13: veth1@if14: &lt;BROADCAST,MULTICAST&gt; mtu 1500 qdisc noop state DOWN mode DEFAULT group default qlen 1000
#    link/ether 72:a4:a7:eb:e8:db brd ff:ff:ff:ff:ff:ff link-netnsid 0</pre>
<p>在宿主机上，通过<span style="background-color: #c0c0c0;">设置路由</span>，可以让其他网络命名空间的流量通过veth对出站。 </p>
<div class="blog_h3"><span class="graybg">和TAP区别</span></div>
<p>VETH和TAP都是用来传递L2以太网帧的，<span style="background-color: #c0c0c0;">TAP/TUN常用于用于加密、VPN、隧道、虚拟机</span>，VETH常用于不同命名空间之间进行数据穿越。这有历史问题在里面。</p>
<p>下面是OpenStack创建了新的虚拟机vm0后的网络架构图：</p>
<p><a href="https://blog.gmem.cc/wp-content/uploads/2006/09/Openstack-VM-details.png"><img class="aligncenter size-full wp-image-27119" src="https://blog.gmem.cc/wp-content/uploads/2006/09/Openstack-VM-details.png" alt="openstack-vm-details" width="700" height="471" /></a></p>
<p>关于虚拟机vm0的网络模型，说明如下：</p>
<ol>
<li>虚拟机中的eth0，连接到vnet0，vnet0是Linux网桥<span style="background-color: #c0c0c0;">qbrXXX的TAP接口</span></li>
<li>Linux网桥qbrXXX连接到OVS网桥br-int，通过一对VETH qvbXXX - qv0XXX</li>
</ol>
<p>可以看到，通过TAP将虚拟机连接到第一个网桥qbrXXX，而VETH对则将第一个网桥连接到第二个。为什么需要两根RJ45网线呢？用其中一个不行么？</p>
<p>主要原因是遗留技术的存在：</p>
<ol>
<li>当KVM产生一个虚拟机，它<span style="background-color: #c0c0c0;">期望一个TAP接口连接到虚拟机的以太网端口</span>（eth0），这样KVM就得到一个FD，它在其上读写以太网帧。也就是说<span style="background-color: #c0c0c0;">TAP是去不掉</span>的</li>
<li>而VETH是相对新的技术，能够支持Linux Bridge、命名空间、Open vSwitch等技术</li>
</ol>
<div class="blog_h2"><span class="graybg">MacVLAN</span></div>
<p>有时我们需要一块物理网卡<span style="background-color: #c0c0c0;">绑定多个 IP 以及多个 MAC 地址</span>，虽然<span style="background-color: #c0c0c0;">绑定多个 IP 很容易（通过网卡别名，例如eth0:1），但是这些 IP 会共享物理网卡的 MAC 地址</span>，可能无法满足我们的设计需求，所以有了 macvlan设备，其工作方式如下：</p>
<p><a href="https://blog.gmem.cc/wp-content/uploads/2010/05/macvlan.png"><img class="aligncenter size-full wp-image-26511" src="https://blog.gmem.cc/wp-content/uploads/2010/05/macvlan.png" alt="macvlan" width="690" height="322" /></a></p>
<p>配合网络命名空间机制，可以在<span style="background-color: #c0c0c0;">不需要建立Bridge</span>的情况下，为虚拟机建立独立的网络栈：</p>
<p><a href="https://blog.gmem.cc/wp-content/uploads/2010/05/macvlan-network-ns.png"><img class="aligncenter size-full wp-image-26517" src="https://blog.gmem.cc/wp-content/uploads/2010/05/macvlan-network-ns.png" alt="macvlan-network-ns" width="650" height="252" /></a></p>
<p>&nbsp;</p>
<p>MacVLAN接口可以看做是物理以太网接口的N个虚拟子接口（sub interface），每个子接口都具有区别于父接口（parent interface）的MAC地址，并且可以像普通网卡一样分配IP地址。这种<span style="background-color: #c0c0c0;">虚拟网卡在逻辑上，和物理网卡具有对等的地位</span>。</p>
<p>父接口可以是一个物理接口（例如eth0），可以是一个 802.1q 的子接口（例如eth0.10），也可以是 bonding 接口。</p>
<p>除了虚拟化之外，Keepalived也通过MacVLAN来使用虚拟Mac地址。</p>
<p>需要注意，使用MacVLAN的虚拟机/容器，<span style="background-color: #c0c0c0;">和主机共享一个网段</span>。如果虚拟机/容器<span style="background-color: #c0c0c0;">需要和宿主机通信，需要额外创建一个子接口给宿主机</span>使用</p>
<p>MacVLAN支持5种模式：</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 13%; text-align: center;">模式</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td class="blog_h3">bridge</td>
<td>
<p>类似于Linux Bridge，最常用。适合共享父接口的MacVLAN虚拟网卡直接进行通信的场景</p>
<p>这种模式下，共享父接口的虚拟网卡可以直接通信，不需要把流量发送到父接口外部</p>
<p>示例：</p>
<pre class="crayon-plain-tag"># 创建MacVLAN子接口，父接口为eth0，MAC地址默认自动分配，如需指定，可以 address 00:00:00:00:00:09
ip link add link eth0 name macv1 type macvlan mode bridge
ip link add link eth0 name macv2 type macvlan mode bridge

# if link可以看到新创建的两个子接口
5: macv1@eth0: &lt;BROADCAST,MULTICAST&gt; mtu 1500 qdisc noop state DOWN mode DEFAULT group default qlen 1
    link/ether a6:6d:2c:6e:33:bf brd ff:ff:ff:ff:ff:ff
6: macv2@eth0: &lt;BROADCAST,MULTICAST&gt; mtu 1500 qdisc noop state DOWN mode DEFAULT group default qlen 1
    link/ether 12:c9:36:fe:f8:6c brd ff:ff:ff:ff:ff:ff

# 移动到命名空间中
ip netns add net1
ip netns add net2
ip link set macv1 netns net1
ip link set macv2 netns net2

# 查看命名空间中的接口状态
ip netns exec net1 ip link
1: lo: &lt;LOOPBACK&gt; mtu 65536 qdisc noop state DOWN mode DEFAULT group default qlen 1
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
# 类似于VETH对的一端，移动到命名空间后，使用@if2指明父接口在宿主机网络命名空间的序号
5: macv1@if2: &lt;BROADCAST,MULTICAST&gt; mtu 1500 qdisc noop state DOWN mode DEFAULT group default qlen 1
    link/ether a6:6d:2c:6e:33:bf brd ff:ff:ff:ff:ff:ff link-netnsid 0

# 配置IP地址
ip netns exec net1 ip addr add dev macv1 10.0.0.200/16
ip netns exec net2 ip addr add dev macv2 10.0.0.201/16
ip netns exec net1 ip link set dev macv1 up
ip netns exec net2 ip link set dev macv2 up

# 通信
ip netns exec net1 ping 10.0.0.1    # OK
ip netns exec net1 ping 10.0.0.201  # OK
ip netns exec net1 ping 10.0.0.200  # 失败
ip net exec net1 ip link set lo up  # 原因是命名空间中lo默认DOWN，导致不能PING自己</pre>
</td>
</tr>
<tr>
<td class="blog_h3">VEPA</td>
<td>
<p>Virtual Ethernet Port Aggregator，虚拟以太网端口聚合，这是默认的模式
<p>所有虚拟接口发出的流量，不管目的地是什么（即使是共享父接口的兄弟虚拟接口），都发给父接口</p>
<p>需要连接父接口们的交换机支持<span style="background-color: #c0c0c0;">Hairpin模式（可以把某个端口发出的包反射回去）</span>，把源、目的地址都是本地MacVLAN虚拟接口地址的流量，发给相应接口</p>
<p>大部分交换机设备不支持Hairpin模式，但是Linux Bridge可以支持：</p>
<pre class="crayon-plain-tag"># 对于网桥br0的端口eth0，从此端口收到的包，允许（可能经过某种处理后）从此端口再发回去
brctl hairpin br0 eth0 on
# or
ip link set dev eth0 hairpin on</pre>
</td>
</tr>
<tr>
<td class="blog_h3">Private</td>
<td>类似于VEPA，但是增强了隔离能力，阻止共享父接口的MacVLAN虚拟接口之间的通信</td>
</tr>
<tr>
<td class="blog_h3">Passthrough</td>
<td>这种模式下一个父接口只能和单个MacVLAN虚拟接口绑定，并且子接口继承父接口的MAC地址</td>
</tr>
<tr>
<td class="blog_h3">Source</td>
<td>MacVLAN接口只接受指定源MAC地址的数据包 </td>
</tr>
</tbody>
</table>
<div class="blog_h3"><span class="graybg">跨主机通信</span></div>
<p>位于两台机器上的MacVLAN虚拟网卡要实现通信，需要满足：
<ol>
<li>两台机器的父接口都处于混杂模式</li>
<li>两台主机的MacVLAN子网IP段不重叠</li>
</ol>
<div class="blog_h3"><span class="graybg">和父接口通信</span></div>
<p>在MacVLAN的虚拟网络中，父接口仅仅相当于一个交换机。对于进出子MacVLAN接口的数据包，物理网卡仅仅转发而不处理，这导致使用本机MacVLAN网卡的IP无法和物理网卡的IP进行通信。</p>
<div class="blog_h3"><span class="graybg">缺点</span></div>
<p>MacVLAN是将虚拟机、容器连接到物理网络的简单方法。但是它有以下缺点：</p>
<ol>
<li>每个虚拟接口都需要MAC地址，但是物理交换器支持的MAC地址数量会有限制，许多物理网卡支持的MAC地址数量也会有限制</li>
<li>IEEE 802.11（WiFi）不支持一个客户端有多个MAC地址</li>
<li>在云上环境，VPC往往对入站报文的MAC地址进行校验，Linux Bridge面临同样的问题</li>
</ol>
<p>面临这些缺点时，可以考虑使用IPVLAN。</p>
<div class="blog_h2"><span class="graybg">IPVLAN</span></div>
<p>类似于MacVLAN，都是在一个父接口下虚拟出多个子接口。不同之处是，<span style="background-color: #c0c0c0;">IPVLAN的所有子接口MAC相同，仅仅IP不同</span>。</p>
<p>IPVLAN从3.19开始支持，<span style="background-color: #c0c0c0;">比较稳定的版本需要4.2+</span>。Docker对老版本的支持存在缺陷。</p>
<p>某些DHCP服务器在分配IP地址时，以MAC地址作为机器的标识，这和IPVLAN无法协同工作。</p>
<p>IPVLAN支持L2、L3等模式，<span style="background-color: #c0c0c0;">一个父接口同时只能在一种模式下工作</span>。</p>
<p><span style="background-color: #c0c0c0;">IPVLAN无法从宿主机访问子接口的IP地址，也不能从子接口所在命名空间访问父接口的IP地址</span>。你可以额外创建一对veth，配合路由，来解决这个问题。</p>
<div class="blog_h3"><span class="graybg">L2模式</span></div>
<p>这种模式下，和MacVLAN工作方式很类似。父接口作为交换机，转发子接口的数据。<span style="background-color: #c0c0c0;">子接口直接加入父接口所在的二层网络</span>。</p>
<p>实验：</p>
<pre class="crayon-plain-tag">ip link add link eth0 ipvlan1 type ipvlan mode l2
ip link add link eth0 ipvlan2 type ipvlan mode l2

ip net add net1
ip net add net2

ip link set ipvlan1 netns net1
ip link set ipvlan2 netns net2

ip net exec net1 ip link set ipvlan1 up
ip net exec net2 ip link set ipvlan2 up

ip net exec net1 ip addr add 10.0.10.1/16 dev ipvlan1
ip net exec net2 ip addr add 10.0.10.2/16 dev ipvlan2

ip net exec net1 route add default dev ipvlan1
ip net exec net2 route add default dev ipvlan2

ip net exec net1 ip link set ipvlan1 up
ip net exec net2 ip link set ipvlan2 up

ip net exec net1 ip link set lo up
ip net exec net2 ip link set lo up</pre>
<div class="blog_h3"><span class="graybg">L3模式</span></div>
<p>这种模式下， 父接口类似于路由器。它在各个<span style="background-color: #c0c0c0;">虚拟网络和主机网络之间</span>进行不同网络报文的<span style="background-color: #c0c0c0;">路由转发</span>工作。<span style="background-color: #c0c0c0;"> 只要父接口相同，即使虚拟机/容器不在同一个网络，也可以互相ping通对方</span>，因为ipvlan会在中间做报文的转发工作。 L3模式下的虚拟接口不会接收到多播或者广播的报文，所有的<span style="background-color: #c0c0c0;"> ARP 过程或者其他多播报文都是在底层的父接口</span>完成的。</p>
<p>外部网络不会理解IPVLAN虚拟出来的网络，因此如果外部路由器上不配置适当路由规则，IPVLAN的虚拟IP无法被外部访问。 </p>
<p>实验：</p>
<pre class="crayon-plain-tag">ip netns add net1
ip netns add net2

# 添加L3模式的IPVLAN子接口
ip link add link eth0 ipvlan1 type ipvlan mode l3
ip link add link eth0 ipvlan2 type ipvlan mode l3

# 可以发现子接口的MAC地址和父接口相同
2: eth0: &lt;BROADCAST,MULTICAST,UP,LOWER_UP&gt; mtu 1500 qdisc pfifo_fast state UP mode DEFAULT group default qlen 1000
    link/ether 10:00:00:00:00:07 brd ff:ff:ff:ff:ff:ff
5: ipvlan1@eth0: &lt;BROADCAST,MULTICAST&gt; mtu 1500 qdisc noop state DOWN mode DEFAULT group default qlen 1000
    link/ether 10:00:00:00:00:07 brd ff:ff:ff:ff:ff:ff
6: ipvlan2@eth0: &lt;BROADCAST,MULTICAST&gt; mtu 1500 qdisc noop state DOWN mode DEFAULT group default qlen 1000
    link/ether 10:00:00:00:00:07 brd ff:ff:ff:ff:ff:ff

# 移动到网络命名空间
ip link set ipvlan1 netns net1
ip link set ipvlan2 netns net2</pre>
<p>注意，由于父子接口的MAC地址一样，因此不能通过DHCP分配IP地址，必须手工添加：</p>
<pre class="crayon-plain-tag">ip net exec net1 ip addr add 10.98.0.1/16 dev ipvlan1
ip net exec net1 ip link set dev ipvlan1 up
ip net exec net2 ip addr add 10.99.0.1/16 dev ipvlan2
ip net exec net2 ip link set dev ipvlan2 up

# 添加路由
ip net exec net1 route add default dev ipvlan1
ip net exec net2 route add default dev ipvlan2

ip net exec net1 ping 10.99.0.1  # OK

# 在宿主机的L2网络的其它主机上，必须有路由才能访问IPVLAN子接口
#                                        IPVLAN父接口IP
ip route add 10.98.0.0/16 dev virbr0 via 10.0.0.7
ip route add 10.99.0.0/16 dev virbr0 via 10.0.0.7</pre>
<div class="blog_h3"><span class="graybg">L3S模式</span></div>
<p>类似于L3模式，但是L3S模式下出入流量均<span style="background-color: #c0c0c0;">经过宿主机网络命名空间的三层网络（L2/L3模式则不经过）</span>，会被宿主机的netfilter框架过滤。</p>
<p>这意味着L3S可以支持kube-proxy，但是，它又会引入以下问题：</p>
<ol>
<li>当K8S服务的客户端、Pod位于同一节点时，访问服务的应答报文会走ipvlan datapath，接收不到</li>
<li>同样场景下，同一方向流量多次进出宿主机 conntrack，datapath复杂，和iptables/ipvs也存在兼容性问题</li>
</ol>
<p>可以为容器额外引入VETH（另外一端放在宿主机），通过路由设置，让K8S服务的流量从VETH出站，解决Service无法访问的问题。</p>
<div class="blog_h2"><span class="graybg">MacVTap</span></div>
<p>Macvtap是一个新式驱动，用于简化虚拟化网络桥接。它基于macvlan设备驱动，将macvlan和TAP的优点进行整合。它使用macvlan的方式来收发数据包，但是收到的数据包<span style="background-color: #c0c0c0;">不会交给独立网络栈处理，而是交给/dev/tapX文件</span>：</p>
<p><a href="https://blog.gmem.cc/wp-content/uploads/2010/05/macvtap.png"><img class="aligncenter size-full wp-image-26521" src="https://blog.gmem.cc/wp-content/uploads/2010/05/macvtap.png" alt="macvtap" width="770" height="280" /></a></p>
<p>由于 macvlan是工作在 MAC 层的，所以 macvtap 也只能工作在 MAC 层，不会存在macvtun这样的设备。</p>
<p>一个macvtap端点（endpoint）是一个字符设备，它很大程度上遵循tun/tap ioctl接口，可以被KVM/QEMU或者其它支持tun/tap接口的hypervisors直接使用。<span style="background-color: #c0c0c0;">macvtap端点扩展一个既有网络接口（底层设备，lower device），具有自己的MAC地址</span>，位于与被扩展接口相同的网段。典型情况下，macvtap让宿主机、客户机一同出现在物理网络上。</p>
<div class="blog_h3"><span class="graybg">工作模式</span></div>
<p>类似于macvlan，一个macvtap可以工作在以下三种模式之一。这些模式定义了连接到同一lower device的macvtap端点之间的通信方式：</p>
<table class=" full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 15%; text-align: center;">模式</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>VEPA</td>
<td>
<p>即虚拟以太网端口聚合器（Virtual Ethernet Port Aggregator），这是默认的模式。从一个端点发送到另外一个端点的数据下发到lower device，进而发送到外部交换机，如果外部交换机支持hairpin模式，则以太网帧被发回lower device进而发送到目标端点。然而，大部分现代交换机都不支持hairpin模式，这意味着两个端点不能交换以太网帧（尽管它们可以通过TCP/IP路由器通信）</p>
</td>
</tr>
<tr>
<td>Bridge</td>
<td>直接连接端点，两个同时处于bridge模式的macvlan可以直接交换以太网帧。对于典型的交换机，这是最有用的模式</td>
</tr>
<tr>
<td>private</td>
<td>类似于VEPA，但是即使交换机支持hairpin，也被忽略，这种模式下端点之间绝不能相互通信</td>
</tr>
</tbody>
</table>
<div class="blog_h1"><span class="graybg">反向代理</span></div>
<p>亦称替代物（surrogate)，这类代理可以假扮Web服务器，接收发给 Web 服务器的真实请求，它可以向一个或者多个服务器索取内容，并返回给客户端，在客户端看来，好像反向代理就是真正的服务器。 反向代理有以下应用场景：</p>
<ol>
<li>反向代理可以缓存静态、动态内容，从而提高访问慢速Web服务器上公共内容时的性能，在此场景下反向代理被称为服务器加速器（Server accelerator）</li>
<li>隐藏原始服务器的存在和特性</li>
<li>为不支持SSL的服务器提供SSL支持</li>
<li>负载均衡：分散入站请求到多个服务器上</li>
<li>任何几个Web服务器需要通过同一IP地址访问的时候，可以将反向代理作为前置机</li>
</ol>
<p>&nbsp;</p>
<div class="blog_h1"><span class="graybg"><a id="transparent-proxy"></a>透明代理</span></div>
<p>所谓透明代理，它透明在：</p>
<ol>
<li>对于客户端，发起连接时连接的服务端是真实的服务器而不是代理服务器</li>
<li>对于服务器，收到的请求来自真实客户端而不是代理服务器</li>
</ol>
<p>必须具有某种机制，在客户端毫无知觉的情况下，将它的请求劫持给代理服务器处理。同样的，真实服务器发回的响应也需要直接发给代理服务器，而不是客户端。</p>
<p>实现透明代理，需要多方面的配合。客户端（C）、代理（P）、服务器（S）的部署架构会很大程度上影响技术实现。主要关注点包括：</p>
<ol>
<li>网络路由：目的地是S的IP包路由给P处理。如果S和P部署在一起（例如用于Sidecar部署的Envoy），通过Linux本身的能力就能达成</li>
<li>IP_TRANSPARENT：P必须启用此套接字选项</li>
</ol>
<div class="blog_h2"><span class="graybg">路由处理</span></div>
<p>当封包到达Linux内核时，它要么被路由，要么被丢弃，要么<span style="background-color: #c0c0c0;">被Linux本地处理 —— 如果目的地址匹配本地地址的话</span>。</p>
<p><span style="background-color: #c0c0c0;">什么是本地地址，是可以设置的</span>，甚至你可以设置为0.0.0.0/0 —— 任何地址都是本地地址，但这样设置会导致系统无法连接到任何远程地址，因为所有地址都是本地地址了…应当发给本地进程处理。幸运的是，<span style="background-color: #c0c0c0;">通过独立路由表，我们可以选择性的将一部分封包路由到本地</span>：</p>
<pre class="crayon-plain-tag"># 对于任何目的端口是53的封包，设置标记
iptables -t mangle -I PREROUTING -p udp --dport 5301 -j MARK --set-mark 1

# 然后配置路由表，强制将其从lo网卡发出，而不管目的地址是什么
ip rule add fwmark 1 lookup 100
ip route add local 0.0.0.0/0 dev lo table 100 </pre>
<p>注意，需要启用IP转发功能：</p>
<pre class="crayon-plain-tag">sysctl net.ipv4.conf.all.forwarding=1
sysctl net.ipv6.conf.all.forwarding=1</pre>
<p>某些情况下反向路径过滤器（reverse path filter）可能会丢弃你拦截的封包，禁用之：</p>
<pre class="crayon-plain-tag">sysctl net.ipv4.conf.eth0.rp_filter=0</pre>
<div class="blog_h2"><span class="graybg">IP_TRANSPARENT</span></div>
<p>套接字选项IP_TRANSPARENT可以<span style="background-color: #c0c0c0;">绑定不属于本机的IP地址</span>，<span style="background-color: #c0c0c0;">P需要启用此选项</span>。此选项的作用：</p>
<ol>
<li>作为S的客户端时，此选项允许P使用不属于本机的IP地址作为源IP（也就是C的地址）发起连接</li>
<li>作为C的服务器时，可以侦听（绑定到）不属于本机的IP地址（也就是S的地址）的连接请求。比如开启此IP_TRANSPARENT选项同时监听0.0.0.0.80，那么本机接收到的DST地址是192.168.0.189:80的TCP请求可被监听</li>
<li>接受被TPROXY重定向的连接和封包</li>
</ol>
<p>下面是一段基于<a href="https://github.com/ahupowerdns/simplesocket">simplesocket</a>的代码，说明了IP_TRANSPARENT如何指定任意源地址的。注意需要CAP_NET_ADMIN权限才能运行：</p>
<pre class="crayon-plain-tag"># 基于TCP/IP协议族的UDP套接字 
Socket s(AF_INET, SOCK_DGRAM, 0);
# 套接字选项，IP_TRANSPARENT
SSetsockopt(s, IPPROTO_IP, IP_TRANSPARENT, 1);
# 使用的源地址
ComboAddress local("1.2.3.4", 5300);
# 使用的目的地址
ComboAddress remote("198.41.0.4", 53);
# 绑定到源地址，注意此地址不是本机地址 
SBind(s, local);
# 发送UDP包
SSendto(s, "hi!", remote);</pre>
<p>使用tcpdump可以发现：</p>
<pre class="crayon-plain-tag"># tcpdump -n host 1.2.3.4
# 源地址不是本机
21:29:41.005856 IP 1.2.3.4.5300 &gt; 198.41.0.4.53</pre>
<div class="blog_h2"><span class="graybg">TPROXY</span></div>
<p>Iptables支持一个特殊的目标TPROXY，它能够拦截流量，并将其转发给任意特定的本地IP地址，同时为封包设置标记：</p>
<pre class="crayon-plain-tag"># 修改针对25端口的TCP流量封包
iptables -t mangle -A PREROUTING -p tcp --dport 25 -j TPROXY
  # 设置0x1/0x1标记
  --tproxy-mark 0x1/0x1 
  # 转发给127.0.0.1:10025，但是不改变IP封包头的任何信息
  --on-port 10025      # 默认原始目的端口
  --on-ip 127.0.0.1    # 默认接收到流量的网卡地址</pre>
<p>注意：</p>
<ol>
<li>--tproxy-mark，它确保此封包通过正确的路由表发出。参考第一小节的路由，此封包  xxx:xxx:127.0.0.1:10025，将从lo网卡出网络栈</li>
<li>TPROXY和REDIRECT不同，后者本质上是DNAT，会改变IP封包头的目的地址</li>
</ol>
<div class="blog_h2"><span class="graybg">SocketMatch</span></div>
<p>Iptables匹配扩展socket能够<span style="background-color: #c0c0c0;">匹配和本地套接字相关的封包</span>，它会进行Socket Hash查找，并且在以下条件下匹配：</p>
<ol>
<li>本地存在已经建立的套接字，匹配此封包</li>
<li>如果本地存在非零监听套接字，并且地址端口匹配封包的地址端口</li>
</ol>
<p>示例：</p>
<pre class="crayon-plain-tag">iptables -t mangle -A PREROUTING -p tcp -m socket -j DIVERT

iptables -t mangle -N DIVERT
iptables -t mangle -A DIVERT -j MARK --set-mark 1
iptables -t mangle -A DIVERT -j ACCEPT

# 配合路由表</pre>
<div class="blog_h2"><span class="graybg">获取原始目的地址</span></div>
<p>使用IP_TRANSPARENT时：</p>
<ol>
<li>对于TCP连接，套接字的原始目的地址、端口可以通过getsockname()调用获得</li>
<li>对于UDP连接，你必须设置套接字选项IP_RECVORIGDSTADDR：<br />
<pre class="crayon-plain-tag">setsockopt (s, IPPROTO_IP, IP_RECVORIGDSTADDR, &amp;n, sizeof(int));</pre></p>
<p>然后，调用recvmsg()方法：</p>
<pre class="crayon-plain-tag">char orig_ip[32] = {0};
int orig_port = 0;
struct sockaddr_in *orig_addr;
for (cmsg = CMSG_FIRSTHDR(&amp;msg); cmsg != NULL; cmsg = CMSG_NXTHDR(&amp;msg,cmsg)) {
    if (cmsg-&gt;cmsg_level == SOL_IP &amp;&amp; cmsg-&gt;cmsg_type == IP_ORIGDSTADDR) {
        orig_addr = (struct sockaddr_in *) CMSG_DATA(cmsg);
        transfer_sock_addr(orig_addr, orig_ip, 32, &amp;orig_port);
        break;
    }
}
if (cmsg == NULL) {
    printf("IP_ORIGDSTADDR not enabled or small buffer or I/O error");
    return;
} </pre>
</li>
</ol>
<div class="blog_h2"><span class="graybg">Istio的实例</span></div>
<p>参考<a href="/istio-study-note#tproxy-iptables-rules">Istio学习笔记</a>。
<div class="blog_h2"><span class="graybg">小结</span></div>
<ol>
<li>需要通过Iptables来对封包进行标记。时用TPROXY目标可以在标记的同时，转发给指定IP:PORT，但却又不改包头</li>
<li>需要为被标记的封包设置路由表，不管其目的地址是什么（0.0.0.0/0），总是发送到lo接口</li>
<li>lo接口接收到的封包，再次进入Iptables，这次不会走PREROUTING链</li>
<li>对于TPROXY，已经明确指定了接收者进程的IP:PORT，直接由此进程处理封包，前提是IP_TRANSPARENT被设置好</li>
<li>SocketMatch的作用是识别本地相关的套接字并标记，它不是必须的，TPROXY也能够做标记工作</li>
</ol>
<div class="blog_h1"><span class="graybg">环回</span></div>
<p>Loopback本来是通信学上的术语，和电信号、数字数据包的路由有关，将它们不做修改的路由给信号的发起者，主要用于测试通信基础设施。</p>
<div class="blog_h2"><span class="graybg">lo</span></div>
<p>在TCP/IP协议族的规范实现中，包含一个虚拟的网络接口，同一机器上的程序可以通过此虚拟接口进行网络通信，此接口完全是软件的，不会引起任何物理网卡的流量。程序发往lo接口的封包会简单的、<span style="background-color: #c0c0c0;">立即发回网络栈，就好像是从网络上收到的一样</span>。</p>
<p>如果路由将<span style="background-color: #c0c0c0;">出口网卡设置为lo（不管目的地址是什么），则报文交由本地的应用程序处理</span>，而不会真正路由出去。</p>
<div class="blog_h2"><span class="graybg">dummy</span></div>
<p>在Linux中，dummy类型的网络接口类似于lo，区别在于，你可以创建多个dummy类型的网络接口。</p>
<p>此外，对于lo接口，内核会自动添加目的地址是CIDR的路由：</p>
<pre class="crayon-plain-tag">local 127.0.0.0/8 dev lo proto kernel scope host src 127.0.0.1</pre>
<p> 而对于dummy接口则仅仅会添加针对其IP地址的路由。</p>
<div class="blog_h1"><span class="graybg">UDS</span></div>
<div class="blog_h2"><span class="graybg">简介</span></div>
<p>Socket API原本是为网络通讯设计的，但后来在socket的框架上发展出一种IPC机制，就是UNIX Domain Socket。虽然网络socket也可用于同一台主机的进程间通讯（通过loopback地址127.0.0.1），但是<span style="background-color: #c0c0c0;">UNIX Domain Socket用于IPC更有效率：不需要经过网络协议栈，不需要打包拆包、计算校验和、维护序号和应答等</span>，只是将应用层数据<span style="background-color: #c0c0c0;">从一个进程拷贝到另一个进程</span>。这是因为，<span style="background-color: #c0c0c0;">IPC机制本质上是可靠的通讯</span>，而网络协议是为不可靠的通讯设计的。UNIX Domain Socket也提供<span style="background-color: #c0c0c0;">面向流和面向数据包两种API接口，类似于TCP和UDP</span>，但是面向消息的UNIX Domain Socke<span style="background-color: #c0c0c0;">t也是可靠</span>的，消息既不会丢失也不会顺序错乱。</p>
<p>UNIX Domain Socket是全双工的，API接口语义丰富，相比其它IPC机制有明显的优越性，目前已成为<span style="background-color: #c0c0c0;">使用最广泛的IPC机制</span>，比如X Window服务器和GUI程序之间就是通过UNIX Domain Socket通讯的。</p>
<p>使用UNIX Domain Socket的过程和网络socket十分相似，也要<span style="background-color: #c0c0c0;">先调用socket()创建一个socket文件描述符</span>，address family指定为AF_UNIX，type可以选择SOCK_DGRAM或SOCK_STREAM，protocol参数仍然指定为0即可。</p>
<p>UNIX Domain Socket与网络socket编程最明显的不同在于地址格式不同，用结构体sockaddr_un表示：</p>
<pre class="crayon-plain-tag">struct sockaddr_un {
    sa_family_t     sun_family;             /*PF_UNIX或AF_UNIX */
    char    sun_path[UNIX_PATH_MAX];        /* 路径名 */
}; </pre>
<p>网络编程的socket地址是IP地址加端口号，而UNIX Domain Socket的地址是一个socket类型的文件在文件系统中的路径，这个socket文件由bind()调用创建，如果<span style="background-color: #c0c0c0;">调用bind()时该文件已存在，则bind()错误返回</span>。</p>
<div class="blog_h2"><span class="graybg">抽象命名空间</span></div>
<p>所谓抽象套接字命名空间（Abstract Socket Namespace）是Linux的特性，用于创建一个<span style="background-color: #c0c0c0;">不去绑定到文件系统中文件的UDS</span>，从而获得以下好处：</p>
<ol>
<li>不需要担心文件系统中的命名冲突</li>
<li>当完成套接字使用后，不需要关注unlink文件路径名的问题 —— 到套接字关闭后，抽象名称会自动删除</li>
<li>不需要在文件系统中创建对应路径，在chroot环境下可能有用，在没有缺陷操控特定文件系统路径时也有用</li>
</ol>
<p>要创建抽象UDS套接字，需要指定sub_path的第一个字节为\0字符。在netstat lsof等命令的输出中，\0显示为@符号</p>
<div class="blog_h1"><span class="graybg">交换机</span></div>
<p>现代以太网依赖于交换机进行帧的交换。它通常是具有多个端口的硬件盒子。如果目的地址对于交换机未知（它不知道关联到哪个端口），那么交换机会广播帧给所有端口。通过观察端口上的帧流量，交换机就能学习到端口-MAC对应关系，从而不再需要广播流量。交换机在FIB（forwarding information base，也叫forwarding table）中缓存对应关系。</p>
<p>交换机可以相互连接，从而构成更大的以太网。</p>
<div class="blog_h1"><span class="graybg"><a id="route"></a>路由</span></div>
<div class="blog_h2"><span class="graybg">目的地类型</span></div>
<div class="blog_h3"><span class="graybg">本机地址</span></div>
<p>Linux支持loopback设备，用于通过网络栈和主机自身进行通信。Linux能够和任何配置在本地网络接口上的IP进行通信，不管地址是不是在loopback设备上。</p>
<p>所有配置在本机网络接口的地址，即本地地址（Locally Hosted Addresses）。</p>
<div class="blog_h3"><span class="graybg">本地可达地址</span></div>
<p>这类地址来自和本机位于同一网段内的其它主机。这些主机通过交换机相互连接，地址本地可达（Locally Reachable Addresses）</p>
<div class="blog_h3"><span class="graybg">其它地址</span></div>
<p>所有其它地址，都必须依赖路由器（IP Routing Device）作为中介才能到达，并且，此路由器具有本地可达地址。</p>
<div class="blog_h2"><span class="graybg">跳/Hop</span></div>
<p>当封包从一个网络（经过路由器）到达另外一个网络，称为经过了一跳。跳的数量，即封包经过的中介路由器的数量。</p>
<div class="blog_h3"><span class="graybg">下一跳</span></div>
<p>对于一个路由路径，封包要经过的下一个网关（路由器），即下一跳。</p>
<div class="blog_h2"><span class="graybg">路由器</span></div>
<p>任何在两个网络之间接受/转发封包的设备，都是路由器。路由器至少是dual-homed的 —— 每个接口接入到一个网络，网络接口通常是NIC。Linux主机经常扮演路由器的角色，NIC经常是VLAN接口之类的虚拟设备。</p>
<div class="blog_h2"><span class="graybg">默认路由</span></div>
<p>默认路由，即目的地是0/0的路由，是最一般的路由——如果找不到和封包目的地更匹配的路由，则自动使用默认路由。</p>
<p>对于连接到因特网的主机来说，默认路由通常是本地可达的路由器，此路由器也叫默认网关。</p>
<div class="blog_h2"><span class="graybg">路由选择</span></div>
<p>路由选择的基本原则是逐跳（hop-by-hop）进行，<span style="background-color: #c0c0c0;">典型情况下以目的地址为选路的唯一标准</span>。Linux作为路由器时，可能<span style="background-color: #c0c0c0;">基于其它的封包特征进行选路，此所谓策略路由</span>。</p>
<p>基于目的地址选路时，<span style="background-color: #c0c0c0;">首先会查询路由缓存，然后查找主路由表</span>。如果<span style="background-color: #c0c0c0;">最近发送封包到了目的地址，则关于目的地址的路由会存在于缓存（本质上是一个hash表）</span>中。如果最近没有发送封包到目的地，则需要查询路由表，匹配的原则是最长前缀匹配（longest prefix match） —— 也就是尽量选择精确的路由。最长前缀匹配能够针对大规模网络的路由规则，被<span style="background-color: #c0c0c0;">更加精确的路由器（通常更靠近）或主机覆盖</span>。</p>
<div class="blog_h2"><span class="graybg">策略路由</span></div>
<p>从2.2开始，Linux支持基于多路由表（multiple routing tables）和路由策略数据库（routing policy database，RPDB）的策略路由。</p>
<p>策略路由的依据，是封包的某些属性，例如源地址、ToS标记、fwmark（仅在内核中存在的、位于表示封包的数据结构中的字段）、接收封包接口名，等等。</p>
<p>启用策略路由的情况下，选路算法如下：</p>
<ol>
<li>首先，仍然是查询路由缓存</li>
<li>基于优先级，来遍历RPDB。查找匹配的RPDB</li>
<li>对于每个匹配RPDB条目，使用最长前缀原则匹配封包目的地址，遍历条目对应的路由表</li>
<li>如果找到匹配路由，则终止迭代</li>
</ol>
<p>伪代码：</p>
<pre class="crayon-plain-tag"># 首先查找路由缓存
if packet.routeCacheLookupKey in routeCache :
    route = routeCache[ packet.routeCacheLookupKey ]
else
    # 然后按优先级遍历RPDB，此数据库中是一条条规则
    for rule in rpdb :
        # 如果封包匹配RPDB规则
        if packet.rpdbLookupKey in rule :
            # 则遍历RPDB规则对应的路由表
            routeTable = rule[ lookupTable ]
            # 根据最长前缀原则来匹配路由表中的路由
            if packet.routeLookupKey in routeTable :
                route = route_table[ packet.routeLookup_key ]</pre>
<p><span style="background-color: #c0c0c0;">使用策略路由时，单个路由表的使用逻辑保持不变</span>。</p>
<div class="blog_h2"><span class="graybg">路由缓存</span></div>
<p>路由缓存，又叫转发信息库（forwarding information base，FIB）。它是一个存放了最近使用的路由条目的哈希表，在查询路由表之前，会优先检查FIB。</p>
<p>FIB由内核独立于路由表维护，修改路由表可能不会立即反应到FIB，要避免此延迟，可以调用：</p>
<pre class="crayon-plain-tag">ip route flush cache</pre>
<p>FIB清空后，新的封包，或者<pre class="crayon-plain-tag">ip route get</pre>命令会触发路由表查找、重新填充缓存。 </p>
<p>FIB提供基于多种方式的哈希查找，包括：dst（目的地址）、src（源地址）、tos（服务类型）、fwmark（内核封包标记）、iif（入站网络接口）。</p>
<p>FIB的缓存条目上，存储了以下属性：cwnd、advmss（建议最大段大小）、src（Preferred Local源地址）、mtu、rtt、age、users、used。</p>
<div class="blog_h2"><span class="graybg">路由表</span></div>
<p>Linux 2.2 / 2.4支持多路由表，除了广泛使用的local / main表外，内核最多支持额外的252张路由表。联合多个路由表（主要基于目的地址）和RPDB（主要基于源地址），Linux内核实现了灵活的路由功能。</p>
<p>支持多重路由表的内核，使用0-255之间的整数来引用路由表。最基本的路由表是local（255）、main（252），路由表序号和名字的映射关系定义在<pre class="crayon-plain-tag">/etc/iproute2/rt_tables</pre>中。</p>
<p>下面的命令显示所有路由表的路由：</p>
<pre class="crayon-plain-tag">ip route show table all</pre>
<p>下面的命令显示特定路由表中的路由：</p>
<pre class="crayon-plain-tag">ip route show table main
ip route show table 2005</pre>
<div class="blog_h3"><span class="graybg">local表</span></div>
<p>这是一张<span style="background-color: #c0c0c0;">由内核维护的特殊表</span>，条目可以删除，但是需要注意风险。ip address和ifconfig命令会导致内核修改local表（通常也同时修改main表）。</p>
<p>local表的主要用途有两个：</p>
<ol>
<li>指定广播地址的规格，仅仅对于支持广播寻址的L2有用</li>
<li>用于<span style="background-color: #c0c0c0;">路由到本机IP地址</span></li>
</ol>
<p>local表中可以出现的路由类型包括：local、nat、broadcast。这些路由类型不会出现在其它表，其它路由类型不会出现在local表。</p>
<p>如果某个<span style="background-color: #c0c0c0;">网络接口具有多个IP地址，则每个IP地址都在local表中具有一个路由条目</span>。这是在Linux下为网络接口添加IP地址的正常附加效果。</p>
<div class="blog_h3"><span class="graybg">main表</span></div>
<p>命令route操控的是该表，如果ip route不指定目标表，则默认操作main表。</p>
<div class="blog_h2"><span class="graybg">路由条目</span></div>
<p>路由表可以包含任意多个（除了local表，它由内核维护）条目。</p>
<p>条目的形式为：</p>
<pre class="crayon-plain-tag">ip route { add | del | change | append | replace } ROUTE

ROUTE := NODE_SPEC [ INFO_SPEC ]
  #            路由类型  匹配CIDR           路由表              路由协议           范围
  NODE_SPEC := [ TYPE ] PREFIX [ tos TOS ] [ table TABLE_ID ] [ proto RTPROTO ] [ scope SCOPE ] [ metric METRIC ]
    TYPE := [ unicast | local | broadcast | multicast | throw | unreachable | prohibit | blackhole | nat ]
    TABLE_ID := [ local| main | default | all | NUMBER ]
    RTPROTO := [ kernel | boot | static | NUMBER ]
    SCOPE := [ host | link | global | NUMBER ]

  INFO_SPEC := NH OPTIONS FLAGS [ nexthop NH ] ...
    #                     下一跳路由器地址             出口网卡名称    权重，在multipath路由中反应此路由的相对带宽或质量 
    NH := [ encap ENCAP ] [ via [ FAMILY ] ADDRESS ] [ dev STRING ] [ weight NUMBER ] NHFLAGS
      FAMILY := [ inet | inet6 | ipx | dnet | mpls | bridge | link ]
      NHFLAGS := [ onlink | pervasive ]
    OPTIONS := FLAGS [ mtu NUMBER ] [ advmss NUMBER ] [ as [ to ] ADDRESS ] rtt TIME ] [ rttvar TIME ] 
                     [ reordering NUMBER ] [ window NUMBER ] [ cwnd NUMBER ] [ ssthresh REALM ] [ realms REALM ] 
                     [ rto_min TIME ] [ initcwnd NUMBER ] [ initrwnd NUMBER ] [ features FEATURES ] [ quickack BOOL ] 
                     [ congctl NAME ] [ pref PREF ] [ expires TIME ]</pre>
<div class="blog_h3"><span class="graybg">路由类型</span></div>
<p>路由类型可以分为一下几种：</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 150px; text-align: center;">类型</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>unicast</td>
<td>单播，最常见的路由条目，针对特定单一目的主机：<br />
<pre class="crayon-plain-tag">ip route add unicast 192.168.0.0/24 via 192.168.100.5
ip route add default via 193.7.255.1
ip route add unicast default via 206.59.29.193
ip route add 10.40.0.0/16 via 10.72.75.254</pre></p>
<p>如果ip route命令不指定路由条目类型，则默认是unicast </p>
</td>
</tr>
<tr>
<td>broadcast</td>
<td>用于支持广播地址的链路层设备，仅用在local表中，通常由内核管理：<br />
<pre class="crayon-plain-tag">ip route add table local broadcast 10.10.20.255 dev eth0 proto kernel scope link src 10.10.20.67 </pre>
</td>
</tr>
<tr>
<td>local</td>
<td>每当一个<span style="background-color: #c0c0c0;">IP地址添加到网络接口上时，内核自动在local表添加local类型的路由条目</span>：<br />
<pre class="crayon-plain-tag">ip route add 
  # 添加到local表    路由类型为local
  table local       local 
  # PREFIX，不带 / 为单一地址
  10.96.0.1 
  # 出口设备名称
  dev kube-ipvs0 
  # 内核自动添加的条目
  proto kernel 
  # 主机范围
  scope host 
  # 优先使用的源地址
  src 10.96.0.1</pre></p>
<p> 例如上面这个条目，是K8S的Kube Proxy IPVS模式下为kube-ipvs0配置10.96.0.1地址后，内核自动增加的条目</p>
</td>
</tr>
<tr>
<td>nat</td>
<td>当尝试配置无状态NAT时，内核添加这类条目到local表：<br />
<pre class="crayon-plain-tag">ip route add nat 193.7.255.184 via 172.16.82.184
ip route add nat 10.40.0.0/16 via 172.40.0.0</pre>
</td>
</tr>
<tr>
<td>unreachable</td>
<td>当匹配这类路由条目时，会返回ICMP unreachable消息：<br />
<pre class="crayon-plain-tag">ip route add unreachable 192.168.14.0/26 </pre>
</td>
</tr>
<tr>
<td>prohibit</td>
<td>当匹配这类路由条目时，会返回ICMP prohibited消息</td>
</tr>
<tr>
<td>blackhole</td>
<td>当匹配这类路由条目时，封包被丢弃，不会产生ICMP消息<br />
<pre class="crayon-plain-tag">ip route add blackhole 64.65.64.0/18 </pre>
</td>
</tr>
<tr>
<td>throw</td>
<td>
<p>导致针对当前路由表的查找立即失败，返回RPDB（可能继续匹配后续规则并查找相应的其它路由表）</p>
<pre class="crayon-plain-tag">ip route add throw 10.79.0.0/16</pre>
</td>
</tr>
</tbody>
</table>
<div class="blog_h3"><span class="graybg">路由SCOPE</span></div>
<p>所谓route scope，是一个“指示符”，用来说明到目标网络的“距离”。 
<p>必须是定义在  /etc/iproute2/rt_scopes 中的名字或数字</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 150px; text-align: center;">取值</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>global<br />universe</td>
<td>
<p>此Scope提示目的地远于一跳</p>
<p>对于所有通过网关（gatewayed）的单播路由，这是默认值</p>
</td>
</tr>
<tr>
<td>link</td>
<td>
<p>此Scope提示目的地在本地网络上</p>
<p>对于直接单播路由、广播路由，这是默认值</p>
</td>
</tr>
<tr>
<td>host</td>
<td>
<p>此Scope提示目的地在本机上</p>
<p>对于local类型路由，这是默认值</p>
</td>
</tr>
</tbody>
</table>
<div class="blog_h3"><span class="graybg">路由协议标识符</span></div>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 150px; text-align: center;">取值</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>redirect</td>
<td>因为ICMP重定向而安装此路由条目</td>
</tr>
<tr>
<td>kernel</td>
<td>因为内核执行指定配置而安装此路由条目</td>
</tr>
<tr>
<td>boot</td>
<td>路由条目在启动期间安装，当路由守护进程启动后，会清除所有这类条目</td>
</tr>
<tr>
<td>static</td>
<td>管理员手工添加，用于覆盖动态路由。路由守护进程会遵从此条目，甚至同步给它的peers</td>
</tr>
<tr>
<td>ra</td>
<td>由路由发现协议安装</td>
</tr>
</tbody>
</table>
<div class="blog_h2"><span class="graybg">路由策略数据库</span></div>
<p>RPDB控制内核遍历多张路由表的顺序。RPDB中的每个条目，具有一个优先级，优先级是0-32767之间的数字，越小优先级越高。下面是一个示例的路由策略：</p>
<pre class="crayon-plain-tag">ip rule list
9:      from all fwmark 0x200/0xf00 lookup 2004 
10:     from all fwmark 0xa00/0xf00 lookup 2005 
100:    from all lookup local 
32766:  from all lookup main 
32767:  from all lookup default</pre>
<p>当封包达到后，假设没有路由缓存，内核会首先检查优先级为0的RPDB规则，如果匹配，则使用该规则指向的路由表，如果不匹配，则依次检查低优先级的规则。</p>
<p>RPDB中的规则类型有若干种，和路由类型对应：</p>
<pre class="crayon-plain-tag"># unicast 这是最常见的规则类型，也是默认的规则类型。会导致查找某个路由表
# 如果源地址是192.168.100.17则查找表5
ip rule add unicast from 192.168.100.17 table 5
# 如果源接口是eth7则查找表5
ip rule add unicast iif eth7 table 5
# 如果封包具有标记4则查找4
ip rule add unicast fwmark 4 table 4


# nat 用于正确实现无状态NAT，通常和nat类型的路由条目耦合在一起。这种条目
# 会导致内核重写出站封包的源地址
#  对于源地址是172.16.82.184的封包，将源地址改写为193.7.255.184
ip rule add nat 193.7.255.184 from 172.16.82.184
ip rule add nat 10.40.0.0 from 172.40.0.0/16


# unreachable 如果匹配，则立即应答 ICMP unreachable 给源地址
# 如果是来自eth2的、服务类型0xc0的封包
ip rule add unreachable iif eth2 tos 0xc0
ip rule add unreachable iif wan0 fwmark 5
ip rule add unreachable from 192.168.7.0/25


# prohibit 如果匹配，则立即应答 ICMP prohibit 给源地址
ip rule add prohibit from 209.10.26.51
ip rule add prohibit to 64.65.64.0/18
ip rule add prohibit fwmark 7


# blackhole 如果匹配，封包被静默的丢弃
ip rule add blackhole from 209.10.26.51
ip rule add blackhole from 172.19.40.0/24
ip rule add blackhole to 10.182.17.64/28</pre>
<div class="blog_h2"><span class="graybg">源地址选择</span></div>
<p>对于具有多重IP地址的主机来说，进行相互通信时必须选择正确的源IP地址。</p>
<p>出站封包的源地址选择的原则如下：</p>
<ol>
<li>如果应用程序已经在使用套接字，则源地址已经被选择过</li>
<li>应用程序<span style="background-color: #c0c0c0;">可以显式请求一个源地址，这个地址甚至可以不是本机地址。</span>很多应用程序支持选取源地址，例如：<br />
<pre class="crayon-plain-tag">nc -s $BINDADDR $DEST $PORT
socat - TCP4:$REMOTEHOST:$REMOTEPORT,bind=$BINDADDR </pre>
</li>
<li>内核进行选路操作，如果<span style="background-color: #c0c0c0;">匹配路由存在src参数，则内核会利用该参数作为源地址</span></li>
<li>如果没有src提示，则内核会根据目的地址，找到本机上<span style="background-color: #c0c0c0;">第一个配置了与该地址在同一个网段、或者与该目的地址的路由下一跳地址在同一网段的IP地址的网络接口，并用此网络接口的IP地址</span>作为源地址</li>
</ol>
<div class="blog_h2"><span class="graybg">ICMP和路由</span></div>
<div class="blog_h3"><span class="graybg">PMTU</span></div>
<p>Path MTU即（Path Maximum Transmission Unit），ICMP报文可以用于发现PMTU，它是整个路由路径中，最小的MTU。知道这个值以后，可以避免不必要的IP分片。</p>
<p>路由中任何一跳阻止ICMP报文，都会导致无法识别PMTU。</p>
<div class="blog_h3"><span class="graybg">ICMP重定向</span></div>
<p>ICMP重定向是路由器提示发送者，具有更好路由路径的一种机制。配置静态路由可以防止不期望的ICMP重定向，尽管ICMP重定向不会导致危险，但是在良好管理的网络中是不应当发生的。</p>
<div class="blog_h2"><span class="graybg"><a id="ecmp"></a>ECMP</span></div>
<p>ECMP是一个逐跳的基于流的负载均衡策略，当路由器发现<span style="background-color: #c0c0c0;">同一目的地址出现多个最优路径时，会更新路由表，为此目的地址添加多条规则，对应于多个下一跳</span>。ECMP的路径选择策略有多种方法：</p>
<ol>
<li>哈希，例如根据源IP地址的哈希为流选择路径</li>
<li>轮询，各个流在多条路径之间轮询传输</li>
<li>基于路径权重，根据路径的权重分配流，权重大的路径分配的流数量更多</li>
</ol>
<p>OSPF、ISIS、EIGRP、BGP等多种路由协议均支持ECMP。</p>
<p>ECMP是一种较为简单的负载均衡策略，其在实际使用中面临的问题也不容忽视：</p>
<ol>
<li>可能增加链路的拥塞：ECMP并没有拥塞感知的机制，只是将流分散到不同的路径上转发。对于已经产生拥塞的路径来说，很可能加剧路径的拥塞</li>
<li>非对称网络使用效果不好</li>
</ol>
<div class="blog_h1"><span class="graybg">防火墙</span></div>
<div class="blog_h2"><span class="graybg">iptables</span></div>
<p>参考：<a href="/iptables">重温iptables</a></p>
<div class="blog_h1"><span class="graybg">流量控制</span></div>
<div class="blog_h3"><span class="graybg">tc</span></div>
<p>参考<a href="/tc">基于tc的网络QoS管理</a></p>
<div class="blog_h1"><span class="graybg">Offloading</span></div>
<p>Offloading是Linux内核中一系列用于减轻CPU网络处理负担的技术。</p>
<p>随着技术的进步，网络接口的带宽越来越大。从早期的10Mbps发展到现在的10Gbps+，带宽增加了3个数量级。与此同时，网络中能够传递的单个封包的大小，受限于路径MTU。在简单的局域网环境下，可以使用Jumbo Frame将MTU提升到9KB，但是在复杂的因特网环境下，MTU一直都只能是1500左右。作为后果，带宽打满的情况下CPU单位时间需要处理的封包个数比早期多了3个数量级。现代处理器在处理1Gbps网络接口时，通常能够应对，不需要考虑Offloading。</p>
<p>尽管这些年CPU的性能也有很大的提升，包括频率升高和核心数增加，但是减少需要处理的封包数量对于提升性能仍然是很有价值的。每个封包都需要经过网络栈，处理过程相当复杂。</p>
<div class="blog_h2"><span class="graybg">TSO</span></div>
<p>TCP Segmentation Offload，允许内核发送一个很大的封包，例如64KB，然后由（支持TSO的）网络适配器将封包分割为合适大小的TCP段传输。</p>
<p>TSO能够平均减少发送单个封包的开销达40倍，对于以发送为主的工作负载，例如下载服务器，足以让10Gbps网络全速工作。</p>
<div class="blog_h2"><span class="graybg">GSO</span></div>
<p>Generic Segmentation Offload，更加一般的segmentation offloading机制，可以用于UDP协议。</p>
<p>即使在驱动层模拟GSO，也能提升性能。</p>
<div class="blog_h2"><span class="graybg">LRO</span></div>
<p>在接收端进行Offloading的技术出现较晚，一方面早期的网络流量均是下行为主，另一方面接收端的处理难度要大的多。你无法控制什么时候接收封包，这是由对端控制的。</p>
<p>Large Receive Offload类似于GSO，它允许网络接口将接收到的封包进行合并，让操作系统看到的封包变少。即使在驱动层模拟LRO，也能提升性能。LRO被Linux中10Gbps网卡驱动广泛的支持。LRO仅仅支持TCP/IPv4。</p>
<p>LRO存在缺陷，它只是简单的将看到的封包都合并起来，如果包头中有差异这些差异会丢失。这些信息的丢失会导致问题：</p>
<ol>
<li>如果Linux主机作为路由器运行，那么它任何时候都不应该改变包头中的信息</li>
<li>某些基于卫星网络的连接，依赖于特殊的头才能正常运作</li>
<li>Linux网桥无法工作，导致很多虚拟化场景无法使用LRO</li>
</ol>
<div class="blog_h2"><span class="graybg">GRO</span></div>
<p>Generic Receive Offload，解决了LRO的缺陷。GRO严格限制了封包合并的条件：</p>
<ol>
<li>MAC地址必须相同</li>
<li>仅允许很少一部分的TCP或IP头不同</li>
</ol>
<p>这些限制让合并后的封包能够被无损的重新分段。<span style="color: #000000;">GRO code可以用于重新分段。</span></p>
<p>GRO不限制于TCP协议。</p>
</div><p>The post <a rel="nofollow" href="https://blog.gmem.cc/network-faq">Linux网络知识集锦</a> appeared first on <a rel="nofollow" href="https://blog.gmem.cc">绿色记忆</a>.</p>
]]></content:encoded>
			<wfw:commentRss>https://blog.gmem.cc/network-faq/feed</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
	</channel>
</rss>
