本篇文章为大家展示了如何理解Linux故障定位技术,内容简明扼要并且容易理解,绝对能使你眼前一亮,通过这篇文章的详细介绍希望你能有所收获。
主要是来了解并学习linux中故障定位技术的学习,故障定位技术分为在线故障定位和离线故障定位。
故障定位(Debugging)场景分类
为便于描述问题,将Linux上各种软件故障定位的情形分成两类
(1)在线故障故障定位
在线故障定位(online-debugging)就是在故障发生时, 故障所处的操作系统环境仍然可以访问,故障处理人员可通过console, ssh等方式登录到操作系统上,在shell上执行各种操作命令或测试程序的方式对故障环境进行观察,分析,测试,以定位出故障发生的原因
(2)离线故障定位
离线故障定位(offline-debugging)就是在故障发生时,故障所处的操作系统环境已经无法正常访问,但故障发生时系统的全部或部分状态已经被系统本身所固有或事先设定的方式收集起来,故障处理人员可通过对收集到的故障定位状态信息进行分析,定位出故障发生的原因
2、应用进程故障情形及处理
应用进程的故障一般不会影响操作系统运行环境的正常使用(如果应用代码的bug导致了内核的crash或hang,则属于内核存在漏洞),所以可采用在线故障定位的方法,灵活的进行分析. 应用代码故障的情形有如下几种:
(1)进程异常终止
很多用户认为进程异常终止情况无从分析,但实际上进程异常终止情况都是有迹可寻的. 所有的进程异常终止行为,都是通过内核发信号给特定进程或进程组实现的. 可分成几个类型进行描述:
- SIGKILL. SIGKILL最特殊,因为该信号不可被捕获,同时SIGKILL不会导致被终止的进程产生core文件, 但如果真正的是由内核中发出的SIGKILL,则内核一定会在dmesg中记录下信息. 另外在内核中使用SIGKILL的地方***,如oom_kill_process()中, 所以通过dmesg记录并且分析内核中使用SIGKILL的代码,并不难分析原因
- SIGQUIT, SIGILL, SIGABRT, SIGBUS, SIGFPE, SIGSEGV. 这几个信号在保留情况下会终止进程并会产生core文件, 用户根据core中的stack trace信息,能直接定位出导致终止信号的代码位置. 另外, SIGQUIT,SIGABRT一般是由用户代码自己使用的,好的代码一般会记录日志. SIGILL, SIGBUS, SIGFPE, SIGSEGV, 都是由内核中产生的,搜索内核源码,不难列出内核中使用这几个信号的地方, 如SIGILL 是非法指令,可能是浮点运算产生的代码被corrupted或文本区域的物理内存corruption; SIGBUS多由MCE故障定位导致; SIGSEGV多由应用代码的指针变量被corrupted导致. 对于应用的heap或stack的内存被corrupted, 可用valgrind工具对应用进行profile, 通常能直接发现导致corruption的代码
- SIGINT, SIGPIPE, SIGALRM, SIGTERM. 这几个信号在保留情况下终止进程但不会产生core文件. 对这几个信号,建议用户一定要定义一个handler,以记录产生问题的上下文. 比较容易忽略的是SIGPIPE, 很多用户程序在使用select()或poll()时只监听read/write描述符,不监听exception描述符,在对方TCP已经关闭的情况下,仍然向socket中写入,导致SIGPIPE.
- 对于恶意的代吗产生的进程终止行为,如合作的一些进程中,A向B发SIGKILL, 而没做日志记录,或者B直接判断某条件而调用exit(), 也没有做日志记录.在应用代码量很大的情况下,通过分析代码故障定位这种情形也许很难. SystemTap提供了解决这个问题的一个比较好的方法,就是写用户层的probes, 追踪进程对signal(), exit() 等系统调用的使用
(2)进程阻塞,应用无法正常推进
这种情况,对于单个被阻塞的进程而言,属于正常状态, 但对于包含多个进程的应用整体而言,属于异常. 应用无法推进,说明其中某一个进程推进的因素出现了问题,导致其他依赖于它的进程也要等待. 分析这种情形需要分析清楚进程或事件之间的依赖关系,及数据的处理流. 首先要用gdb -p 的back trace功能查出各进程阻塞的执行路径, 以确定每个进程所处在的状态机的位置.
通常而言,如果只考虑各个进程的状态,则进程之间可能形成了一种互相依赖的环形关系,如(P1发请求=>P2处理=>P2发反应=>P1再请求=>P2处理=>P2再发反应), 但应用对workload, 一般是按一个个的transaction 或 session的方式进行处理的,每个transaction都有起点和终点, 我们需要用strace, tcpdump 等工具以及应用的执行日志进行观察,分析出当前正被处理的transaction所被阻滞的位置,从而找出全部状态机被阻塞的原因. 导致这种状态机停止运转的原因有多个:如和应用通信的远端出现了问题,后端数据库/目录等出现了问题,应用的某个进程或线程处于非正常的blocking位置或直接终止,不再正常工作.
(3)用户进程形成死锁
用户进程形成死锁,如果没有内存上的故障定位,则完全是应用自身的逻辑问题. 死锁的进程或线程之间由于锁的互相占有形成了环路。 这种情况发生时,用gdb -p 的back trace的功能能直接确定死锁的进程全部阻塞在futex()等和锁相关的系统调用上, 这些调用futex()的路径可能是mutex, semaphore, conditional variable 等锁函数. 通过分析call trace 的代码,能直接确定各进程在执行到该位置时,可能已经持有的全部锁, 根据这个修改程序的代码,消除死锁环路,就可解决问题.
注意,内存故障也可导致假的死锁的,如物理内存故障可直接导致锁变量的值为-1, 所以使用该锁的进程都会阻塞. 如果是代码的bug导致的内存corruption,可用valgrind工具检查程序来发现. 但如果是物理内存的故障定位导致的corruption, 则需要硬件的支持,对于高端的PC, 如MCE功能的机器,当物理内存故障定位时能直接产生异常或报告, 但对于低端PC服务器,除了运行memtest工具进行检测外,没有其他方法
(4)进程长期处于 'D' (UnInterruptible)状态没法退出
这种多是由内核中的故障引起的. 内核在很多执行路径中会将进程至于'D'的状态,以确保关键的执行路径不被外部的信号中断, 导致不必要的内核中数据结构状态的不一致性. 但一般而言,进程处于 'D' 状态的时间不会太久, 因为状态结束的条件(如timer触发,
IO操作完成等)很快会将进程唤醒. 当进程长期处于 'D',关键是要找出其阻塞的代码位置, 用 sysrq 的t键功能可直接打印出系统中全部睡眠进程的内核执行堆栈,如 echo 't' > /proc/sysrq-trigger, 其中包括出现 'D'状态的进程的内核态堆栈. 找出代码位置后,一般可直接分析出 'D' 状态不能退出的原因, 如IO read操作因硬件或nfs故障而不能完成.
有可能导致 'D' 状态的原因比较复杂,如‘D’的退出依赖于某变量的值,而该变量的值因某种原因被***corrupted掉了.
3、内核故障情形及处理
(1)内核panic
panic是内核最直接的故障定位报告,发生panic时,内核已经认为故障定位已经导致操作系统不再具备正常运行的条件了. 当发生panic时,Linux会将所有CPU的中断和进程调度功能都关掉,所以这时系统是没有任何反应的,如果用户启动的是图形界面,则在屏幕上也看不到任何关于panic的信息.
我们通常遇到的,机器没反应,ping不通的情况,绝大部分都是panic. Panic发生时,内核直接在console上打印导致panic的代码位置的调用堆栈, 传统的用户用串口连接到机器上来收集console上的打印信息, 但串口这种方式,显然用起来不方便, 现在的Linux, 如RHEL5,RHEL6, 都采用kdump的方法来收集panic时的信息. 在配置好kdump的情况下,panic时系统会用kexec加载并切换到一个新的内核上(放置在预先分配的内存位置),并用磁盘或网络等将系统的全部或部分内存数据保存起来.
用kdump收集到panic的数据后,用户用crash工具就能直接查看导致panic的代码路径.
panic一般是很直观的,panic的堆栈信息能直接反映出导致bug的原因,如MCE故障,NMI故障, 数据结构分配失败等. 但有时panic是因为内核主动发现了关键的数据结构不一致性,这种不一致性是什么时候,什么代码导致的,并不清楚,可能还需要多次测试用SystemTap这样的工具进行捕捉
(2)多处理机环境内核执行路径产生的死锁
内核死锁和panic不一样,产生死锁时,内核并不主动的使自己处于挂起状态. 但内核死锁发生时,两个以上的CPU的执行路径在内核态不能推进了,处于互相阻塞状态, 而且是100%的占用CPU(用的spin-lock),直接或间接的导致全部CPU上的进程无法调度. 内核死锁又分两种情况:
- 涉及到中断上下文的死锁. 这种情况的死锁,最少一个CPU上的中断被屏蔽了.系统可能没法响应ping请求. 由于有一个CPU已经没法响应中断,其上的local APIC定时中断没法工作,可以用NMI Watchdog的方法来检测出来(检查local APIC handler维护的计数器变量),NMI Watchdog可以在其处理程序中调用panic(), 用户就可以用kdump收集内存信息,从而分析各死锁CPU上的调用堆栈,查处导致死锁的逻辑原因.
- 不涉及中断上下文的死锁. 这种情况的死锁,各CPU上的中断都是正常的,系统能对ping请求作出反应,这时NMI Watchdog无法被触发. 在 2.6.16之前的内核中,并没有一种很好的方法来处理这种情形. 在RHEL5, RHEL6 内核中, 每个CPU上提供了一个watchdog内核线程,在死锁出现的情况下,死锁CPU上的watchdog内核线程没法被调度(即使它是***优先级的实时进程),它就没法update相应的counter变量,各CPU的NMI Watchdog中断会周期性的检查其CPU对应的counter, 发现没有updated, 会调用panic(),用户就可用kdump收集内存信息,分析各死锁CPU上的调用堆栈,查处导致死锁的逻辑原因.
(3)内核的oops或warning
oops 和warning和panic类似的地方是,他们都是因内核发现了不一致而主动报告的异常. 但oops和warning导致的问题严重程度要比panic轻很多,以致于内核处理该问题时不需要使系统挂起. 产生oops和warning, 内核通常已经在dmesg中记录了相当的信息,特别是oops, 至少会打印出现故障的地方的call trace. Oops也可转换成panic/kdump来进行offline-debugging, 只要将/proc/sys/kernel下的panic_on_oops变量设置为1就行了.
产生oops和warning的直接原因有很多,如内核中的segment fault, 或内核发现的某数据结构的counter值不对, 而segment fault 和counter值的变化还有更深层次的原因,通常并不能从内核dmesg的信息中看出来,解决这种问题的是要用SystemTap进行probe, 如发现某counter的值不对,就用SystemTap做一个probe来记录所有代码对该counter的访问, 然后再进行分析.
定位oops和warning会比定位应用程序的内存访问故障定位困难很多,因为在内核并不能象用valgrind去trace应用程序一样跟踪数据结构的分配和使用情况.
其他(硬件相关)故障
机器自动重启是一种常见的故障情形,一般是由硬件如物理内存故障引起的,软件的故障只会导致死锁或panic, 内核中几乎没有代码在发现问题的情况下去reboot机器. 在/proc/sys/kernel目录下有个参数“panic”, 其值如果设置为非0,则在panic发生若干秒后,内核会重启机器. 现在高端的PC服务器,都在努力用软件来处理物理内存故障,如MCA的 “HWPoison”方法会将故障的物理页隔离起来,Kill掉故障页所在的进程就可以了,RHEL6现在已经支持 “HWPoison”. 那些不具备MCA能力的机器,物理内存故障时,不会产生MCE异常,直接由硬件机制reboot机器
4、RHEL6 上的Debugging技术介绍
(1)Kdump故障定位收集和crash分析
kdump就是用来在内核panic的情况下收集系统内存信息的, 用户也可在online情况下用sysrq的'c'键触发. Kdump 采用没有污染的内核来执行dump工作,所以其比以前的diskdump, lkcd方法更可靠. 使用kdump,用户可选择将数据dump到本地盘或网络上,也可通过定义makedumpfile的参数过滤要收集的内存信息,已减少kdump所需要的停机时间
Crash就是对kdump的信息进行分析的工具.其实际就是gdb的一个wrapper. 使用crash时,***安装kernel-debuginfo包,这样能解析kdump收集的内核数据的符号信息. 用crash来定位问题的能力,完全取决于用户对内核代码的理解和分析能力
参考 “#>man kdump.conf”, “#>man crash”, “#>man makedumpfile”学习怎样使用kdump和crash. 访问 http://ftp.redhat.com可下载debuginfo文件
(2)用systemTap定位bug
systemtap 属于probe类的定位工具,它能对内核或用户代码的指定位置进行probe, 当执行到指定位置或访问指定位置的数据时,用户定义的probe函数自动执行,可打印出该位置的调用堆栈,参数值,变量值等信息. systemtap选择进行probe的位置很灵活,这是systemtap的强大功能所在. Systemtap的probe点可包括如下几个方面:
- 内核中全部系统调用,内核及模块中全部函数的入口或出口点
- 自定义的定时器probe点
- 内核中任意指定的代码或数据访问位置
- 特定用户进程中任意制定的代码或数据访问位置
- 各个功能子系统预先设置的若干probe点,如tcp,udp,nfs,signal各子系统都预先设置了很多探测点
systemTap的脚本用stap脚本语言来编写,脚本代码中调用stap提供的API进行统计,打印数据等工作,关于stap语言提供的API函数,参考 “#> man stapfuncs”. 关于systemTap的功能和使用可参考 “#> man stap”, “#> man stapprobes”
(3)ftrace
ftrace 是linux内核中利用tracepoints基础设施实现的事件追踪机制,它的作用在于能比较清楚的给出在一定时间内系统或进程所执行的活动,如函数调用路径,进程切换流等. Ftrace可用于观察系统各部分的latency,以便进行实时应用的优化; 它也可以通过记录一段时间内的内核活动来帮助故障定位. 如用以下方法可trace某个进程在一端时间的函数调用情况
#> echo “function” > /sys/kernel/debug/tracing/current_tracer #> echo “xxx” > /sys/kernel/debug/tracing/set_ftrace_pid #> echo 1 > /sys/kernel/debug/tracing/tracing_enabled
除tracing函数调用外,ftrace还可tracing系统的进程切换,唤醒,块设备访问,内核数据结构分配等活动. 注意,tracing和profile是不同的,tracing记录的是一段时间内的全部活动,而不是统计信息,用户可以通过/sys/kernel/debug/tracing下的buffer_size_kb设置缓冲区的大小, 以记录更长时间的数据.
关于ftrace的具体使用可参考内核源码Documenation/trace下的内容
(4)oprofile 和 perf
oprofile和perf都是对系统进行profile(抽样,统计)的工具,它们主要用来解决系统和应用的性能问题. perf功能更强大,更全面,同时perf的用户空间工具和内核源码一起维护和发布,让用户能及时的享受perf内核新增加的特征. Perf 是在RHEL6中才有,RHEL5中没有Perf. Oprofile和perf 都使用现代CPU中具有的硬件计数器进行统计工作,但perf还可以使用内核中定义的 “software counter”及 “trace points”, 所以能做更多的工作. Oprofile的抽样工作利用 CPU的NMI中断来进行,而perf既可以利用NMI中断也可利用硬件计数器提供的周期性中断. 用户能很容易用perf来oprofile一个进程或系统的执行时间分布,如
#> perf top -f 1000 -p
还可以利用系统定义的 “software counter”和各子系统的 “trace points” 对子系统进行分析, 如
#>perf stat -a -e kmem:mm_page_alloc -e kmem:mm_page_free_direct -e kmem:mm_pagevec_free sleep 6
能统计6秒内kmem子系统的活动 (这一点实际是利用ftrace提供的tracepoints来实现)
我认为有了perf, 用户就没必要使用oprofile了
5、用kdump工具内核故障定位实例
A) 部署Kdump
部署 kdump 收集故障信息的步骤如下:
(1)设置好相关的内核启动参数
在 /boot/grub/menu.lst 中加入如下内容
crashkernel=128M@16M nmi_watchdog=1
其中crashkernel参数是用来为kdump的内核预留内存的; nmi_watchdog=1 是用来激活NMI中断的, 我们在未确定故障是否关闭了中断的情况下, 需要部署NMI watchdog才能确保触发panic. 重启系统确保设置生效
(2)设置好相关的sysctl内核参数
在/etc/sysctl.conf 中***加入一行
kernel.softlookup_panic = 1
该设置确保softlock发生时会调用panic, 从而触发kdump行为执行 #>sysctl -p 确保设置生效
(3)配置 /etc/kdump.conf
在 /etc/kdump.conf 中加入如下几行内容
ext3 /dev/sdb1 core-collector makedumpfile -c –message-level 7 -d 31 -i /mnt/vmcoreinfo path /var/crash default reboot
其中 /dev/sdb1 是用于放置dumpfile 的文件系统, dumpfile 文件放置在/var/crash下, 要事先在/dev/sdb1分区下创建/var/crash 目录. “-d 31”指定对dump内容的过滤级别,这参数对于dump分区放不下全部内存内容或用户不想让dumping中断业务太长时间时很重要. vmcoreinfo 文件放置在 /dev/sdb1 分区的 / 目录下, 需要使用如下命令产生:
#>makedumpfile -g //vmcoreinfo -x /usr/lib/debug/lib/modules/2.6.18-128.el5.x86_64/vmlinux
“vmlinux” 文件是由kernel-debuginfo 包提供的,在运行makedumpfile 之前需要安装相应内核的 kernel-debuginfo 和 kernel-debuginfo-common 两个包,该两个包需从 http://ftp.redhat.com 下载. “default reboot” 用来告诉kdump, 收集完dump信息后重启系统
(4)激活kdump
运行 #>service kdump start 命令,你会看到,在成功完成的情况下会在/boot/目录下生成一个initrd-2.6.18-128.el5.x86_64kdump.img 文件,该文件就是kdump加载的内核的 initrd文件,收集dump信息的工作就是在该initrd的启动环境下进行的. 查看/etc/init.d/kdump脚本的代码,你可看到其中会调用mkdumprd命令创建用于dump的initrd文件
测试Kdump部署的有效性
为了测试kdump部署的有效性,本人写了如下一个内核模块,通过insmod 加载该内核模块, 就能产生一个内核线程,在10秒左右后,占据100%的CPU,在20秒左右后触发kdump. 系统重启后,检查/oracle分区/var/crash 目录下的内容,就能确认vmcore文件是否生成.
Zqfthread.c #include #include #include #include #include #include MODULE_AUTHOR("frzhang@redhat.com"); MODULE_DESCRIPTION("A module to test ...."); MODULE_LICENSE("GPL"); static struct task_struct *zqf_thread; static int zqfd_thread(void *data); static int zqfd_thread(void *data) { int i=0; while (!kthread_should_stop()) { i++; if ( i < 10 ) { msleep_interruptible(1000); printk("%d seconds\n", i); } if ( i == 1000 ) // Running in the kernel i = 11 ; } return 0; } static int __init zqfinit(void) { struct task_struct *p; p = kthread_create(zqfd_thread, NULL,"%s","zqfd"); if ( p ) { zqf_thread = p; wake_up_process(zqf_thread); // actually start it up return(0); } return(-1); } static void __exit zqffini(void) { kthread_stop(zqf_thread); } module_init(zqfinit); module_exit(zqffini) Makefile obj-m += zqfthread.o Making #> make -C /usr/src/kernels/2.6.32-71.el6.x86_64/ M=`pwd` modules
用crash 工具分析vmcore 文件
用crash 命令分析vmcore 的命令行格式如下所示. 用crash打开vmcore后,主要是用dmesg及 bt 命令打印出问题的执行路径的call trace, 用dis 反汇编出代码,最终确认call trace对应的C源码中的位置,再进行逻辑分析.
#>crash /usr/lib/debug/lib/modules/2.6.18-128.el5.x86_64/vmlinux /boot/System.map-2.6.18-128.el5.x86_64 ./vmcore
6、使用kprobe来观察内核函数的执行实例
kprobe是SystemTap对内核函数进行probing的功能在内核中的实现,由于内核中提供了正式的API来使用kprobe,所以对很多内核程序员来说,也许直接使用kprobe比使用SystemTap更方便. 内核中提供了三种类型的kprobe处理函数,分别是jprobe, kprobe, kretprobe, 下面的代码用这三个probe观察在TCP/IP的arp_process函数执行中对ip_route_input()调用的返回结果.这个代码还展示了在同一个函数probe的Entry handler和Ret handler之间共享参数的方法. 代码如下:
arp_probe.c #include #include #include #include #include #include #include #include MODULE_AUTHOR("frzhang@redhat.com"); MODULE_DESCRIPTION("A module to track the call results of ip_route_input() inside arp_process using jprobe and kretprobe"); MODULE_LICENSE("GPL"); static int j_arp_process(struct sk_buff *skb) { struct net_device *dev = skb->dev; struct in_device *in_dev; int no_addr, rpf; in_dev = in_dev_get(dev); no_addr = ( in_dev->ifa_list == NULL ); rpf = IN_DEV_RPFILTER(in_dev); in_dev_put(in_dev); printk("\narp_process() is called with interface device %s, in_dev(no_addr=%d,rpf=%d) \n", dev->name, no_addr, rpf); jprobe_return(); return(0); }; static int j_fib_validate_source(__be32 src, __be32 dst, u8 tos, int oif, struct net_device *dev, __be32 *spec_dst, u32 *itag, u32 mark) { printk("fib_validate_source() is called with dst=0x%x, oif=%d \n", dst, oif); jprobe_return(); return(0); }; static struct jprobe my_jp1 = { .entry = j_arp_process, .kp.symbol_name = "arp_process" }; static struct jprobe my_jp2 = { .entry = j_fib_validate_source, .kp.symbol_name = "fib_validate_source" }; static int entry_handler(struct kretprobe_instance *ri, struct pt_regs *regs) { printk("Calling: %s()\n", ri->rp->kp.symbol_name); return(0); }; static int return_handler(struct kretprobe_instance *ri, struct pt_regs *regs) { int eax; eax = regs->ax & 0xffff ; printk("Returning: %s() with a return value: 0x%lx(64bit) 0x%x(32bit)\n", ri->rp->kp.symbol_name, regs->ax, eax); return(0); }; static int fib_lookup_entry_handler(struct kretprobe_instance *ri, struct pt_regs *regs) { struct fib_result *resp; resp = (struct fib_result *) regs->dx; printk("Calling: %s()\n", ri->rp->kp.symbol_name); *((struct fib_result **)ri->data) = resp; return(0); }; static int fib_lookup_return_handler(struct kretprobe_instance *ri, struct pt_regs *regs) { struct fib_result *resp; int eax; eax = regs->ax & 0xffff ; resp = *((struct fib_result **) ri->data); printk("Returning: fib_lookup() with a return value: 0x%lx(64bit) 0x%x(32bit), result->type: %d\n", regs->ax, eax, resp->type); return(0); } static struct kretprobe my_rp1 = { .handler = return_handler, .entry_handler = entry_handler, .kp.symbol_name = "ip_route_input_slow" }; static struct kretprobe my_rp2 = { .handler = return_handler, .entry_handler = entry_handler, .kp.symbol_name = "fib_validate_source" }; static struct kretprobe my_rp3 = { .handler = fib_lookup_return_handler, .entry_handler = fib_lookup_entry_handler, .kp.symbol_name = "fib_lookup", .data_size = sizeof(struct fib_result *) }; static int __init init_myprobe(void) { int ret; printk("RTN_UNICAST is %d\n", RTN_UNICAST); if ( (ret = register_jprobe(&my_jp1)) < 0) { printk("register_jprobe %s failed, returned %d\n", my_jp1.kp.symbol_name, ret); return(-1); } if ( (ret = register_jprobe(&my_jp2)) < 0) { printk("register_jprobe %s failed, returned %d\n", my_jp2.kp.symbol_name, ret); return(-1); } if ( (ret = register_kretprobe(&my_rp1)) < 0 ) { printk("register_kretprobe %s failed, returned %d\n", my_rp1.kp.symbol_name, ret); unregister_jprobe(&my_jp1); unregister_jprobe(&my_jp2); return(-1); } if ( (ret = register_kretprobe(&my_rp2)) < 0 ) { printk("register_kretprobe %s failed, returned %d\n", my_rp2.kp.symbol_name, ret); unregister_jprobe(&my_jp1); unregister_jprobe(&my_jp2); unregister_kretprobe(&my_rp1); return(-1); } if ( (ret = register_kretprobe(&my_rp3)) < 0 ) { printk("register_kretprobe %s failed, returned %d\n", my_rp3.kp.symbol_name, ret); unregister_jprobe(&my_jp1); unregister_jprobe(&my_jp2); unregister_kretprobe(&my_rp1); unregister_kretprobe(&my_rp2); return(-1); } return 0; } static void __exit rel_myprobe(void) { unregister_jprobe(&my_jp1); unregister_jprobe(&my_jp2); unregister_kretprobe(&my_rp1); unregister_kretprobe(&my_rp2); unregister_kretprobe(&my_rp3); } module_init(init_myprobe); module_exit(rel_myprobe); Makefile obj-m += arp_probe.o Making #> make -C /usr/src/kernels/2.6.32-71.el6.x86_64/ M=`pwd` modules
上述内容就是如何理解Linux故障定位技术,你们学到知识或技能了吗?如果还想学到更多技能或者丰富自己的知识储备,欢迎关注编程网行业资讯频道。