同步IO、异步IO、阻塞IO、非阻塞IO之间的联系与区别

POSIX

同步IO、异步IO、阻塞IO、非阻塞IO,这几个词常见于各种各样的与网络相关的文章之中,往往不同上下文中它们的意思是不一样的,以致于我在很长一段时间对此感到困惑,所以想写一篇文章整理一下。

1
POSIX(可移植操作系统接口)把同步IO操作定义为导致进程阻塞直到IO完成的操作,反之则是异步IO

按POSIX的描述似乎把同步和阻塞划等号,异步和非阻塞划等号,但是为什么有的人说同步IO不等于阻塞IO呢?先来说说几种常见的IO模型吧。

IO模型

这里统一使用Linux下的系统调用recv作为例子,它用于从套接字上接收一个消息,因为是一个系统调用,所以调用时会从用户进程空间切换到内核空间运行一段时间再切换回来。默认情况下recv会等到网络数据到达并且复制到用户进程空间或者发生错误时返回,而第4个参数flags可以让它马上返回。

  • 阻塞IO模型

使用recv的默认参数一直等数据直到拷贝到用户空间,这段时间内进程始终阻塞。A同学用杯子装水,打开水龙头装满水然后离开。这一过程就可以看成是使用了阻塞IO模型,因为如果水龙头没有水,他也要等到有水并装满杯子才能离开去做别的事情。很显然,这种IO模型是同步的。

image

  • 非阻塞IO模型

改变flags,让recv不管有没有获取到数据都返回,如果没有数据那么一段时间后再调用recv看看,如此循环。B同学也用杯子装水,打开水龙头后发现没有水,它离开了,过一会他又拿着杯子来看看……在中间离开的这些时间里,B同学离开了装水现场(回到用户进程空间),可以做他自己的事情。这就是非阻塞IO模型。但是它只有是检查无数据的时候是非阻塞的,在数据到达的时候依然要等待复制数据到用户空间(等着水将水杯装满),因此它还是同步IO。

image

  • IO复用模型

这里在调用recv前先调用select或者poll,这2个系统调用都可以在内核准备好数据(网络数据到达内核)时告知用户进程,这个时候再调用recv一定是有数据的。因此这一过程中它是阻塞于select或poll,而没有阻塞于recv,有人将非阻塞IO定义成在读写操作时没有阻塞于系统调用的IO操作(不包括数据从内核复制到用户空间时的阻塞,因为这相对于网络IO来说确实很短暂),如果按这样理解,这种IO模型也能称之为非阻塞IO模型,但是按POSIX来看,它也是同步IO,那么也和楼上一样称之为同步非阻塞IO吧。

这种IO模型比较特别,分个段。因为它能同时监听多个文件描述符(fd)。这个时候C同学来装水,发现有一排水龙头,舍管阿姨告诉他这些水龙头都还没有水,等有水了告诉他。于是等啊等(select调用中),过了一会阿姨告诉他有水了,但不知道是哪个水龙头有水,自己看吧。于是C同学一个个打开,往杯子里装水(recv)。这里再顺便说说鼎鼎大名的epoll(高性能的代名词啊),epoll也属于IO复用模型,主要区别在于舍管阿姨会告诉C同学哪几个水龙头有水了,不需要一个个打开看(当然还有其它区别)。

image

  • 信号驱动IO模型

通过调用sigaction注册信号函数,等内核数据准备好的时候系统中断当前程序,执行信号函数(在这里面调用recv)。D同学让舍管阿姨等有水的时候通知他(注册信号函数),没多久D同学得知有水了,跑去装水。是不是很像异步IO?很遗憾,它还是同步IO(省不了装水的时间啊)。

image

  • 异步IO模型

调用aio_read,让内核等数据准备好,并且复制到用户进程空间后执行事先指定好的函数。E同学让舍管阿姨将杯子装满水后通知他。整个过程E同学都可以做别的事情(没有recv),这才是真正的异步IO。

image

总结

IO分两阶段:

1
2
1.数据准备阶段
2.内核空间复制回用户进程缓冲区阶段

一般来讲:阻塞IO模型、非阻塞IO模型、IO复用模型(select/poll/epoll)、信号驱动IO模型都属于同步IO,因为阶段2是阻塞的(尽管时间很短)。只有异步IO模型是符合POSIX异步IO操作含义的,不管在阶段1还是阶段2都可以干别的事。

  • ps:以上图片均截自UNIX网络编程卷1。

进程、线程和协程

概念

  • 进程是一个具有一定独立功能的程序关于某次数据集合的一次运行活动,它是操作系统分配资源的基本单元。它可以申请和拥有系统资源,是一个动态的概念,是一个活动的实体。它不只是程序的代码,还包括当前的活动,通过程序计数器的值和处理寄存器的内容来表示。进程的概念主要有两点:第一,进程是一个实体。每一个进程都有它自己的地址空间,一般情况下,包括文本段、数据段和堆栈段。文本段存储处理器执行的代码;数据段储变量和进程执行期间使用的动态分配的内存;堆栈段存储着活动过程调用的指令和本地变量。第二,进程是一个“执行中的程序”。程序是一个没有生命的实体,只有处理器赋予程序生命时(操作系统执行之),它才能成为一个活动的实体,我们称其为进程。

  • 线程是进程的一个执行流,是CPU调度和分派的基本单位,它是比进程更小的能独立运行的基本单位。一个标准的线程由线程ID,当前指令指针(PC),寄存器集合和堆栈组 成。另外,线程是进程中的一个实体,是被系统独立调度和分派的基本单位,线程自己不拥有系统资源,只拥有一点儿在运行中必不可少的资源,每一个程序都至少有一个线程,若程序只有一个线程,那就是程序本身。

  • 协程是一种编译器级实现的用户态的轻量级线程,本质上讲只是线程的一种实现。相对于线程,协程也相对独立,有自己的上下文,但是其切换由程序自己控制。

image

以上是抽象概念,它们的特性也很大程度取决于具体实现(操作系统或者编译器)。比如LINUX的线程实现就和WINDOWS有很大不同,LINUX的线程其实是轻量级进程(Light Weight Process,LWP),GO的协程也和C#的协程不同。

如果把进程比作工厂里的一个车间,车间里有各种的原材料,设备,和工作人员等等。那么线程就是车间里的生产线,它们负责整个车间的生产工作,共享车间内的其它资源,一个车间最少有一个生产线,也可以有多个。协程就好像是生产线里更小的生产单位,能够被一些阀门控制。

从概念上看,协程其实是线程的一种实现,所以可以先简单的认为协程就是线程的一种。而多进程和多线程程序却有着比较大的区别。

进程的创建

在WINDOWS和LINUX下的进程创建不太一样。

  • LINUX的fork()是函数通过系统调用创建一个与原来进程几乎完全相同的进程,exec()则是用来启动另外的进程以取代当前运行的进程,一个进程一旦调用exec类函数,它本身就”死亡”了,系统把代码段替换成新的程序的代码,废弃原有的数据段和堆栈段,并为新程序分配新的数据段与堆栈段,唯一留下的,就是进程号。

  • WINDOWS则是用CreateProccess()来创建一个新的进程,这个新进程运行指定的可执行文件,看上去效果和LINUX的fork()+exec()差不多,只是实现上不太相同。

线程的模型

  • 多对一模型

将多个用户级线程映射到一个内核级线程,线程管理在用户空间完成,这种模型下操作系统并不知道多线程的存在。如的GO(1.5以前)就是这种模型。
优点:线程管理是在用户空间进行的,切换上下文开销比较小,性能较高。
缺点:当一个线程在使用内核服务时被阻塞,那么整个进程都会被阻塞;多个线程不能并行地运行在多处理机上。

  • 一对一模型

将每个用户级线程映射到一个内核级线程。Java的线程就属于这种模型。
优点:当一个线程被阻塞后,允许另一个线程继续执行,所以并发能力较强;能很好的利用到CPU的多核心。
缺点:每创建一个用户级线程都需要创建一个内核级线程与其对应,这样创建线程的开销比较大,会影响到应用程序的性能。并且切换线程要进出内核,代价比较大。

  • 多对多模型

将n个用户级线程映射到m个内核级线程上,要求 m <= n。GO(1.5之后)的协程就属于这种线程模型。
特点:既克服了多对一模型的并发度不高的缺点,又克服了一对一模型的一个用户进程占用太多内核级线程,开销太大的缺点。又拥有多对一模型和一对一模型各自的优点。

进程和线程的区别

  • 进程间是完全独立的个体,多进程环境中,任何一个进程终止不会影响其他进程,而多线程环境中任何一个线程执行exit系统调用,则所有线程退出,最常见的是因某个线程异常导致程序的退出。

  • 通信方式,进程间通信(IPC:无名管道,有名管道,消息队列,信号,信号量,共享内存等)较为复杂,线程间可以直接读写进程数据段(如全局变量)来进行通信(需要线程同步和互斥手段的辅助,以保证数据的一致性)。

  • 线程比进程轻,不管是创建还是上下文切换,线程的开销都要比进程小。

C语言编写守护进程

概念

守护进程(daemon)是一种运行在后台的一种特殊的进程,它独立于控制终端并且周期性的执行某种任务或等待处理某些发生的事件。由于在Linux中,每个系统与用户进行交流的界面成为终端,每一个从此终端开始运行的进程都会依附于这个终端,这个终端被称为这些进程的控制终端,当控制终端被关闭的时候,相应的进程都会自动关闭。但是守护进程却能突破这种限制,它脱离于终端并且在后台运行,并且它脱离终端的目的是为了避免进程在运行的过程中的信息在任何终端中显示并且进程也不会被任何终端所产生的终端信息所打断。它从被执行的时候开始运转,知道整个系统关闭才退出(当然可以认为的杀死相应的守护进程)。如果想让某个进程不因为用户或中断或其他变化而影响,那么就必须把这个进程变成一个守护进程。

实现步骤

  1. 创建子进程,父进程退出(使子进程成为孤儿进程):这是编写守护进程的第一步,由于守护进程是脱离终端的,因此完成第一步后就会在shell终端里造成一个程序已经运行完毕的假象。之后的所有工作在子进程中完成,而用户在shell终端里则可以执行其他命令,从而在形式上做到了与控制终端脱离。实现的语句如下:
1
2
3
4
// 是父进程就结束,然后子进程继续执行
if(pid = fork()) {
exit(0);
}
  1. 在子进程中创建新的会话(脱离控制终端):这步是创建守护进程中最重要的一步,虽然实现起来很简单,但是它的意义非常重要,在这里使用的是系统函数setsid()来创建一个新的会话,并且担任该会话组的组长。

    • 进程组(process group):一个或多个进程的集合,每一个进程组有唯一一个进程组ID,即进程组长进程的ID。

    • 会话期(session):一个或多个进程组的集合,有唯一一个会话期首进程(session leader)。会话期ID为首进程的ID。会话期可以有一个单独的控制终端(controlling terminal)。与控制终端连接的会话期首进程叫做控制进程(controlling process)。当前与终端交互的进程称为前台进程组。其余进程组称为后台进程组。

    • 挂断信号(SIGHUP):默认的动作是终止程序。当终端接口检测到网络连接断开,将挂断信号发送给控制进程(会话期首进程)。如果会话期首进程终止,则该信号发送到该会话期前台进程组。一个进程退出导致一个孤儿进程组中产生时,如果任意一个孤儿进程组进程处于STOP状态,发送SIGHUP和SIGCONT信号到该进程组中所有进程。因此当网络断开或终端窗口关闭后,控制进程收到SIGHUP信号退出,会导致该会话期内其他进程退出。

image

  1. 改变当前目录为根目录:使用fork()创建的子进程是继承了父进程的当前工作目录,由于在进程运行中,当前目录所在的文件系统是不能卸载的,这对以后使用会造成很多的麻烦。因此通常的做法是让“/”作为守护进程的当前目录,当然也可以指定其他的别的目录来作为守护进程的工作目录。

  2. 重设文件权限掩码:文件权限掩码是屏蔽掉文件权限中的对应位。由于使用fork()函数新创建的子进程继承了父进程的文件权限掩码,这就给该子进程使用文件带了很多的麻烦(比如父进程中的文件没有执行文件的权限,然而在子进程中希望执行相应的文件这个时候就会出问题)。因此在子进程中要把文件的权限掩码设置成为0,即在此时有最大的权限,这样可以大大增强该守护进程的灵活性。设置的方法是:umask(0)。

  3. 关闭文件描述符:同文件权限码一样,用fork()函数新建的子进程会从父进程那里继承一些已经打开了的文件。这些文件被打开的文件可能永远不会被守护进程读写,如果不进行关闭的话将会浪费系统的资源,造成进程所在的文件系统无法卸下以及引起预料的错误。

  4. 守护进程的退出:
    上面建立了守护进程,当用户需要外部停止守护进程运行时,往往需要使用kill命令来停止该守护进程,所以守护进程中需要编码来实现kill发出的signal信号处理,达到进程的正常退出。实现该过程的函数是signal函数:

1
2
3
4
5
// 将一个给定的函数和一个特定的信号联系起来,即在收到特定的信号的时候执行相应的
signal(SIGTERM, sigterm_handler);
void sigterm_handler(int arg) {
// 进行相应处理的函数
}。

例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
#include<stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/wait.h>
#define MAXFILE 65535
int main()
{
pid_t pc;
int i,fd,len;
char *buf="this is a dameon \n";
len = strlen(buf);
// 创建一个进程用来做守护进程
// 有看到很多开源服务有fork两次,但第2次fork不是必须的。
// fork第二次主要目的是。防止进程再次打开一个控制终端。
// 因为打开一个控制终端的前台条件是该进程必须是会话组长。再fork一次,子进程ID != sid(sid是进程父进程的sid)。所以也无法打开新的控制终端。
pc =fork();
if(pc<0)
{
printf("error fork \n");
exit(1);
} else if(pc>0) {
exit(0); // 结束父进程
}
setsid(); // 使子进程独立1.摆脱原会话控制 2.摆脱原进程组的控制 3.摆脱控制中端的控制
chdir("/"); // 改变当前工作目录,这也是为了摆脱父进程的影响
umask(0); // 重设文件权限掩码
for(i = 0;i < MAXFILE; i++) // 关闭文件描述符(常说的输入,输出,报错3个文件),
// 因为守护进程要失去了对所属的控制终端的联系,这三个文件要关闭
close(i);
while(1)
{
if((fd=open("/tmp/dameon.txt",O_CREAT|O_WRONLY|O_APPEND,0600))<0)
{
printf("open file err \n");
exit(0);
}
write(fd,buf,len+1);
close(fd);
sleep(10);
}
}

GDB基本用法

基本命令

  • 进入GDB:#gdb test

  test是要调试的程序,由gcc test.c -g -o test生成。进入后提示符变为(gdb) 。

  • 查看源码:(gdb) l

  源码会进行行号提示。

  如果需要查看在其他文件中定义的函数,在l后加上函数名即可定位到这个函数的定义及查看附近的其他源码。或者:使用断点或单步运行,到某个函数处使用s进入这个函数。

  • 设置断点:(gdb) b 6

  这样会在运行到源码第6行时停止,可以查看变量的值、堆栈情况等;这个行号是gdb的行号。

  • 查看断点处情况:(gdb) info b

  可以键入”info b”来查看断点处情况,可以设置多个断点;

  • 运行代码:(gdb) r

  • 显示变量值:(gdb) p n

  在程序暂停时,键入”p 变量名”(print)即可;

  GDB在显示变量值时都会在对应值之前加上 N N”,而无需写冗长的变量名;

  • 观察变量:(gdb) watch n

    在某一循环处,往往希望能够观察一个变量的变化情况,这时就可以键入命令”watch”来观察变量的变化情况,GDB在”n”设置了观察点;

  • 单步运行:(gdb) n

  • 程序继续运行:(gdb) c

  使程序继续往下运行,直到再次遇到断点或程序结束;

  • 退出GDB:(gdb) q

断点调试

命令格式 例子 作用
break + 设置断点的行号 break n 在n行处设置断点
tbreak + 行号或函数名 tbreak n/func 设置临时断点,到达后被自动删除
break + filename + 行号 break main.c:10 用于在指定文件对应行设置断点
break + <0x...> break 0x3400a 用于在内存某一位置处暂停
break + 行号 + if + 条件 break 10 if i==3 用于设置条件断点,在循环中使用非常方便
info breakpoints/watchpoints [n] info break n表示断点编号,查看断点/观察点的情况
clear + 要清除的断点行号 clear 10 用于清除对应行的断点,要给出断点的行号,清除时GDB会给出提示
delete + 要清除的断点编号 delete 3 用于清除断点和自动显示的表达式的命令,要给出断点的编号,清除时GDB不会给出任何提示
disable/enable + 断点编号 disable 3 让所设断点暂时失效/使能,如果要让多个编号处的断点失效/使能,可将编号之间用空格隔开
awatch/watch + 变量 awatch/watch i 设置一个观察点,当变量被读出或写入时程序被暂停
rwatch + 变量 rwatch i 设置一个观察点,当变量被读出时,程序被暂停
catch 设置捕捉点来补捉程序运行时的一些事件。如:载入共享库(动态链接库)或是C++的异常
tcatch 只设置一次捕捉点,当程序停住以后,应点被自动删除

数据命令

命令格式 例子 作用
display +表达式 display a 用于显示表达式的值,每当程序运行到断点处都会显示表达式的值
info display 用于显示当前所有要显示值的表达式的情况
delete + display 编号 delete 3 用于删除一个要显示值的表达式,被删除的表达式将不被显示
disable/enable + display 编号 disable/enable 3 使一个要显示值的表达式暂时失效/使能
undisplay + display 编号 undisplay 3 用于结束某个表达式值的显示
whatis + 变量 whatis i 显示某个表达式的数据类型
print(p) + 变量/表达式 p n 用于打印变量或表达式的值
set + 变量 = 变量值 set i = 3 改变程序中某个变量的值
  • 在使用print命令时,可以对变量按指定格式进行输出,其命令格式为print /变量名 + 格式,其中常用的变量格式:x:十六进制;d:十进制;u:无符号数;o:八进制;c:字符格式;f:浮点数。

调试运行环境相关命令

命令格式 例子 作用
set args set args arg1 arg2 设置运行参数
show args show args 参看运行参数
set width + 数目 set width 70 设置GDB的行宽
cd + 工作目录 cd ../ 切换工作目录
run r/run 程序开始执行
step(s) s 进入式(会进入到所调用的子函数中)单步执行,进入函数的前提是,此函数被编译有debug信息
next(n) n 非进入式(不会进入到所调用的子函数中)单步执行
finish finish 一直运行到函数返回并打印函数返回时的堆栈地址和返回值及参数值等信息
until + 行数 u 3 运行到函数某一行
continue(c) c 执行到下一个断点或程序结束
return <返回值> return 5 改变程序流程,直接结束当前函数,并将指定值返回
call + 函数 call func 在当前位置执行所要运行的函数

堆栈相关命令

命令格式 例子 作用
backtrace/bt bt 用来打印栈帧指针
frame frame 1 用于打印指定栈帧
info reg info reg 查看寄存器使用情况
info stack info stack 查看堆栈使用情况
up/down up/down 跳到上一层/下一层函数
1
backtrace/bt 也可以在该命令后加上要打印的栈帧指针的个数,查看程序执行到此时,是经过哪些函数呼叫的程序,程序“调用堆栈”是当前函数之前的所有已调用函数的列表(包括当前函数)。每个函数及其变量都被分配了一个“帧”,最近调用的函数在 0 号帧中(“底部”帧)

跳转执行

  • jump:指定下一条语句的运行点。可以是文件的行号,可以是file:line格式,可以是+num这种偏移量格式。表式着下一条运行语句从哪里开始。相当于改变了PC寄存器内容,堆栈内容并没有改变,跨函数跳转容易发生错误。

信号命令

  • signal:signal SIGXXX,产生XXX信号,如SIGINT。一种速查Linux查询信号的方法:# kill -l

运行Shell命令

  • 如(gdb)shell ls来运行ls

更多程序运行选项和调试

  • 程序运行参数
1
2
set args 可指定运行时参数。(如:set args 10 20 30 40 50)
show args 命令可以查看设置好的运行参数。
  • 运行环境
1
2
3
4
5
6
7
path 可设定程序的运行路径。
show paths 查看程序的运行路径。
set environment varname [=value] 设置环境变量。如:set env USER=hchen
show environment [varname] 查看环境变量。
```
* **工作目录**

cd 相当于shell的cd命令。
pwd 显示当前的所在目录。

1
2
* **程序的输入输出**

info terminal 显示你程序用到的终端的模式。
使用重定向控制程序输出。如:run > outfile
tty命令可以指写输入输出的终端设备。如:tty /dev/ttyb

1
2
* **调试已运行的程序**

两种方法:
(1)在UNIX下用ps查看正在运行的程序的PID(进程ID),然后用gdb PID格式挂接正在运行的程序。
(2)先用gdb 关联上源代码,并进行gdb,在gdb中用attach命令来挂接进程的PID。并用detach来取消挂接的进程。

1
2
* **暂停 / 恢复程**

序运行当进程被gdb停住时,你可以使用info program 来查看程序的是否在运行,进程号,被暂停的原因。 在gdb中,我们可以有以下几种暂停方式:断点(BreakPoint)、观察点(WatchPoint)、捕捉点(CatchPoint)、信号(Signals)、线程停止(Thread Stops),如果要恢复程序运行,可以使用c或是continue命令。

1
2
* **线程(Thread Stops)**

如果程序是多线程,可以定义断点是否在所有的线程上,或是在某个特定的线程。
break thread
break thread if …
linespec指定了断点设置在的源程序的行号。threadno指定了线程的ID,注意,这个ID是GDB分配的,可以通过“info threads”命令来查看正在运行程序中的线程信息。如果不指定thread 则表示断点设在所有线程上面。还可以为某线程指定断点条件。如:
(gdb) break frik.c:13 thread 28 if bartab > lim
当你的程序被GDB停住时,所有的运行线程都会被停住。这方便查看运行程序的总体情况。而在你恢复程序运行时,所有的线程也会被恢复运行。

1
2
3
4
5
6
## 调试core文件
* **Core Dump**:Core的意思是内存,Dump的意思是扔出来,堆出来。开发和使用Unix程序时,有时程序莫名其妙的down了,却没有任何的提示(有时候会提示core dumped),这时候可以查看一下有没有形如core.进程号的文件生成,这个文件便是操作系统把程序down掉时的内存内容扔出来生成的, 它可以做为调试程序的参考
* **生成Core文件**

一般默认情况下,core file的大小被设置为了0,这样系统就不dump出core file了。修改后才能生成core文件。

#设置core大小为无限
ulimit -c unlimited

#设置文件大小为无限
ulimit unlimited

这些需要有root权限, 在ubuntu下每次重新打开中断都需要重新输入上面的第一条命令, 来设置core大小为无限

core文件生成路径:输入可执行文件运行命令的同一路径下。若系统生成的core文件不带其他任何扩展名称,则全部命名为core。新的core文件生成将覆盖原来的core文件。

1)/proc/sys/kernel/core_uses_pid可以控制core文件的文件名中是否添加pid作为扩展。文件内容为1,表示添加pid作为扩展名,生成的core文件格式为core.xxxx;为0则表示生成的core文件同一命名为core。
可通过以下命令修改此文件:
echo “1” > /proc/sys/kernel/core_uses_pid

2)proc/sys/kernel/core_pattern可以控制core文件保存位置和文件名格式。
可通过以下命令修改此文件:
echo “/corefile/core-%e-%p-%t” > core_pattern,可以将core文件统一生成到/corefile目录下,产生的文件名为core-命令名-pid-时间戳
以下是参数列表:
%p - insert pid into filename 添加pid
%u - insert current uid into filename 添加当前uid
%g - insert current gid into filename 添加当前gid
%s - insert signal that caused the coredump into the filename 添加导致产生core的信号
%t - insert UNIX time that the coredump occurred into filename 添加core文件生成时的unix时间
%h - insert hostname where the coredump happened into filename 添加主机名
%e - insert coredumping executable name into filename 添加命令名

1
2
* **用gdb查看core文件**

发生core dump之后, 用gdb进行查看core文件的内容, 以定位文件中引发core dump的行.
gdb [exec file] [core file]
如:
gdb ./test core

或gdb ./a.out
core-file core.xxxx
gdb后, 用bt命令backtrace或where查看程序运行到哪里, 来定位core dump的文件->行.

待调试的可执行文件,在编译的时候需要加-g,core文件才能正常显示出错信息

1)gdb -core=core.xxxx
file ./a.out
bt
2)gdb -c core.xxxx
file ./a.out
bt

1
2
* **用gdb实时观察某进程crash信息**

启动进程
gdb -p PID
c
运行进程至crash
gdb会显示crash信息
bt
```