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

<channel>
	<title>绿色记忆 &#187; 系统编程</title>
	<atom:link href="https://blog.gmem.cc/tag/%e7%b3%bb%e7%bb%9f%e7%bc%96%e7%a8%8b/feed" rel="self" type="application/rss+xml" />
	<link>https://blog.gmem.cc</link>
	<description></description>
	<lastBuildDate>Mon, 06 Apr 2026 12:46:48 +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>Go语言系统编程</title>
		<link>https://blog.gmem.cc/go-system-programming</link>
		<comments>https://blog.gmem.cc/go-system-programming#comments</comments>
		<pubDate>Sat, 28 Feb 2015 07:32:32 +0000</pubDate>
		<dc:creator><![CDATA[Alex]]></dc:creator>
				<category><![CDATA[Go]]></category>
		<category><![CDATA[系统编程]]></category>

		<guid isPermaLink="false">https://blog.gmem.cc/?p=25549</guid>
		<description><![CDATA[<p>信号处理 knative knative项目提供了信号处理的封装。 等待信号 [crayon-69d4975f550bd709957484/] &#160;</p>
<p>The post <a rel="nofollow" href="https://blog.gmem.cc/go-system-programming">Go语言系统编程</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">knative</span></div>
<p>knative项目提供了信号处理的封装。</p>
<div class="blog_h3"><span class="graybg">等待信号</span></div>
<pre class="crayon-plain-tag">import "github.com/knative/pkg/signals"

// 创建默认的信号处理器
stopCh := signals.SetupSignalHandler()


// goroutine应该等待此通道可读，然后清理并退出
go func(stopCh &lt;-chan struct{}){
    &lt;-stopCh
}(stopCh)</pre>
<p>&nbsp;</p>
</div><p>The post <a rel="nofollow" href="https://blog.gmem.cc/go-system-programming">Go语言系统编程</a> appeared first on <a rel="nofollow" href="https://blog.gmem.cc">绿色记忆</a>.</p>
]]></content:encoded>
			<wfw:commentRss>https://blog.gmem.cc/go-system-programming/feed</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<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-69d4975f55507439646950/]  根据需要，切换分支。 安装工具 [crayon-69d4975f5550b777617157/] 配置内核 [crayon-69d4975f5550e834641756/] 你也可以手工直接编辑.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-69d4975f55978127678863/] popen函数 Linux提供了类似的API，允许通过编程的方式，利用管道在两个程序之间传递数据。在两个程序之间进行数据传递的最简单方式是使用popen/pclose函数： [crayon-69d4975f5597c819368111/] 下面是一个简单的示例，执行uname命令并获取其输出： [crayon-69d4975f5597f884414934/] pipe调用 在底层，Linux提供了pipe函数，通过该函数可以在两个进程之间传递数据，不需要启动Shell，该函数提供了对读写数据的更多控制： [crayon-69d4975f55981292976978/] 初看，pipe函数没有什么价值，但由于fork调用创建新进程时，默认原先打开的文件描述符仍然保持打开状态，因此，管道可以被父子进程共享，从而用来在其间进行数据传递，下面是pipe在父子进程之间使用的例子： [crayon-69d4975f55984702661360/] 上面的例子中，父子进程运行的是相同的程序，如果两个进程是完全不同的程序呢？通过exec()调用后，只需要将文件描述符传递给新进程就可以继续使用管道了，因为文件描述符本质上只是一个数字而已： [crayon-69d4975f55987589075630/] 关于管道的使用，应当注意： 对于已经关闭写端的管道，对其指向read()调用不会阻塞，而会立即返回0。这与读取无效文件描述符不同，后者会返回-1 如果通过fork()调用使用管道，就会存在两个不同的文件描述符可以用来向管道写数据，一个在父进程中，一个在子进程中。只有在父子文件中均把针对管道的写描述符关闭，管道才认为是关闭的，对其进行read才会立即返回 将管道用作标准输入输出 通过管道连接两个进程，具有更加简洁的方法： [crayon-69d4975f55989132959620/] 命名管道 <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-69d4975f56a9d801166512-i/]，MAJOR表示ABI不再向后兼容，MINOR表示ABI仍然向后兼容，BUGFIX意味着属于内部改变，ABI不发生变化。 当安装libx库到系统时，可以建立符号链接[crayon-69d4975f56aa1409495021-i/] ，当libx出现不兼容升级时，则需要修改前述符号链接，例如[crayon-69d4975f56aa3735580352-i/] 。这样，新的程序总是和最新的libx版本进行链接。 另一方面，这种符号链接用法可以扩展为，将某个soname链接到特定文件，例如[crayon-69d4975f56aa6410575756-i/] ，如果1.3版本有一个BUG在1.3.2中修复，可以修改前述符号链接为[crayon-69d4975f56aa8688614749-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>
	</channel>
</rss>
