精华内容
下载资源
问答
  • 《解决Linux内核问题实用技巧之 - Crash工具结合/dev/mem任意修改内存》 Linux内核程序员几乎每天都在和各种问题互相对峙: 内核崩溃了,需要排查原因。 系统参数不合适,需要更改,却没有接口。 改一个...

    来自 《解决Linux内核问题实用技巧之 - Crash工具结合/dev/mem任意修改内存》


    Linux内核程序员几乎每天都在和各种问题互相对峙:

    • 内核崩溃了,需要排查原因。
    • 系统参数不合适,需要更改,却没有接口。
    • 改一个变量或一条if语句,就要重新编译内核。
    • 想稍微调整下逻辑却没有源码,没法编译。

    解决每一类问题都需要消耗大量的时间,特别是重新编译内核这种事情。于是,每一个Linux内核程序员或多或少都会掌握一些Hack技巧,以节省时间提高工作效率。

    然而,自己Hack内核非常容易出错,稍不留意就会伤及无辜(panic,踩内存…),然后你会陷入没完没了的细节,比如查找页表就够折腾。

    俗话说工欲善其事,必先利其器,临渊羡鱼,不如退而结网。

    但是如果你使用现成的工具,就会发现有时候工具很难扩展。自己需要的边缘小众功能往往并不提供,你依然需要自己动手但却又无从下手。

    怎么办?为何不把二者结合呢?

    本文将通过几个简单的小例子,描述如何综合systemtap,crash & gdb,/dev/mem,内核模块等技术排查以及解决现实中的Linux问题。

    关于前置知识

    本文不想花太多笔墨在前置知识上,本文默认读者已经了解systemtap,crash & gdb等工具的基本用法。作为Linux内核开发者,这些工具的熟练使用是必须的。

    如果需要这些知识,自行百度或者Google(有条件的话),会得到更好的答案。其中每一个细节都可以单独写一篇文章甚至一本书。

    但还是要说一点关于 /dev/mem 的话题。

    /dev/mem 几乎总是被宣称为作为整个物理内存映像可以被mmap到进程地址空间,很多人确实将/dev/mem设备文件mmap到了自己的程序,然而却几乎一无所得。这不是程序员的错,毕竟作为一个平坦的内存地址空间,/dev/mem的内容看起来没有任何结构,一般DIY的程序根本就无力解析它。

    /dev/mem 是个宝藏,它暴露了整个内存,但是只有你拥有强大的分析能力时,它才是宝藏,否则它只是一块平坦的空间,充满着0或1。所有的内核实时数据均在 /dev/mem 中,找到它们才有意义,但找到它们并不容易。

    crash & gdb工具会把这件事情做得很好。本文后面将侧重于crash工具,gdb与此类似。

    crash不光可以用来分析调试已经死掉的Linux尸体的vmcore内存映像,还可以用来分析调试活着的Linux Live内存映像,比如/dev/mem和/proc/kcore。同样都是内存映像,调试活着的内存映像显得更加有趣些。本文的例子将无一例外地描述这个方面的操作步骤和细节。

    现在让我们开始。

    使/dev/mem可写

    小贴士:

    这个步骤非常重要!建议始终作为hack /dev/mem的第一步!

    这个例子是第一步,也是继续下面所有例子的前提。

    首先,我们执行crash命令,调试/dev/mem内存映像:

    [root@localhost ~]# crash /usr/lib/debug/usr/lib/modules/3.10.0-15.327.x86_64/vmlinux /dev/mem
    

    大多数情况下,当我们尝试使用crash工具的wr命令写一个变量或者内存地址的时候,会收获一个报错提示:

    crash> wr jiffies 123456
    wr: cannot write to /proc/kcore
    

    这是因为我们运行的Linux内核大多数情况下都开启了以下的编译选项:

    CONFIG_STRICT_DEVMEM=y
    

    这意味着,当我们尝试写 /dev/mem 的时候,会受到内核函数 devmem_is_allowed 的约束。所以,为了我们使用crash wr命令修改内存成为可能,我们必须要绕开这一约束,即:

    • 让 devmem_is_allowed 函数恒返回1。

    这一点通过systemtap很容易做到:

    [root@localhost mod]# stap -g -e 'probe kernel.function("devmem_is_allowed").return { $return = 1 }'
    
    

    在上述stap命令保持的情况下,退出crash并再次运行,此时我们便将可以完全读写 /dev/mem 了,如果说依然发生内存不可写的情况,那便是受到了页表项的约束,这个我们后面会谈。

    我们并不想让那个stap命令一直运行在那里,我们不希望通过crash写内存这个操作依赖一个不能退出的stap命令,所以第一步,我们将直接修改 devmem_is_allowed 函数本身!

    我们先反汇编它:

    crash> dis -s devmem_is_allowed
    FILE: arch/x86/mm/init.c
    LINE: 583
    
      578    * contains bios code and data regions used by X and dosemu and similar apps.
      579    * Access has to be given to non-kernel-ram areas as well, these contain the PCI
      580    * mmio resources as well as potential bios/acpi data regions.
      581    */
      582   int devmem_is_allowed(unsigned long pagenr)
    * 583   {
      584           if (pagenr < 256)
      585                   return 1;
      586           if (iomem_is_exclusive(pagenr << PAGE_SHIFT))
      587                   return 0;
      588           if (!page_is_ram(pagenr))
      589                   return 1;
      590           return 0;
      591   }
    
    crash>
    

    非常简单的逻辑,我想我们可以很快完成该函数的二进制修改。

    让我们看一下它的汇编码,并且注意到下图红色框里的细节:
    在这里插入图片描述
    我们只要将 ja xxx 处的指令改成nop序列即可绕开这个跳转,即修改 0xffffffff8105e649 地址处的2个字节的值:

    crash> wr -16 0xffffffff8105e649 0x9090
    

    当然,比这个更直接的方法是直接重写这个函数,仅仅执行两个指令, mov $0x1,%eaxretq 。但是很遗憾,使用crash命令完成这个修改难度极大,我们仔细缕一下:
    在这里插入图片描述
    无论先替换nop还是先替换ret,均会破坏栈帧,造成返回地址错误从而panic:
    在这里插入图片描述
    除非同时原子替换二者(这在crash工具中几乎不可能)。更安全的替换方案是在crash外部去替换,比如写一个内核模块。先将crash查询到的地址记录下来:
    在这里插入图片描述
    随后编写模块,修改两个地址的值:

    #include <linux/module.h>
    
    static int __init hook_init(void)
    {
    	char *to_nop = 0xffffffff8105e635;
    	char *to_ret = 0xffffffff8105e642;
    
    	to_nop[0] = 0x90;// 替换push为nop
    	to_ret[0] = 0xc3;// 替换mov为ret
    	
    	// 不是真的加载。
    	return -1;
    }
    
    static void __exit hook_exit(void)
    {
    }
    
    module_init(hook_init);
    module_exit(hook_exit);
    MODULE_LICENSE("GPL");
    

    模块加载命令执行后,我们再次crash反汇编devmem_is_allowed,看看效果:
    在这里插入图片描述
    代码还是很简洁的,最终也成功了,但是挺迂回的,没有第一种方法修改ja指令更简单。

    OK,接着我实际选择的 “修改ja指令为两个nop” 继续讲。现在让我们杀掉stap命令,并且重新打开crash,再次看 devmem_is_allowed 函数的反汇编:
    在这里插入图片描述
    很明显,条件跳转已经被改成了nop序列,至此,我们已经解除了对stap的依赖。没有stap的情况下,我们可以试试看修改一些无关紧要的东西:

    crash> rd panic_on_oops
    ffffffff81977890:  0000000000000001                    ........
    crash> wr panic_on_oops 0
    crash> rd panic_on_oops
    ffffffff81977890:  0000000000000000                    ........
    crash>
    

    现在,我们可以使用crash来修改内存了。当然,如果你照着上面的步骤一步一步挨着做也没有成功,比如在写内存时收获了下面的错误:

    crash> wr -16 0xffffffffa8c6fad4 0x9090
    wr: write error: kernel virtual address: ffffffffa8c6fad4  type: "write memory"
    crash>
    

    不要着急,后面我们会专门分析这种情况下怎么应对。

    现在,让我们继续更多的例子。

    修改TCP初始拥塞窗口

    很多搞TCP调优的同行曾经吐槽, “为什么不能修改系统的TCP初始拥塞窗口啊?!”

    目前Linux的TCP实现中初始拥塞窗口时10个mss,该值是Google内网经验和全球经验折中的结果。

    该值不能修改的原因之一我觉得是为了保证TCP的公平性。如果这个值能被随意配置修改,那岂不是把该值配置越来对自己越有利吗?这会破坏公平性。用户态的Quic就有这样的问题。

    修改TCP初始拥塞窗口的方法很多,比如将该值导出成sysctl配置并重新编译内核,比如用iproute2配置携带 init_cwnd 参数的的路有项(同样有最大值限制)等等,但是都不直接也并不简单,我们要做的只是将下面的宏改成别的值即可:

    /* TCP initial congestion window as per draft-hkchu-tcpm-initcwnd-01 */
    #define TCP_INIT_CWND       10
    

    然而宏并非变量,宏是在编译期就被替换的。

    为了确认这宏定义值,我们编写一个简单的packetdrill脚本:

    // test.pkt
    // Establish a connection and send 20 MSS.
    0.000 socket(..., SOCK_STREAM, IPPROTO_TCP) = 3
    0.000 bind(3, ..., ...) = 0
    0.000 listen(3, 1) = 0
    
    0.000 < S 0:0(0) win 32792 <mss 1000,sackOK,nop,nop,nop,wscale 7>
    0.0 > S. 0:0(0) ack 1 <...>
    0.0 < . 1:1(0) ack 1 win 1024
    0.0 accept(3, ..., ...) = 4
    
    0.0 %{ print tcpi_snd_cwnd }%
    0.0 write(4, ..., 20000) = 20000
    
    5.0 < . 1:1(0) ack 1 win 257 <sack 1001:3001,nop,nop>
    

    运行它:

    [root@localhost sack]# pdrill ./test.pkt --tolerance_usecs=10000
    10
    

    OK,很明显是10个mss。现在让我们用crash来修改它。

    TCP连接初始化拥塞窗口的函数是 tcp_init_cwnd,我们用crash的dis命令看看它是怎么实现的:
    在这里插入图片描述
    改法很简单,我们看 0xffffffff815790e7 处的内容:

    crash> rd 0xffffffff815790e7
    ffffffff815790e7:  e083480000000aba                    .....H..
    crash>
    

    将 0000000a 改成 00000005,这意味着初始拥塞窗口变成了5个mss:

    crash> wr  0xffffffff815790e7 0xe0834800000005ba
    crash>
    

    我们用packetdrill脚本确认这个修改:

    [root@localhost sack]# pdrill ./test.pkt --tolerance_usecs=10000
    5
    

    通过利用crash工具,修改TCP初始拥塞窗口非常简单。

    修改TCP Time wait时间

    很多人遭遇过TCP Time wait过多的问题,一个主动断开的连接要维持60秒的Time wait状态(Linux系统),这在现代高速网络环境下已经不再必要。我们想把这个值调小,但遗憾的是,这个值在Linux内核中同样是是以宏定义存在的,无法调整。

    和修改TCP初始拥塞窗口方法一致,不同的是 tcp_time_wait 函数的复杂度要远高于 tcp_init_cwnd 函数,不过大同小异,TCP_TIMEWAIT_LEN 和 TCP_INIT_CWND 在同一个地方被定义:

    #define TCP_TIMEWAIT_LEN (60*HZ) /* how long to wait to destroy TIME-WAIT
                      * state, about 60 seconds */
    

    在HZ被定义为1000的情况下,只需要注意即时数60*1000,即0xea60即可:
    在这里插入图片描述
    注意到该值,我们需要将其改为我们需要的更小的值。

    对于复杂的函数,在我们使用crash dis命令时,可以配合-l -s等参数将源代码和汇编指令做对应关系,更有效率地定位到我们需要修改的地方。

    修改页表项之后…

    每一个运行的Linux内核都可能来自不同的编译选项,从而导致了crash命令运行时的不同表现行为。

    比如,即便是用stap hook住了devmem_is_allowed的返回值,让它恒为1,也依然无法修改内存:

    crash> wr -16 0xffffffffa8c6fad4 0x9090
    wr: write error: kernel virtual address: ffffffffa8c6fad4  type: "write memory"
    

    这是为什么呢?

    在现代操作系统中,地址空间的内存操作全部针对虚拟地址进行,而决定该虚拟内存对应的物理内存是否可写是由页表项决定的。

    所以,首先我们要确认页表项的权限,这在crash中用vtop即可:

    crash>
    crash> vtop 0xffffffffa8c6fad4
    wr: current context no longer exists -- restoring "crash" context:
    
        PID: 3262
    COMMAND: "crash"
       TASK: ffff9e6bfdbacf10  [THREAD_INFO: ffff9e6bf67e4000]
        CPU: 2
      STATE: TASK_RUNNING (ACTIVE)
    
    VIRTUAL           PHYSICAL
    ffffffffa8c6fad4  3b86fad4
    
    PML4 DIRECTORY: ffffffffa980e000
    PAGE DIRECTORY: 3c412067
       PUD: 3c412ff0 => 3c413063
       PMD: 3c413a30 => 3b8000e1
      PAGE: 3b800000  (2MB)
    
      PTE     PHYSICAL  FLAGS
    3b8000e1  3b800000  (PRESENT|ACCESSED|DIRTY|PSE) # 无可写标志!!
    
          PAGE       PHYSICAL      MAPPING       INDEX CNT FLAGS
    ffffe06f80ee1bc0 3b86f000                0        0  1 1fffff00000400 reserved
    crash>
    

    注意到, 3b8000e1 表明该页表项指向的页面是不可写的!

    我们需要修改页表项,而这个很容易,注意以下的关系:
    在这里插入图片描述
    我们只需要写物理内存 0x3c413a30 即可:

    crash> rd -p 3c413a30
            3c413a30:  000000003b8000e1                    ...;....
    crash> wr -p 3c413a30 000000003b8000e3
    crash>
    

    此时,再次确认之前写失败的vtop结果:

      PTE     PHYSICAL  FLAGS
    3b8000e3  3b800000  (PRESENT|RW|ACCESSED|DIRTY|PSE) # 有了可写标志
    

    OK,有了可写标志,现在写入它:

    wr: write error: kernel virtual address: ffffffffa8c6fad4  type: "write memory"
    

    很不幸,又失败了。这是为什么呢?

    写一个单独的内核模块,刷新一下TLB。刷TLB很容易,就是重新载入RC4寄存器并重新使能分页机制即可,此时系统会将所有的TLB失效。

    有个未导出的 flush_tlb_all 现成的函数却不可以直接调用,我们需要在 /proc/kallsyms 里找到它的地址来调用。代码如下:

    #include <linux/module.h>
    
    void (*pf)(void);
    static int __init flushtlb_init(void)
    {
    	// 从/proc/kallsyms获取flush_tlb_all的地址并调用.
    	pf = 0xffffffffa8c7a040;
    	pf();
    	// 不要真正加载
    	return -1;
    }
    
    static void __exit flushtlb_exit(void)
    {
    }
    
    module_init(flushtlb_init);
    module_exit(flushtlb_exit);
    MODULE_LICENSE("GPL");
    

    我们发现,只要刷一遍TLB,crash中读取的页表项PTE就会重新还原为不可写。

    出现该问题的内核编译时有两个选项CONFIG_PHYSICAL_START和CONFIG_PHYSICAL_START,crash的manual中有关于此的描述:

    –reloc size
    When analyzing live x86 kernels that were configured with a CONFIG_PHYSICAL_START value that is larger than its CONFIG_PHYSICAL_ALIGN value, then it will be necessary to enter a
    relocation size equal to the difference between the two values.

    我们确认下当前写失败的内核的config文件配置:

    CONFIG_PHYSICAL_START=0x1000000
    CONFIG_PHYSICAL_ALIGN=0x200000
    

    与此同时,该内核的flush_tlb_all并非简单操作RC4寄存器这么简单。

    我们不再指望使用crash直接修改内存,退一步,让crash作为我们的信息查询工具起作用,剩余的内存写操作让我们自己写内核模块来做。

    依然以修改TCP初始拥塞窗口为例,我们要改两个地方:

    1. 改页表项,让 tcp_init_cwnd 函数指令可写。
    2. 改tcp_init_cwnd函数硬编码的指令操作数。

    分两步走,先找到页表项并通过ptov命令得到它的虚拟地址(所有OS级别的写内存都基于虚拟地址进行):
    在这里插入图片描述
    再找需要修改的指令地址和值:
    在这里插入图片描述
    我们依据这些值编写内核模块:

    #include <linux/module.h>
    
    void (*pf)(void);
    static int __init modcwnd_init(void)
    {
    	char *ppte = 0xffff9e6bfc413a48; // 需要修改的PTE地址
    	long *pvalue = 0xffffffffa924eb67; // 需要修改的窗口值地址
    
    	pf = 0xffffffffa8c7a040;
    
    	ppte[0] = 0xe3;
    	pf();
    
    	// 我们将其改成一个奇怪的值:6
    	pvalue[0] = 0xe0834800000006ba;
    
    	return -1;
    }
    
    static void __exit modcwnd_exit(void)
    {
    }
    
    module_init(modcwnd_init);
    module_exit(modcwnd_exit);
    MODULE_LICENSE("GPL");
    

    尝试加载模块之后,执行我们上面的packetdrill脚本,这次让我们用抓包来确认:
    在这里插入图片描述
    正好6个数据段。

    这种情况下,crash工具成了辅助,而真正起作用的是我们自己编写的内核模块,而这些背后,需要我们对操作系统整体的内存管理机制拥有清晰的认知。编写这种内核模块也是Linux内核程序员必备的技能。

    修改用户进程内存

    来点轻松的,我们来用crash修改一个用户态程序的内存,先看程序代码:

    #include <stdio.h>
    #include <stdlib.h>
    
    int main()
    {
    	unsigned long a = 0x1122334455667788;
    	printf("%lx   %p\n", a, &a);
    	getchar();
    	printf("%lx\n", a);
    	getchar();
    }
    

    和《Linux内核如何私闯进程地址空间并修改进程内存》这篇文章里做的事情差不多,不同的是,本文我们用一种更加优雅的方式来进行内存篡改。

    首先,运行它:

    [root@localhost mod]# ./a.out
    1122334455667788   0x7fff45d5d4d8
    
    

    然后在crash中找到它:

    crash> ps |grep a.out
       7166   1582   0  ffff9e6bf66f8000  IN   0.0    4324    516  a.out
    crash> set 7166
        PID: 7166
    COMMAND: "a.out"
       TASK: ffff9e6bf66f8000  [THREAD_INFO: ffff9e6bcb0ec000]
        CPU: 0
      STATE: TASK_INTERRUPTIBLE
    

    我们的目标是修改变量a的值,因此我们要定位该进程的用户态堆栈。

    注意,此时getchar已经在内核空间等待了,所以bt命令只是内核栈,用户占还需要我们自己来找,我们从进程的task_struct结构体里寻找用户堆栈的蛛丝马迹:

    crash> task_struct ffff9e6bf66f8000
    struct task_struct {
      state = 1,
      stack = 0xffff9e6bcb0ec000,
      usage = {
        counter = 2
      },
      flags = 4202496,
    ...
        sp0 = 18446636784538288128,
        sp = 18446636784538287176,
        usersp = 140734365029480,
    ...
    

    我们就从 usersp = 140734365029480 开始找吧:
    在这里插入图片描述
    现在修改它:

    crash> wr -u -64 0x7fff45d5d4d8 0xaabbccdd99887700
    

    然后在a.out运行的终端敲回车:

    [root@localhost mod]# ./a.out
    1122334455667788   0x7fff45d5d4d8
    
    aabbccdd99887700
    
    

    可以看到,变量a的值发生了改变。

    拯救D进程

    我们经常在系统中发现D状态的进程,大多数情况下我们对其无能为力。

    D进程处在 等待资源不能满足却又不能离开 的两难境地。然而这并不意味着D进程不可拯救,我们只需要解除它对资源的依赖,然后让它退出即可,即调用 do_exit

    以下三篇文章描述了一种拯救D进程的方法:
    Linux如何终止D状态的进程https://blog.csdn.net/dog250/article/details/53043445
    Linux x86内核终止D状态的进程https://blog.csdn.net/dog250/article/details/53071973
    Linux x86_64内核终止D状态的进程https://blog.csdn.net/dog250/article/details/53072028

    下面将介绍另一种方法。为了举个例子,首先我们要先自己造一个D进程。我们先写一个内核模块

    // main4.c
    #include <linux/module.h>
    #include <linux/sched.h>
    #include <linux/wait.h>
    
    int condition = 1234;
    module_param(condition, uint, 0644);
    
    wait_queue_head_t waitq;
    
    static int __init Ds1_init(void)
    {
    	long magic = 0x22334455667788;
    
    	condition = 0x1234;
    	printk("condition:%lu  magic:%lu   %p\n", condition, magic, &waitq);
    	init_waitqueue_head (&waitq);
    	// 此处没有任何人会将condition设置为123,因此insmod会一直等待,进而D住
    	wait_event(waitq, condition == 123);
    
    	return 0;
    }
    
    static void __exit Ds1_exit(void)
    {
    }
    
    module_init(Ds1_init);
    module_exit(Ds1_exit);
    MODULE_LICENSE("GPL");
    

    然后我们试着加载这个模块。很不幸,卡住了,即便是 kill -9 也无法杀掉它。很显然,它D住了:
    在这里插入图片描述
    现在,我们如何将其从D状态激活呢?下面我们用crash工具试试看。我们的目标有3步:

    1. 找到insmod进程。
    2. 修改内核模块里的condition变量的值为希望的123。
    3. 唤醒insmod睡眠在的wait队列。

    我们一步一步来。先找到insmod进程:

    crash> ps |grep insmod
       9074   1408   0  ffff88003a3e3de0  UN   0.1   13252    800  insmod
    crash> set 9074
        PID: 9074
    COMMAND: "insmod"
       TASK: ffff88003a3e3de0  [THREAD_INFO: ffff880022a2c000]
        CPU: 0
      STATE: TASK_UNINTERRUPTIBLE  # D状态
    crash>
    

    接下来我们要找到condition变量的位置,这个需要些技巧,千人千法。我这里只介绍我采用的方法。先打印出最后的stack:

    crash> bt
    PID: 9074   TASK: ffff88003a3e3de0  CPU: 0   COMMAND: "insmod"
     #0 [ffff880022a2fca8] __schedule at ffffffff81639b5d
     #1 [ffff880022a2fd10] schedule at ffffffff8163a199
     #2 [ffff880022a2fd20] init_module at ffffffffa00370bb [main4]
     #3 [ffff880022a2fd60] do_one_initcall at ffffffff810020e8
     #4 [ffff880022a2fd90] load_module at ffffffff810e9f3e
     #5 [ffff880022a2fee8] sys_finit_module at ffffffff810ea8f6
     #6 [ffff880022a2ff80] system_call_fastpath at ffffffff81645189
        RIP: 00007f313239a1c9  RSP: 00007ffdbeea5578  RFLAGS: 00010216
    ... # 不care寄存器,因为这种case用不到
        R13: 00000000007671c0  R14: 0000000000000000  R15: 00000000007671f0
        ORIG_RAX: 0000000000000139  CS: 0033  SS: 002b
    crash>
    

    我们希望可以在main4模块的函数 内部 找到为condition变量赋值的语句或者找到 “condition == 123” 。我们注意到以下的行:

     #2 [ffff880022a2fd20] init_module at ffffffffa00370bb [main4]
    

    我们就在地址 0xffffffffa00370bb 前面某个地方找找看。之所以在前面找是因为condition变量的操作语句在执行流调用下一个函数之前,所以还在上一个栈帧上:
    在这里插入图片描述
    当然,更直接的,还可以直接反汇编Ds1_init函数,很容易从内核栈上获取它的位置:
    在这里插入图片描述
    现在很明确,方法有两个:

    1. 修改cmpl语句,将123,即0x7b改成0x1234。
    2. 修改condition变量位置的值,改成0x7b,即123。

    我选择方法2(宁改数据不改指令,万一碰到指令不可写又要改页表项):

    crash> rd -32 0xffffffffa0146000
    ffffffffa0146000:  00001234                              4...
    crash> wr -32  0xffffffffa0146000 0x0000007b
    crash>
    crash> waitq 0xffffffffa0370260
    PID: 9074   TASK: ffff88003a3e3de0  CPU: 1   COMMAND: "insmod"
    crash>
    

    现在condition的值已经是123了,接下来最后一步,唤醒insmod的睡眠队列wait。再看上面的反汇编:
    在这里插入图片描述
    注意到地址 0xffffffffa0146260 ,作为函数 prepare_to_wait 的参数,它就是等待队列waitq。我们确认一下:

    crash> wait_queue_head_t
    typedef struct __wait_queue_head {
        spinlock_t lock;
        struct list_head task_list;
    } wait_queue_head_t;
    SIZE: 24
    crash> wait_queue_head_t 0xffffffffa0146260
    struct wait_queue_head_t {
      lock = {
        {
          rlock = {
            raw_lock = {
              {
                head_tail = 131074,
                tickets = {
                  head = 2,
                  tail = 2
                }
              }
            }
          }
        }
      },
      task_list = {
        next = 0xffff880022a2fd40,
        prev = 0xffff880022a2fd40
      }
    }
    

    0xffff880022a2fd40 作为两枚list_head指针,链入的正是wait_queue_t,我们可以再次确认:
    在这里插入图片描述
    其中确认wait_queue_t对象时将list减去3*8这个偏移是可以用crash工具的 struct wait_queue_t.task_list -o 计算出来的。

    现在,是时候写一个内核模块来唤醒D进程了:

    #include <linux/module.h>
    #include <linux/sched.h>
    
    static int __init wake_init(void)
    {
    	wait_queue_head_t *wait = 0xffff880022a2fd28;
    
    	wake_up(wait);
    	// 并不加载,wakeup后即退出。
    	return -1;
    }
    
    static void __exit wake_exit(void)
    {
    }
    
    module_init(wake_init);
    module_exit(wake_exit);
    MODULE_LICENSE("GPL");
    

    编译加载,加载,效果如下:

    [root@localhost mod]# insmod ./wake.ko
    insmod: ERROR: could not insert module ./wake.ko: Operation not permitted
    [root@localhost mod]# ps -elf|grep [i]nsmod
    [root@localhost mod]#
    

    这就完成了我们拯救D进程的演示,但是注意,拯救D进程没有通用的方法,即便是成功将其从D状态救出,也依然要确认资源依赖,解除资源依赖后尽快调用 do_exit

    结语

    以上只是抛砖引玉般结合 crash,stap以及内核模块分析了几个简单的实例,如果继续下去,还会有非常多类似的例子以及更为复杂更为有趣的案例供我们去分析或者把玩,这将给我们带来无穷无尽的快乐。

    但是限于篇幅,本文只能在此点到为止。本文的宗旨在于,通过这些简单有趣的实例,让我们理解工具的使用方法以及使用这些工具的重要性。

    与此同时,在我们日常分析解决Linux内核问题时,如何使用工具并不是核心,工具始终只是一个让你的工作效率更高的锦上之花,真正干货的背后永远都是对操作系统理论以及对Linux内核本身的理解和掌握,否则,工具掌握得再熟练也只能是个熟练工。


    浙江温州皮鞋湿,下雨进水不会胖。

    展开全文
  • 1.vmstat [-V] [-n] [depay [count]] -V : 打印出版本信息,可选参数 -n : 在周期性循环输出时,头部信息仅显示一次 delay : 两次输出之间的时间间隔 count : 按照delay指定的时间间隔统计的...

    一. CPU性能评估

    1.vmstat [-V] [-n] [depay [count]]

    -V : 打印出版本信息,可选参数

    -n : 在周期性循环输出时,头部信息仅显示一次

    delay : 两次输出之间的时间间隔

    count : 按照delay指定的时间间隔统计的次数。默认是1

    如:vmstat 1 3

    user1@user1-desktop:~$ vmstat 1 3

    procs -----------memory---------- ---swap-- -----io---- -system-- ----cpu----

    r b swpd free buff cache si so bi bo in cs us sy id wa

    0 0 0 1051676 139504 477028 0 0 46 31 130 493 3 1 95 2

    0 0 0 1051668 139508 477028 0 0 0 4 377 1792 3 1 95 0

    0 0 0 1051668 139508 477028 0 0 0 0 327 1741 3 1 95 0

    r : 运行和等待CPU时间片的进程数(若长期大于CPU的个数,说明CPU不足,需要增加CPU)

    b : 在等待资源的进程数(如等待I/O或者内存交换等)

    swpd : 切换到内存交换区的内存数量,单位kB

    free : 当前空闲物理内存,单位kB

    buff : buffers cache的内存数量,一般对块设备的读写才需要缓存

    cache : page cached的内存数量,一般作为文件系统cached,频繁访问的文件都会被cached

    si : 由磁盘调入内存,即内存进入内存交换区的数量

    so : 内存调入磁盘,内存交换区进入内存的数量

    bi : 从块设备读入数据的总量,即读磁盘,单位kB/s

    bo : 写入到块设备的数据总量,即写磁盘,单位kB/s

    in : 某一时间间隔中观测到的每秒设备中断数

    cs : 每秒产生的上下文切换次数

    us :用户进程消耗的CPU时间百分比【注意】

    sy : 内核进程消耗CPU时间百分比【注意】

    id : CPU处在空闲状态的时间百分比【注意】

    wa :IO等待所占用的CPU时间百分比

    如果si、so的值长期不为0,表示系统内从不足,需要增加系统内存

    bi+bo参考值为1000,若超过1000,且wa较大,表示系统IO有问题,应该提高磁盘的读写性能

    in与cs越大,内核消耗的CPU时间就越多

    us+sy参考值为80%,如果大于80%,说明可能存在CPU资源不足的情况

    综上所述,CPU性能评估中重点注意r、us、sy和id列的值。

    2. sar [options] [-o filename] [interval [count] ]

    options:

    -A :显示系统所有资源设备(CPU、内存、磁盘)的运行状态

    -u : 显示系统所有CPU在采样时间内的负载状态

    -P : 显示指定CPU的使用情况(CPU计数从0开始)

    -d : 显示所有硬盘设备在采样时间内的使用状况

    -r : 显示内存在采样时间内的使用状况

    -b : 显示缓冲区在采样时间内的使用情况

    -v : 显示进程、文件、I节点和锁表状态

    -n :显示网络运行状态。参数后跟DEV(网络接口)、EDEV(网络错误统计)、SOCK(套接字)、FULL(显示其它3个参数所有)。可单独或一起使用

    -q : 显示运行队列的大小,与系统当时的平均负载相同

    -R : 显示进程在采样时间内的活动情况

    -y : 显示终端设备在采样时间内的活动情况

    -w : 显示系统交换活动在采样时间内的状态

    -o : 将命令结果以二进制格式存放在指定的文件中

    interval : 采样间隔时间,必须有的参数

    count : 采样次数,默认1

    如:sar -u 1 3

    user1@user1-desktop:~$ sar -u 1 3

    Linux 2.6.35-27-generic (user1-desktop) 2011年03月05日 _i686_ (2 CPU)

    09时27分18秒 CPU %user %nice %system %iowait %steal %idle

    09时27分19秒 all 1.99 0.00 0.50 5.97 0.00 91.54

    09时27分20秒 all 3.90 0.00 2.93 5.85 0.00 87.32

    09时27分21秒 all 2.93 0.00 1.46 4.39 0.00 91.22

    平均时间: all 2.95 0.00 1.64 5.40 0.00 90.02

    %user : 用户进程消耗CPU时间百分比

    %nice : 运行正常进程消耗CPU时间百分比

    %system : 系统进程消耗CPU时间百分比

    %iowait : IO等待多占用CPU时间百分比

    %steal : 内存在相对紧张坏经下pagein强制对不同页面进行的steal操作

    %idle : CPU处在空闲状态的时间百分比

    3. iostat [-c | -d] [-k] [-t] [-x [device]] [interval [count]]

    -c :显示CPU使用情况

    -d :显示磁盘使用情况

    -k : 每秒以k bytes为单位显示数据

    -t :打印出统计信息开始执行的时间

    -x device :指定要统计的磁盘设备名称,默认为所有磁盘设备

    interval :制定两次统计时间间隔

    count : 统计次数

    如: iostat -c

    user1@user1-desktop:~$ iostat -c

    Linux 2.6.35-27-generic (user1-desktop) 2011年03月05日 _i686_ (2 CPU)

    avg-cpu: %user %nice %system %iowait %steal %idle

    2.51 0.02 1.27 1.40 0.00 94.81

    (每项代表的含义与sar相同)

    4uptime ,如:

    user1@user1-desktop:~$ uptime

    10:13:30 up 1:15, 2 users, load average: 0.00, 0.07, 0.11

    显示的分别是:系统当前时间,系统上次开机到现在运行了多长时间,目前登录用户个数,系统在1分钟内、5分钟内、15分钟内的平均负载

    注意:load average的三个值一般不能大于系统CPU的个数,否则说明CPU很繁忙

    二 . 内存性能评估

    1free

    2.watch 与 free 相结合,在watch后面跟上需要运行的命令,watch就会自动重复去运行这个命令,默认是2秒执行一次,如:

    Every 2.0s: free Sat Mar 5 10:30:17 2011

    total used free shared buffers cached

    Mem: 2060496 1130188 930308 0 261284 483072

    -/+ buffers/cache: 385832 1674664

    Swap: 3000316 0 3000316

    (-n指定重复执行的时间,-d表示高亮显示变动)

    3.使用vmstat,关注swpd、si和so

    4. sar -r如:

    user1@user1-desktop:~$ sar -r 2 3

    Linux 2.6.35-27-generic (user1-desktop) 2011年03月05日 _i686_ (2 CPU)

    10时34分11秒 kbmemfree kbmemused %memused kbbuffers kbcached kbcommit %commit

    10时34分13秒 923548 1136948 55.18 265456 487156 1347736 26.63

    10时34分15秒 923548 1136948 55.18 265464 487148 1347736 26.63

    10时34分17秒 923548 1136948 55.18 265464 487156 1347736 26.63

    平均时间: 923548 1136948 55.18 265461 487153 1347736 26.63

    kbmemfree : 空闲物理内存

    kbmemused : 已使用物理内存

    %memused : 已使用内存占总内存百分比

    kbbuffers : Buffer Cache大小

    kbcached : Page Cache大小

    kbcommit : 应用程序当前使用内存大小

    %commit :应用程序使用内存百分比

    三 . 磁盘I/O性能评估

    1sar -d ,如:

    user1@user1-desktop:~$ sar -d 1 3

    Linux 2.6.35-27-generic (user1-desktop) 2011年03月05日 _i686_ (2 CPU)

    10时42分27秒 DEV tps rd_sec/s wr_sec/s avgrq-sz avgqu-sz await svctm %util

    10时42分28秒 dev8-0 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00

    10时42分28秒 DEV tps rd_sec/s wr_sec/s avgrq-sz avgqu-sz await svctm %util

    10时42分29秒 dev8-0 2.00 0.00 64.00 32.00 0.02 8.00 8.00 1.60

    10时42分29秒 DEV tps rd_sec/s wr_sec/s avgrq-sz avgqu-sz await svctm %util

    10时42分30秒 dev8-0 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00

    平均时间: DEV tps rd_sec/s wr_sec/s avgrq-sz avgqu-sz await svctm %util

    平均时间: dev8-0 0.67 0.00 21.33 32.00 0.01 8.00 8.00 0.53

    DEV : 磁盘设备名称

    tps :每秒到物理磁盘的传送数,即每秒的I/O流量。一个传送就是一个I/O请求,多个逻辑请求可以被合并为一个物理I/O请求

    rc_sec/s:每秒从设备读入的扇区数(1扇区=512字节)

    wr_sec/s : 每秒写入设备的扇区数目

    avgrq-sz : 平均每次设备I/O操作的数据大小(以扇区为单位)

    avgqu-sz : 平均I/O队列的长度

    await : 平均每次设备I/O操作的等待时间(毫秒)

    svctm :平均每次设备I/O 操作的服务时间(毫秒)

    %util :一秒中有百分之几的时间用用于I/O操作

    正常情况下svctm应该小于await,而svctm的大小和磁盘性能有关,CPU、内存的负荷也会对svctm值造成影响,过多的请求也会简介导致svctm值的增加。

    await的大小一般取决与svctm的值和I/O队列长度以及I/O请求模式。如果svctm与await很接近,表示几乎没有I/O等待,磁盘性能很好;如果await的值远高于svctm的值,表示I/O队列等待太长,系统上运行的应用程序将变慢,此时可以通过更换更快的硬盘来解决问题。

    %util若接近100%,表示磁盘产生I/O请求太多,I/O系统已经满负荷地在工作,该磁盘可能存在瓶颈。长期下去,势必影响系统的性能,可通过优化程序或者通过更换更高、更快的磁盘来解决此问题。

    2. iostat -d

    user1@user1-desktop:~$ iostat -d 2 3

    Linux 2.6.35-27-generic (user1-desktop) 2011年03月05日 _i686_ (2 CPU)

    Device: tps Blk_read/s Blk_wrtn/s Blk_read Blk_wrtn

    sda 5.89 148.87 57.77 1325028 514144

    Device: tps Blk_read/s Blk_wrtn/s Blk_read Blk_wrtn

    sda 0.00 0.00 0.00 0 0

    Device: tps Blk_read/s Blk_wrtn/s Blk_read Blk_wrtn

    sda 0.00 0.00 0.00 0 0

    Blk_read/s : 每秒读取的数据块数

    Blk_wrtn/s : 每秒写入的数据块数

    Blk_read : 读取的所有块数

    Blk_wrtn : 写入的所有块数

    如果Blk_read/s很大,表示磁盘直接读取操作很多,可以将读取的数据写入内存中进行操作;如果Blk_wrtn/s很大,表示磁盘的写操作很频繁,可以考虑优化磁盘或者优化程序。这两个选项没有一个固定的大小,不同的操作系统值也不同,但长期的超大的数据读写,肯定是不正常的,一定会影响系统的性能。

    3iostat -x /dev/sda 2 3 ,对指定磁盘的单独统计

    4vmstat -d

    四 . 网络性能评估

    1ping

    time值显示了两台主机之间的网络延时情况,若很大,表示网络的延时很大。packets loss表示网络丢包率,越小表示网络的质量越高。

    2netstat -i ,如:

    user1@user1-desktop:~$ netstat -i

    Kernel Interface table

    Iface MTU Met RX-OK RX-ERR RX-DRP RX-OVR TX-OK TX-ERR TX-DRP TX-OVR Flg

    eth0 1500 0 6043239 0 0 0 87311 0 0 0 BMRU

    lo 16436 0 2941 0 0 0 2941 0 0 0 LRU

    Iface : 网络设备的接口名称

    MTU : 最大传输单元,单位字节

    RX-OK / TX-OK : 准确无误地接收 / 发送了多少数据包

    RX-ERR / TX-ERR : 接收 / 发送数据包时产生了多少错误

    RX-DRP / TX-DRP : 接收 / 发送数据包时丢弃了多少数据包

    RX-OVR / TX-OVR : 由于误差而遗失了多少数据包

    Flg :接口标记,其中:

    L :该接口是个回环设备

    B : 设置了广播地址

    M : 接收所有的数据包

    R :接口正在运行

    U : 接口处于活动状态

    O : 在该接口上禁用arp

    P :表示一个点到点的连接

    正常情况下,RX-ERR,RX-DRP,RX-OVR,TX-ERR,TX-DRP,TX-OVR都应该为0,若不为0且很大,那么网络质量肯定有问题,网络传输性能也一定会下降。

    当网络传输存在问题时,可以检测网卡设备是否存在故障,还可以检查网络部署环境是否合理。

    3. netstat -r (default行对应的值表示系统的默认路由)

    4sar -n ,n后为DEV(网络接口信息)、EDEV(网络错误统计信息)、SOCK(套接字信息)、和FULL(显示所有)

    wangxin@wangxin-desktop:~$ sar -n DEV 2 3

    Linux 2.6.35-27-generic (wangxin-desktop) 2011年03月05日 _i686_ (2 CPU)

    11时55分32秒 IFACE rxpck/s txpck/s rxkB/s txkB/s rxcmp/s txcmp/s rxmcst/s

    11时55分34秒 lo 2.00 2.00 0.12 0.12 0.00 0.00 0.00

    11时55分34秒 eth0 2.50 0.50 0.31 0.03 0.00 0.00 0.00

    11时55分34秒 IFACE rxpck/s txpck/s rxkB/s txkB/s rxcmp/s txcmp/s rxmcst/s

    11时55分36秒 lo 0.00 0.00 0.00 0.00 0.00 0.00 0.00

    11时55分36秒 eth0 1.50 0.00 0.10 0.00 0.00 0.00 0.00

    11时55分36秒 IFACE rxpck/s txpck/s rxkB/s txkB/s rxcmp/s txcmp/s rxmcst/s

    11时55分38秒 lo 0.00 0.00 0.00 0.00 0.00 0.00 0.00

    11时55分38秒 eth0 14.50 0.00 0.88 0.00 0.00 0.00 0.00

    平均时间: IFACE rxpck/s txpck/s rxkB/s txkB/s rxcmp/s txcmp/s rxmcst/s

    平均时间: lo 0.67 0.67 0.04 0.04 0.00 0.00 0.00

    均时间: eth0 6.17 0.17 0.43 0.01 0.00 0.00 0.00

    IFACE : 网络接口设备

    rxpck/s : 每秒接收的数据包大小

    txpck/s :每秒发送的数据包大小

    rxkB/s : 每秒接受的字节数

    txkB/s : 每秒发送的字节数

    rxcmp/s : 每秒接受的压缩数据包

    txcmp/s : 每秒发送的压缩数据包

    rxmcst/s : 每秒接受的多播数据包

    原文链接:http://blog.csdn.net/xiangqiao123/article/details/37659745


    安装 yum install -y sysstat

    sar -d 1 1

    rrqm/s: 每秒进行 merge 的读操作数目。即 delta(rmerge)/s
    wrqm/s: 每秒进行 merge 的写操作数目。即 delta(wmerge)/s
    r/s: 每秒完成的读 I/O 设备次数。即 delta(rio)/s
    w/s: 每秒完成的写 I/O 设备次数。即 delta(wio)/s
    rsec/s: 每秒读扇区数。即 delta(rsect)/s
    wsec/s: 每秒写扇区数。即 delta(wsect)/s
    rkB/s: 每秒读K字节数。是 rsect/s 的一半,因为每扇区大小为512字节。(需要计算)
    wkB/s: 每秒写K字节数。是 wsect/s 的一半。(需要计算)
    avgrq-sz: 平均每次设备I/O操作的数据大小 (扇区)。delta(rsect+wsect)/delta(rio+wio)
    avgqu-sz: 平均I/O队列长度。即 delta(aveq)/s/1000 (因为aveq的单位为毫秒)。
    await: 平均每次设备I/O操作的等待时间 (毫秒)。即 delta(ruse+wuse)/delta(rio+wio)
    svctm: 平均每次设备I/O操作的服务时间 (毫秒)。即 delta(use)/delta(rio+wio)
    %util: 一秒中有百分之多少的时间用于 I/O 操作,或者说一秒中有多少时间 I/O 队列是非空的。即 delta(use)/s/1000 (因为use的单位为毫秒)

    如果 %util 接近 100%,说明产生的I/O请求太多,I/O系统已经满负荷,该磁盘
    可能存在瓶颈。
    idle小于70% IO压力就较大了,一般读取速度有较多的wait.
    同时可以结合vmstat 查看查看b参数(等待资源的进程数)和wa参数(IO等待所占用的CPU时间的百分比,高过30%时IO压力高)

    另外还可以参考
    svctm 一般要小于 await (因为同时等待的请求的等待时间被重复计算了),svctm 的大小一般和磁盘性能有关,CPU/内存的负荷也会对其有影响,请求过多也会间接导致 svctm 的增加。await 的大小一般取决于服务时间(svctm) 以及 I/O 队列的长度和 I/O 请求的发出模式。如果 svctm 比较接近 await,说明 I/O 几乎没有等待时间;如果 await 远大于 svctm,说明 I/O 队列太长,应用得到的响应时间变慢,如果响应时间超过了用户可以容许的范围,这时可以考虑更换更快的磁盘,调整内核 elevator 算法,优化应用,或者升级 CPU。
    队列长度(avgqu-sz)也可作为衡量系统 I/O 负荷的指标,但由于 avgqu-sz 是按照单位时间的平均值,所以不能反映瞬间的 I/O 洪水。

    在命令行方式下,如何查看CPU、内存的使用情况,网络流量和磁盘I/O?

    Q: 在命令行方式下,如何查看CPU、内存的使用情况,网络流量和磁盘I/O?

     

    A: 在命令行方式下,

    1. 查看CPU使用情况的命令

    $ vmstat 5

    每5秒刷新一次,最右侧有CPU的占用率的数据

    $ top

    top 然后按Shift+P,按照进程处理器占用率排序

    2. 查看内存使用情况的命令

    $ free

    top 然后按Shift+M, 按照进程内存占用率排序

    $ top

    3. 查看网络流量

    可以用工具iptraf工具

    $ iptraf -g

    “”针对某个Interface的网络流量可以通过比较两个时间网络接口的RX和TX数据来获得

    $ date; ifconfig eth1

    $ date; ifconfig eth1

    4. 查看磁盘i/o

    $ iostat -d -x /dev/sdc3 2

    用iostat查看磁盘/dev/sdc3的磁盘i/o情况,每两秒刷新一次

    $ vmstat 2

    用vmstat查看io部分的信息

    procs:
    r–>;在运行队列中等待的进程数
    b–>;在等待io的进程数
    w–>;可以进入运行队列但被替换的进程

    memoy
    swap–>;现时可用的交换内存(k表示)
    free–>;空闲的内存(k表示)

    pages
    re--》回收的页面
    mf--》非严重错误的页面
    pi--》进入页面数(k表示)
    po--》出页面数(k表示)
    fr--》空余的页面数(k表示)
    de--》提前读入的页面中的未命中数
    sr--》通过时钟算法扫描的页面

    disk 显示每秒的磁盘操作。 s表示scsi盘,0表示盘号

    fault 显示每秒的中断数
    in--》设备中断
    sy--》系统中断
    cy--》cpu交换

    cpu 表示cpu的使用状态
    cs--》用户进程使用的时间
    sy--》系统进程使用的时间
    id--》cpu空闲的时间

    其中:
    如果 r经常大于 4 ,且id经常少于40,表示cpu的负荷很重。
    如果pi,po 长期不等于0,表示内存不足。
    如果disk 经常不等于0, 且在 b中的队列 大于3, 表示 io性能不好。


    【编辑推荐】

    展开全文
  • 环境使用python3.6.2版本,...通过远程执行‘cat /proc/meminfo’可以获取内存相关信息。这里我只读取MemTotal和MemFree的信息。需要读取其他信息可以利用正则表达式匹配获取其他数据。 import paramiko import re #

    环境使用python3.6.2版本,linux使用centos7


    这个程序主要使用paramiko模块实现。我们实现相关信息的获取,具体的监控系统和手段由于大家实现方式不一,就不举例了。

    1、内存信息的读取。

    通过远程执行‘cat /proc/meminfo’可以获取内存相关信息。这里我只读取MemTotal和MemFree的信息。需要读取其他信息可以利用正则表达式匹配获取其他数据。例子把这两个信息输出到标准输出中,实际应用可以通过crontab定时执行脚本,把结果写入文件,可以给传给监控系统,当超越一定阀值的时候进行相应的处理

    import paramiko
    import re
    
    #设置主机列表
    host_list=({'ip':'192.168.98.130', 'port':22, 'username':'root', 'password':'123'},
               {'ip':'192.168.98.131', 'port':22, 'username':'root', 'password':'123'},)
    
    ssh = paramiko.SSHClient()
    # 设置为接受不在known_hosts 列表的主机可以进行ssh连接
    ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
    
    for host in host_list:
        ssh.connect(hostname=host['ip'], port=host['port'], username=host['username'], password=host['password'])
        print(host['ip'])
        stdin, stdout, stderr = ssh.exec_command('cat /proc/meminfo')
        str_out = stdout.read().decode()
        str_err = stderr.read().decode()
        
        if str_err != "":
            print(str_err)
            continue
    
        
        str_total = re.search('MemTotal:.*?\n', str_out).group()
        print(str_total)
        totalmem = re.search('\d+',str_total).group()
        
        str_free = re.search('MemFree:.*?\n', str_out).group()
        print(str_free)
        freemem = re.search('\d+',str_free).group()
        use = round(float(freemem)/float(totalmem), 2)
        print('当前内存使用率为:'+ str(use))
        
        ssh.close()

    2、CPU使用率的读取

    这里使用cat /proc/stat命令读取实时的CPU使用率。这个命令可以获取到cpu的各种时间。具体的分析请自行百度。由于这个时间数值是从开机之后一直累加的,因此我们要取一次值之后,隔一小段时间再取一次值,前后两次的值相减,再计算这段时间的CPU利用率。具体公式是:

    CPU利用率= 1-(CPU空闲时间2 - CPU空闲时间1) / (CPU总时间2 - CPU总时间1)

    其中"CPU空闲时间1"为第一次取值时第4项的值,"CPU空闲时间2"为第二次取值时第4项的值,"CPU总时间1"为第一次取值时各项数值的总和,"CPU总时间2"为第二次取值时各项数值的总和

    import paramiko
    import re
    import time
    import sys
    
    #设置主机列表
    host_list=({'ip':'192.168.98.130', 'port':22, 'username':'root', 'password':'123'},
               {'ip':'192.168.98.131', 'port':22, 'username':'root', 'password':'123'},)
    
    ssh = paramiko.SSHClient()
    # 设置为接受不在known_hosts 列表的主机可以进行ssh连接
    ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
    
    for host in host_list:
        ssh.connect(hostname=host['ip'], port=host['port'], username=host['username'], password=host['password'])
        print(host['ip'])
        stdin, stdout, stderr = ssh.exec_command('cat /proc/stat | grep "cpu "')
        str_out = stdout.read().decode()
        str_err = stderr.read().decode()
    
        if str_err != "":
            print(str_err)
            continue
        else:
            cpu_time_list = re.findall('\d+', str_out)
            cpu_idle1 = cpu_time_list[3]
            total_cpu_time1 = 0
            for t in cpu_time_list:
                total_cpu_time1 = total_cpu_time1 + int(t)
    
        time.sleep(2)
    
        stdin, stdout, stderr = ssh.exec_command('cat /proc/stat | grep "cpu "')
        str_out = stdout.read().decode()
        str_err = stderr.read().decode()
        if str_err != "":
            print(str_err)
            continue
        else:
            cpu_time_list = re.findall('\d+', str_out)
            cpu_idle2 = cpu_time_list[3]
            total_cpu_time2 = 0
            for t in cpu_time_list:
                total_cpu_time2 = total_cpu_time2 + int(t)
    
        cpu_usage = round(1 - (float(cpu_idle2) - float(cpu_idle1)) / (total_cpu_time2 - total_cpu_time1), 2)
        print('当前CPU使用率为:' + str(cpu_usage))
    
        
        ssh.close()
    

    3、磁盘使用率

    这里使用df命令获取磁盘使用情况。这个命令相当方便,既获取了磁盘的容量,也获取了使用率,可以根据需要进行后续的处理

    import paramiko
    import re
    
    #设置主机列表
    host_list=({'ip':'192.168.98.130', 'port':22, 'username':'root', 'password':'123'},
               {'ip':'192.168.98.131', 'port':22, 'username':'root', 'password':'123'},)
    
    ssh = paramiko.SSHClient()
    # 设置为接受不在known_hosts 列表的主机可以进行ssh连接
    ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
    
    for host in host_list:
        ssh.connect(hostname=host['ip'], port=host['port'], username=host['username'], password=host['password'])
        print(host['ip'])
        stdin, stdout, stderr = ssh.exec_command('df -lm')
        str_out = stdout.read().decode()
        str_err = stderr.read().decode()
        
        if str_err != "":
            print(str_err)
            continue
    
        print(str_out)
    
        ssh.close()
    

    4、网络流量

    网络流量可以使用cat /proc/net/dev查看,可以看到每个网络接口当前发送和接收的字节和包的数量,由于是一个累计的值,如果需要计算一定时间间隔内的流量,可以让程序sleep一定时间,然后再次获取,进行计算。个人建议每隔一段时间获取这个值,并且写入文件中,然后再使用其他程序去计算其速度和流量

    import paramiko
    import re
    
    #设置主机列表
    host_list=({'ip':'192.168.98.130', 'port':22, 'username':'root', 'password':'123'},
               {'ip':'192.168.98.131', 'port':22, 'username':'root', 'password':'123'},)
    
    ssh = paramiko.SSHClient()
    # 设置为接受不在known_hosts 列表的主机可以进行ssh连接
    ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
    
    for host in host_list:
        ssh.connect(hostname=host['ip'], port=host['port'], username=host['username'], password=host['password'])
        print(host['ip'])
        stdin, stdout, stderr = ssh.exec_command('cat /proc/net/dev')
        str_out = stdout.read().decode()
        str_err = stderr.read().decode()
        
        if str_err != "":
            print(str_err)
            continue
    
        print(str_out)
    
        ssh.close()
    


    展开全文
  • 但因为内存空间是连续的,所以在进行插入和删除操作时,会造成内存块的拷贝,时间复杂度为o(n)。另外,当数组中内存空间不够时,会重新申请一块内存空间并进行内存拷贝; list数据结构:list是由双向链表实现的,...

    以下为牛客网C/C++专项刷题:

     

    1、阅读以下程序,当输入数据的形式为12a345b789↙,正确的输出结果为()。

    void main()
    {
        char c1,c2;
        int a1,a2;
    
        c1=getchar();
        scanf("%2d",&a1);
        c2=getchar();
        scanf("%3d",&a2);
        printf ("%d,%d,%c,%c\n",a1,a2,c1,c2);
    }

    KEY:2,345,1,a

    解答:这里的"%2d"表示向缓冲区中放入2个输入。如果放入的输入都符合输入的条件,那么都一起接受了;如果放入的输入不符合输入的条件,那么也不丢弃掉,看接下来的输入条件。

     

    2、Math.round(11.5) 等于()。

    KEY:12

    解答:取近似值,如果有两个数距离它的距离相等,那么就取较大的那个值。

     

    3、如下程序:

    #include <iostream>
    using namespace std;
     
    class A
    {
    public:
        A()
        {
            printf("A");
        }
    };
     
    int main()
    {
        A *p1 = new A;
        A *p2 = (A *)malloc(sizeof(A));
         
        return 0;
    }

    该程序运行结果为()。

    KEY:A

    解答:这题主要是考的new和malloc的区别,new会分配内存,并调用类的构造函数创建对象,而malloc只是分配内存,不调用类的构造函数创建对象。同时这个程序应该用delete p1;free(p2);。

     

    4、以下程序用来统计文件中字符的个数(函数feof 用以检查文件是否结束,结束是返回非零):

    #include<stdio.h>
    
    main()
    { 
        FILE *fp; 
        long num=0;
        fp=fopen("fname.dat", "r" );
        while (________) 
        { 
            fgetc( fp );
            num++ ;
        }
        printf( " num= % d\n",num);
        fclose( fp );
    }

    下面选项中,填入横线处不能得到正确结果的是?

    feof( fp )= =NULL

    !feof( fp )

    feof(fp)

    feof( fp ) == 0

    KEY:C

    解答:feof()函数,如果遇到文件结束,函数值为非零值,否则函数值为0。文件结束符EOF,Windows下为组合键Ctrl+Z,Unix/Linux下为组合键Ctrl+D。

     

    5、若有以下类W说明,则函数fConst的正确定义是(  )。

    class W
    {
        int a;
    
        public:
            void fConst(int&) const;
    };

    void W::fConst( int &k )const  { k = a; }

    void W::fConst( int &k )const  { k = a++; }

    void W::fConst( int &k )const  { cin >> a; }

    void W::fConst( int &k )const  { a = k; }

    KEY:A

    解答:这道题目考的是const关键字,类的方法后面加了const后,该方法的实现中不能修改类的成员。即不能修改类成员a选项中,只有选项A,没有修改类成员a的值。

    注意:const如此用法,可以在该成员函数中引用成员数据的值,但不能修改对象的数据成员,在函数体内只能调用const成员函数,不能调用其他成员函数。

     

    6、C++内存分配中说法错误的是:______。

    对于栈来讲,生长方向是向上的,也就是向着内存地址增加的方向

    对于堆,大量的 new/delete 操作会造成内存空间的不连续

    堆容易产生 memory leak(内存泄漏)

    堆的效率比栈要低得多

    栈变量引用容易逃逸

    栈区一般由编译器自动分配释放,堆区一般由程序员分配释放

    KEY:A

    解答:栈:在Windows下,栈是向低地址扩展的数据结构,是一块连续的内存的区域。这句话的意思是栈顶的地址和栈的最大容量是系统预先规定好的,在WINDOWS下,栈的大小是2M(也有的说是1M,总之是一个编译时就确定的常数),如果申请的空间超过栈的剩余空间时,将 提示overflow。因此,能从栈获得的空间较小;

    堆:堆是向高地址扩展的数据结构,是不连续的内存区域。这是由于系统是用链表来存储的空闲内存地址的,自然是不连续的,而链表的遍历方向是由低地址向高地址。堆的大小受限于计算机系统中有效的虚拟内存。由此可见,堆获得的空间比较灵活,也比较大。 

     

    7、函数func的定义如下:

    void func(const int& v1, const int& v2)
    {
        std::cout << v1 << ' ';
        std::cout << v2 << ' ';
    }

    以下代码在vs中输出结果为____。

    int main (int argc, char* argv[])
    {
        int i=0;
        func(++i,i++);
        return 0;
    }

    0 1

    1 2

    2 1

    2 0

    KEY:D

    解答:在C语言中,函数的参数是从右到左的顺序压入到栈的,函数执行时再从栈弹出弹出来执行。

    参数i先入栈0,然后执行i++此时i为1,接着参数i先执行++i,i此时为2,后入栈进行输出2 0。

     

    8、下面有关回调函数的说法,错误的是?

    回调函数就是一个通过函数指针调用的函数

    回调函数可能被系统API调用一次,也可能被循环调用多次

    回调函数本身可以是全局函数 ,静态函数和某个特定的类的成员函数

    回调函数可用于通知机制

    KEY:C

    解答:所谓的回调函数,就是预先在系统的对函数进行注册,让系统知道这个函数的存在,以后,当某个事件发生时,再调用这个函数对事件进行响应。 定义一个类的成员函数时在该函数前加CALLBACK即将其定义为回调函数,函数的实现和普通成员函数没有区别。

    回调函数就是一个通过函数指针调用的函数。如果你把函数的指针(地址)作为参数传递给另一个函数,当这个指针被用来调用其所指向的函数时,我们就说这是回调函数。回调函数不是由该函数的实现方直接调用,而是在特定的事件或条件发生时由另外的一方调用的,用于对该事件或条件进行响应。

    类的成员函数需要隐含的this指针,而回调函数没有办法提供。

     

    9、下列情况中,不会调用拷贝构造函数的是()

    用一个对象去初始化同一个类的另一个新对象时

    将类的一个对象赋值给该类的另一个对象时

    函数的形参对象,调用函数进行形参和实参结合时

    函数的返回值是类的对象,函数执行返回调用时

    KEY:B

    解答:关于拷贝构造函数的意义:

    class Base{
        public:
            Base(){};        //构造函数
            Base(int i)();        //构造函数
            Base(Base &b){};        //拷贝构造函数
    };
    
    Base w1;     //构造函数
    Base w2(w1);   //拷贝构造函数
    Base w3 = w1; //拷贝构造函数 ,与上式等价
    w1 = w2;     //赋值运算符
    
    Base w4(100);        //构造函数
    Base w5 = 100;    //构造函数,与上式等价
    w5 = 200;        //构造函数,实现了类型转换功能

    可知:将类的一个对象赋值给该类的另一个对象时,是赋值运算,不是构造函数。

     

    10、函数外部访问x等于什么?

    enum string{    
        x1,    
        x2,    
        x3=10,    
        x4,    
        x5,    
    } x;

    5

    12

    0

    随机值

    KEY:C

    解答:在函数外部定义,为全局变量,默认为0;若在函数内部定义,则在编译时会显示变量未初始化,是一个随机值。

     

    11、若变量已正确的定义为float类型,要通过输入函数scanf(“%f%f%f”,&a,&b,&c)给a赋值10,b赋值22,c赋值33,以下不正确的输入形式是()。

    10  22  33

    10.0,22.0,33.0

    KEY:B

    解答:空格、换行和制表符都可以用来分割,但是逗号不行。

     

    12、若 a 是 float 型变量,b 是 unsigned 型变量,以下输入语句中合法的是()。

    scanf("%6.2f%d",&a,&b);

    scanf("%f%n",&a,&b);

    scanf("%f%3o",&a,&b);

    scanf("%f%f",&a,&b);

    KEY:BC

    解答:%n用于接受一个uint,代表到%n为止所输入的字符数,其本身不消耗字符。也就是说,此时的b不是命令行输入进去的,而是自动生成的!命令行输入一个float数,然后将这个数赋值给a,同时将这个数的字符数赋值给b。

    而u、o、x分别代表10、8、16进制的无符号数输入。

     

    13、两个线程并发执行以下代码,假设a是全局变量,那么以下输出___哪个是可能的?

    int a=1;
    void foo(){
        ++a;
        printf("%d",a);
    }

    3 2

    2 3

    3 3

    2 2

    KEY:ABCD

    解答:选项D是,假设线程A先执行++a操作但没有写回到内存,这时线程B执行++a操作写回内存并printf,输出2,线程A继续执行,++a操作写回内存,a的值保持2,再printf。

    这里关键有两点: 

    • 两个线程可随时被抢占 ;
    • ++a和printf不是原子指令,可随时被打断;特别注意函数printf,a作为参数压栈后,a再变化则不会影响输出(printf实际打印的是压栈的参数,是值拷贝的栈变量)。

     

    14、vector和list的区别:

    • vector数据结构:vector和数组类似,拥有一段连续的内存空间,并且起始地址不变。因此能高效的进行随机存取,时间复杂度为o(1)。但因为内存空间是连续的,所以在进行插入和删除操作时,会造成内存块的拷贝,时间复杂度为o(n)。另外,当数组中内存空间不够时,会重新申请一块内存空间并进行内存拷贝;
    • list数据结构:list是由双向链表实现的,因此内存空间是不连续的。只能通过指针访问数据,所以list的随机存取非常没有效率,时间复杂度为o(n)。但由于链表的特点,能高效地进行插入和删除。

    在对它们的迭代器iterator进行讨论:

    • vector拥有一段连续的内存空间,能很好的支持随机存取,因此vector<int>::iterator支持“+”,“+=”,“<”,“[]”等操作符。
    • list的内存空间可以是不连续,它不支持随机访问,因此list<int>::iterator则不支持“+”、“+=”、“<”,“[]”等
    • vector<int>::iterator和list<int>::iterator都重载了“++”运算符。

    总之,如果需要高效的随机存取,而不在乎插入和删除的效率,使用vector;如果需要大量的插入和删除,而不关心随机存取,则应使用list。

     

    15、代码生成阶段的主要任务是:

    把高级语言翻译成机器语言

    把高级语言翻译成汇编语言

    把中间代码变换成依赖具体机器的目标代码

    把汇编语言翻译成机器语言

    KEY:C

    解答:编译的过程为:扫描(词法分析)、语法分析、语义分析、源代码优化、代码生成和目标代码优化。

    源码 ->(扫描)-> 标记 ->(语法分析)-> 语法树 ->(语义分析)-> 标识语义后的语法树 ->(源码优化)-> 中间代码 ->(代码生成)-> 目标机器代码 ->(目标代码优化)-> 最终目标代码。

    参考文章:谈谈c语言程序如何变成可执行文件

     

    16、以下程序的打印结果是()。

    #include<iostream>
    using namespace std;
    
    void swap_int(int a , int b)
    {
        int temp = a;
        a = b;
        b = temp;
    }
    
    void swap_str(char* a , char* b)
    {
        char* temp = a;
        a = b;
        b = temp;
    }
    
    int main(void)
    {
        int a = 10;
        int b = 5;
        char* str_a = "hello world";
        char* str_b = "world hello";
    
        swap_int(a , b);.
        swap_str(str_a , str_b);
        printf("%d %d %s %s\n",a,b,str_a,str_b);
    
        return 0;
    }

    KEY:10 5 hello world world hello

    解答:要交换字符串,需要将两个char*的地址发送过去,而不是发送一个char的地址过去!其实两个交换函数都犯得相同的一个错误,在函数中形成了局部变量。正确的字符串交换的方法为:

    void swap_str(char **a , char **b)
    {
        char *temp = *a;
        *a = *b;
        *b = temp;
    }

    或者,还是使用引用:

    void swap_str(char* &a , char* &b)
    {
        char *temp = a;
        a = b;
        b = temp;
    }

     

    17、如下程序用于输出“Welcome to Huawei Test”,请指出其中的两处错误。

    char * GetWelcome(void){
        char * pcWelcome;
        char * pcNewWelcome;
        pcWelcome="Welcome to Huawei Test";
        pcNewWelcome=(char *)malloc(strlen(pcWelcome));    //1
        if(NULL==pcNewWelcome){
            return NULL;        //2
        }
        strcpy(pcNewWelcome, pcWelcome);    //3
        return pcNewWelcome;            //4
    }
    

    KEY:1 3

    解答:1处,正确形式是:pcNewWelcome=(char*)malloc(strlen(pcWelcome)*sizeof(char)) ,当然char的大小为1,形式上可以算是对了。但是逻辑上呢?strlen()统计字符个数,不含结尾符'\0',所以这样子分配会少一个字节。3处,由1处,既然新分配的空间少了一个字节,你用原来的来复制到新的里?能装下吗?不能。

    同样看一下strlen和sizeof的区别:

    char *a = "hello";
    char b[] = "hello";
    cout << sizeof(a) << ' ' << strlen(a) << endl;
    cout << sizeof(b) << ' ' << strlen(b) << endl;

    这个的答案是4 5 6 5。需要注意的是sizeof(a)判断的是一个指针的大小,不是字符串的大小!需要使用数组才行。

     

    18、编译运行如下程序会出现什么结果?

    #include <iostream>
    using namespace std;
    
    class A
    {
        A()
        {
            printf("A()");
        }
    };
    
    void main()
    {
        A a;
    }

    A()

    编译错误

    链接错误

    以上都不对

    KEY:B

    解答:在类中,其成员的缺省的存取权限是私有的;而在结构体类型中,其成员的缺省的存取权限是公有的。也就是说,使用一个私有的构造函数进行初始化。

     

    19、下面代码的输出是什么?

    class A  
    {  
    public:  
        A()  {     }  
        ~A() {    cout<<"~A"<<endl;   }  
    };  
       
    class B:public A  
    {  
        public:  
            B(A &a):_a(a)  
            {  
                 
            }  
            ~B()  
            {  
                cout<<"~B"<<endl;  
            }  
        private:  
            A _a;  
        };  
           
    int main(void)  
     {  
            A a;
            B b(a); 
    }

    KEY:~B ~A ~A ~A

    解答:对象成员的构造函数、基类构造函数、派生类构造函数的调用顺序:先调用基类的构造函数,再调用对象成员的构造函数,最后执行派生类的构造函数。

    • 多个基类的条件下:取决于在类继承中说明的顺序,与它们在构造函数的初始化成员列表的先后顺序无关;
    • 多个对象成员的条件下:取决于它们在派生类中说明的顺序。

    本题:A a;,调用一次构造函数。B b(a);,先基类的构造函数,再对象成员的构造函数,再派生类的构造函数。析构的时候,顺序正好相反。

     

    20、关于CSingleLock,下面说法错误的是?

    主要是同步多个线程对一个数据类的同时访问。

    CSingleLock有RAII的好处。

    CSingleLock对象需要有一个从CSyncObject派生的对象存在。

    CSingleLock必须要全部显示的进行unLock操作

    KEY:D

    解答:RAII(Resource Acquisition Is Initialization)是一种利用对象生命周期来控制程序资源(如内存、文件句柄、网络连接、互斥量等等)的简单技术。 RAII的一般做法是这样的:在对象构造时获取资源,接着控制对资源的访问使之在对象的生命周期内始终保持有效,最后在对象析构的时候释放资源。借此,我们实际上把管理一份资源的责任托管给了一个对象。这种做法有两大好处: 不需要显式地释放资源。 采用这种方式,对象所需的资源在其生命期内始终保持有效。

    CSingleLock主要是同步多个线程对一个数据类的同时访问。在创建CSingleLock对象时类对象会自动根据参数赋值,而且会lock,不用显式lock,即,只需创建对象就可lock。CSinglelock退出时自动调用析构函数,析构时自动unlock。

     

    21、不考虑任何编译器优化(如:NRVO),下述代码的第10行会发生:

    #include <stdio.h>                       //1
    
    class B                                  //2
    {                                        //3
    };                                       //4
    
    B func(const B& rhs){                    //5
        return rhs;                          //6
    }                                        //7
    
    int main(int argc, char **argv){         //8
        B b1,b2;                             //9
        b2=func(b1);                         //10
    }                                        //11

    一次默认构造函数,一次拷贝构造函数,一次析构函数,一次(拷贝赋值运算符)operator=

    二次拷贝构造函数,一次析构函数

    一次(拷贝赋值运算符)operator=,一次析构函数

    一次拷贝构造函数,一次析构函数,一次(拷贝赋值运算符)operator=

    KEY:D

    解答:答案是D。但是次序并不是,正确的顺序是一次拷贝构造函数,一次(拷贝赋值运算符)operator= ,一次析构函数。

    一次拷贝构造函数发生在func函数调用完成,返回B类型的对象时,因为返回的不是引用类型,所以会生成一个对象,不妨称为TEMP,将返回的对象通过拷贝构造函数复制给TEMP,同时由于拷贝构造函数的参数是const B&,rhs并不会在函数结束时候被析构,这时并不调用析构函数(也就是return这一句,由b&的引用类型,来生成TEMP);

    赋值运算符在func函数执行完成后,将上面提到的TEMP,通过赋值运算符赋值给b2;

    表达式的最后将临时对象TEMP进行析构。

     

    22、下面程序的输出结果是:

    char *p1= “123”, *p2 = “ABC”, str[50]= "xyz";
    strcpy(str+2,strcat(p1,p2));
    cout << str;

    xyz123ABC

    z123ABC

    xy123ABC

    出错

    KEY:D

    解答:首先,c++的内存分配地址有小到大分别是:动态内存区,包括栈和堆;代码内存区;静态内存区,包括全局变量,静态变量,只读变量(就是常量),按照定义的先后顺序分配地址;

    本题中,p1,p2都是指针,是局部变量,在分配内存时会将其分配到栈区,且只会分配p本身所需要的内存空间,即p所指向的内容的地址大小的空间,并没有为字符串分配内存,所以*p1="123"和*p2="ABC"中 123和ABC都被分配在了文字常量区,既然是常量,大小就是不可变的,而strcat的第一个变量必须是可变的,所以程序会出错。

    也就是说:

    char* strcat(char *,const char*);    //第一个参数所指向的内容必须可以修改,可以赋值为在栈上分配的数组
    strcat(p1,p2);        //试图修改p1的内容,p1指向文字常量区,其指向的内容无法修改

     

    23、如果x=2014,下面函数的返回值是()。

    int fun(unsigned int x)
    {
         int n=0;
         while((x+1))
         {
             n++;
             x=x|(x+1);
         }
         return n;
    }

    KEY:C

    解答:x&(x-1)统计1的个数,x|(x+1)统计0的个数。

    2014对应的二进制为:0000 0000 000 0000 0000 0111 1101 1110

    • x|(x+1)的结束终止条件:直到变成全1的时候x+1就溢出为全0,循环结束;
    • x&(x-1)的结束终止条件:直到变成全0的时候,循环结束。

     

    24、若有说明:int *p,m=5,n;以下正确的程序段是()。

    p=&n;scanf("%d",&p);

    p=&n;scanf("%d",*p)

    scanf("%d",&n);*p=n;

    p=&n;*p=m;

    KEY:D

    解答:scanf的一般格式下,放入的应该为地址,也就是说,正确的写法应该是:

    int i;
    scanf("%d", &i);
    
    int* p=&i;
    scanf("%d", p);

     

    25、下列表达式正确的是:

    9++

    (x+y)++

    c+++c+++c++

    ++(a-b--)

    KEY:C

    解答:a++表达式的本质是编译器翻译成a=a+1。

    那么:A、9=9+1?;B、x+y=x+y+1? C++左值表达式不能有运算符;D、选项与B类似。也就是主要考的就是,C++的等号左边不能有运算符!

     

    26、下面程序运行结果为()。

    void main()
    {
        char c=’a’;
     
        if ('a'<c<='z') 
            printf ("Low”);
        else 
            printf("UP");
    }

    LOW

    UP

    LOWUP

    程序语法错误

    KEY:A

    解答:尽管我们都知道表示一个数是否处于一个范围之间时,这样使用时达不到效果的。但是,这句话本身是没有语法错误的。不要突然就脑子转不过来弯了。

     

    27、下面代码会输出什么?()

    class A{
        public:
            int m;
            void print()  { cout << "A\n"; }
    };
    
    A *p = 0;
    p->print();

    KEY:A

    解答:初始化为NULL(或者0)的类指针可以安全的调用不涉及类成员变量的类成员函数而不出错,但是如果类成员函数中调用了类成员变量则会出错。

    原因:调用成员函数的时候,函数地址是编译期间确定的,成员函数不通过对象指针去调用,对象指针仅仅作为参数传入函数然后去调用成员变量。而如果是虚函数,需要通过this去计算vptr,然后找到vtable,而this为NULL,那么就会报错。

     

    28、C语言和C++的默认缺省大比较:

    返回值方面

    • C中:如果函数未指定返回值类型,则默认为int;
    • c++中:如果一个函数没有返回值,返回值类型必须指定为void。

    参数列表方面

    • C中:如果函数没有指定参数列表,则默认可以接受任意多个参数;
    • C++中:有严格的类型检测,没有参数列表的函数默认为void,不接受任意参数。

     

    29、看以下代码:

    A *pa = new A[10];
    delete pa;

    则类A的构造函数和析构函数分别执行了几次()。

    KEY:10 1

    解答:应该改成 delete[] pa;才对,这样会调用10次A的构造函数和10次A的析构函数。

    但是这还没有完。如果A是简单类型,比如 int ,char ,double 或者结构体,那么动态创建简单类型的数组,是可以调用delete pa;这样来析构的,效果是和 delete[] pa的效果一样,不会报错。

    但是如果,A是自己定义的一个类,那么动态创建了对象数组,用delete pa;就会使程序崩溃。因为动态创建一个对象的内存实际上比A要大,有一些附加的内存需要存放附加的信息。

     

    30、若 ch 为 char 型变量,k 为 int 型变量(已知字符 a 的 ASCII 十进制代码为97),则以下程序段的执行结果是()。

    char ch='a';
    int k=12;
    printf("%x,%o,", ch, ch, k);
    printf("k=%%d\n", k);

    因变量类型与格式描述符的类型不匹配,输出无定值

    输出项与格式描述符个数不符,输出为零值或不定值

    61,141,k=%d

    61,141,k=%12

    KEY:C

    解答:若输出项多于格式描述符的个数,输出输出项中对应格式描述符的内容,剩余输出项则丢弃;若输出项少于格式描述符的个数,输出输出项中对应格式描述符的内容,缺少的输出项则输出不定值。

    printf("%x,%o,", ch, ch, k);    //将ch以16进制输出为61,八进制为141,k参数被忽略。
    printf("k=%%d\n", k);           // %是控制符,用 %% 表示输出一个百分号

     

    31、switch后面的“表达式”,可以是int、char和枚举型中的一种,不能是float型变量;同时,case后面必须是“整型常量表达式”,表达式中不能包含变量。

    注意:字符型常量也是属于整型常量表达式!

     

    32、下列关于数组与指针的区别描述正确的是?

    数组要么在静态存储区被创建(如全局数组),要么在栈上被创建。

    用运算符sizeof 可以计算出数组的容量(字节数)

    指针可以随时指向任意类型的内存块。

    用运算符sizeof 可以计算出指针所指向内容的容量(字节数)

    KEY:B

    解答:A.堆上创建动态数组;B.sizeof(数组名)就是数组的容量;C.const指针不可以;D. char* str = "hello"; sizeof(str)不能计算出内容的容量,只是指针的容量。

     

    33、下面哪种情况下,B不能隐式转换为A?

    class B:public A{}

    class A:public B{}

    class B { operator A(); }

    class A { A(const B&); }

    KEY:B

    解答:答案A,表示A是基类,B是派生类,向上级类型转换是隐式的,因为部分元素丢弃可以自动完成,向下转型是显式的因为不知道应该增加的值是什么。所以B不能。

    答案C,转换函数(又称为类型转换函数)。

    答案D,拷贝构造函数, B b = A 肯定是可以的。

    转换函数(又称为类型转换函数)是类中定义的一个成员函数,其一般格式为:

    类名::operator 转换后的类型 (){        //将“类名”类型转换为“转换后的类型”
        ...
    }

    其中:operator和“转换后的类型”一起构成转换函数名。该函数不能带有参数,也不能指定返回值类型。因为它的返回值类型就是“转换后的类型”。转换函数的作用就是将对象内的成员数据转换成某种特定的类型。

     

    34、C++语言本身没有输入输出语句。说法是否正确?

    正确

    错误

    KEY:A

    解答:输入和输出并不是C++语言中的正式组成成分。C和C++本身都没有为输入和输出提供专门的语句结构。输入输出不是由C++本身定义的,而是在编译系统提供的I/O库中定义的。 C++的输出和输入是用“流”(stream)的方式实现的。

     

    35、下面有关C++中为什么用模板类的原因,描述错误的是?

    可用来创建动态增长和减小的数据结构

    它是类型无关的,因此具有很高的可复用性

    它运行时检查数据类型,保证了类型安全

    它是平台无关的,可移植性

    KEY:C

    解答:(1)可用来创建动态增长和减小的数据结构 (2)它是类型无关的,因此具有很高的可复用性。 (3)它在编译时而不是运行时检查数据类型,保证了类型安全 (4)它是平台无关的,可移植性 (5)可用于基本数据类型。

     

    36、以下哪些做法是不正确或者应该极力避免的:【多选】( )

    构造函数声明为虚函数

    派生关系中的基类析构函数声明为虚函数

    构造函数中调用虚函数

    析构函数中调用虚函数

    KEY:ACD

    解答:构造函数和析构函数都不能调用虚函数:

    先析构子类再析构父类,如果父类析构函数有虚函数,会导致调用子类的已经析构的内容。先构造父亲类再构造子类,如果父类构造函数有虚函数,会导致调用子类还没构造的内容。

    构造函数不能为虚函数:

    虚函数对应一个vtable,可是这个vtable其实是存储在对象的内存空间的。问题出来了,如果构造函数是虚的,就需要通过 vtable来调用,可是对象还没有实例化,也就是内存空间还没有,无法找到vtable,所以构造函数不能是虚函数。

    基类的析构函数要定义为虚函数:

    我们往往通过基类的指针来销毁对象。这时候如果析构函数不是虚函数,就不能正确识别对象类型从而不能正确调用析构函数。也就是说,基类的虚构函数如果不声明成为虚函数,那么销毁派生类时有可能造成资源泄漏,出现指向基类的派生类指针在销毁时,只能销毁基类对象而无法销毁派生类对象的现象。

    参考文章:析构函数可以为virtual,构造函数则不能。原因?为什么构造函数不能为虚函数,而析构函数可以为虚函数

     

    37、关于以下代码,哪个说法是正确的?

    myClass::foo(){
        delete this;
    }
    
    void func(){
        myClass *a = new myClass();
        a->foo();
    }

    它会引起栈溢出

    都不正确

    它不能编译

    它会引起段错误

    KEY:B

    解答:在类的成员函数中能不能调用delete this?答案是肯定的,能调用。那么这个对象在调用release方法后,还能进行其他操作,如调用该对象的其他方法么?答案仍然是肯定 的,调用release之后还能调用其他的方法,但是有个前提:被调用的方法不涉及这个对象的数据成员和虚函数。

    根本原因在于delete操作符的功能和类对象的内存模型。当一个类对象声明时,系统会为其分配内存空间。在类对象的内存空间中,只有数据成员和虚函数表指针,并不包含代码内容,类的成员函数单独放在代码段中。在调用成员函数时,隐含传递一个this指针,让成员函数知道当前是哪个对象在调用它。当调用delete this时,类对象的内存空间被释放。在delete this之后进行的其他任何函数调用,只要不涉及到this指针的内容,都能够正常运行。一旦涉及到this指针,如操作数据成员,调用虚函数等,就会出现不可预期的问题。

    大致明白在成员函数中调用delete this会发生什么之后,再来看看另一个问题,如果在类的析构函数中调用delete this,会发生什么?实验告诉我们,会导致堆栈溢出。原因很简单,delete的本质是“为将被释放的内存调用一个或多个析构函数,然后,释放内存” (来自effective c++)。显然,delete this会去调用本对象的析构函数,而析构函数中又调用delete this,形成无限递归,造成堆栈溢出,系统崩溃。

     

    38、下列格式控制符,既可以用于输入,又可以用于输出的是(  )。

    setbase

    setfill

    setprecision

    setw

    KEY:A

    解答:C++提供了许多的预定义格式控制函数,可直接用于控制输入/输出数据的格式,如下表:

    C++中预定义的格式控制函数
    格式控制函数名功能适用于输入/输出流
    dec设置为十进制IO
    hex设置为十六进制IO
    oct设置为八进制IO
    ws提取空白字符I
    endl插入一个换行符O
    flush刷新流O
    resetioflags(long)取消指定的标志IO
    setioflags(long)设置指定的标志IO
    setbase(int)将数字转换为 n 进制IO
    setfill(int)设置填充字符O
    setprecision(int)设置实数的精度O
    setw(int)设置宽度O
    ends插入一个表示字符串结束的字符 

    这些格式控制函数的标头都是:<iomanip>,std命令空间。

     

    39、请问运行Test 函数会有什么样的结果?

    char* getmemory(void)
    {
        char p[]="hello world";
        return p;
    }
    
    void test(void)
    {
        char *str=NULL;
        str=getmemory();
        printf(str);
    }

    出错

    输出"hello world"

    输出空""

    输出乱码

    KEY:D

    解答:getmemory,返回的指针,是内部变量(动态数据区),调用之后会被回收。 所以输出是不确定的。

    假如修改为:

    char* getmemory(void)
    {
    	char *p = "hello world";
    	return p;
    }
    
    void test(void)
    {
    	char *str = NULL;
    	str = getmemory();
    	printf(str);
    }

    就可以输出"hello world"啦!

    返回“字符串常量的指针”和“返回数组名”的区别在于,一个返回常量区的地址,一个返回栈内存(动态数据区)的地址。动态数据区的内容出了getmemory()函数范围就被释放了,但常量区则不会被释放。

     

    40、注意区分一下指针函数和函数指针:

    • char *p (): 是指针函数,函数;
    • char (*p) ():是函数指针,指针。

     

    41、若调用fputc函数输出字符成功,则其返回值是()。

    EOF

    1

    0

    输出的字符

    KEY:D

    解答:fputc函数有一个返回值,如写入成功则返回写入的字符, 否则返回一个EOF。可用此来判断写入是否成功。

     

    42、以下程序的输出结果是()。

    #include <string.h>
    #include <stdio.h>
    
    void main()
    {
        char a[80] = "AB", b[80] = "LMNP", i=0;
        strcat(a, b);
        while (a[i] != '\0')
        {
            i++;
            b[i] = a[i];
        }
        puts(b);
    }

    KEY:LBLMNP

    解答:主要是介绍一下C++string类的几个函数:

    string &append(const char *s);            //把c类型字符串s连接到当前字符串结尾
    string &append(const char *s,int n);        //把c类型字符串s的前n个字符连接到当前字符串结尾
    
    int compare(const string &s) const;        //比较当前字符串和s的大小
    int compare(int pos, int n,const string &s)const;        //比较当前字符串从pos开始的n个字符组成的字符串与s的大小
    
    string substr(int pos = 0,int n = npos) const;    //返回pos开始的n个字符组成的字符串
    
    void swap(string &s2);        //交换当前字符串与s2的值
    
    int find(char c, int pos = 0) const;        //从pos开始查找字符c在当前字符串的位置
    int find(const char *s, int pos = 0) const;        //从pos开始查找字符串s在当前串中的位置
    int find(const char *s, int pos, int n) const;        //从pos开始查找字符串s中前n个字符在当前串中的位置
    
    int rfind(char c, int pos = npos) const;        //从pos开始从后向前查找字符c在当前串中的位置
    int rfind(const char *s, int pos = npos) const;
    int rfind(const char *s, int pos, int n = npos) const;
    
    string &replace(int p0, int n0,const char *s);    //删除从p0开始的n0个字符,然后在p0处插入串s
    string &replace(int p0, int n0,const char *s, int n);    //删除p0开始的n0个字符,然后在p0处插入字符串s的前n个字符
    
    string &insert(int p0, const char *s);        //在p0位置插入字符串s
    string &insert(int p0, const char *s, int n);    //在p0位置插入字符串s中pos开始的前n个字符
    
    string &erase(int pos = 0, int n = npos);        //删除pos开始的n个字符,返回修改后的字符串

    再介绍一下C语言标准库<string.h>的几个函数:

    void *memchr(const void *str, int c, size_t n);        //在参数 str 所指向的字符串的前 n 个字节中搜索第一次出现字符 c(一个无符号字符)的位置。
    
    int memcmp(const void *str1, const void *str2, size_t n);        //把 str1 和 str2 的前 n 个字节进行比较。
    
    void *memcpy(void *dest, const void *src, size_t n);        //从 src 复制 n 个字符到 dest。
    
    void *memmove(void *dest, const void *src, size_t n);        //另一个用于从 src 复制 n 个字符到 dest 的函数。
    
    void *memset(void *str, int c, size_t n);        //复制字符 c(一个无符号字符)到参数 str 所指向的字符串的前 n 个字符。
    
    char *strchr(const char *str, int c);        //在参数 str 所指向的字符串中搜索第一次出现字符 c(一个无符号字符)的位置。
    char *strrchr(const char *str, int c);        //在参数 str 所指向的字符串中搜索最后一次出现字符 c(一个无符号字符)的位置。
    
    char *strcat(char *dest, const char *src);        //把 src 所指向的字符串追加到 dest 所指向的字符串的结尾。
    char *strncat(char *dest, const char *src, size_t n);        //把 src 所指向的字符串追加到 dest 所指向的字符串的结尾,直到 n 字符长度为止。
    
    int strcmp(const char *str1, const char *str2);        //把 str1 所指向的字符串和 str2 所指向的字符串进行比较。
    int strncmp(const char *str1, const char *str2, size_t n);        //把 str1 和 str2 进行比较,最多比较前 n 个字节。
    
    char *strcpy(char *dest, const char *src);        //把 src 所指向的字符串复制到 dest。
    char *strncpy(char *dest, const char *src, size_t n);        //把 src 所指向的字符串复制到 dest,最多复制 n 个字符。
    
    size_t strlen(const char *str);        //计算字符串 str 的长度,直到空结束字符,但不包括空结束字符

     

    43、在16位IBM-PC上使用C语言,若有如下定义:

    struct data 
    { 
        int i; 
        char ch;
        double f;
    } b;

    则结构变量b占用内存的字节数是()。

    1

    2

    8

    11

    KEY:D

    解答:16位下,int 2字节,char 1字节,double 8字节。这个版本的编译器太旧了,没有内存对齐。

     

    44、以下描述错误的是:

    函数的形参在函数未调用时不分配存贮空间

    若函数的定义出现在主函数之前且仅被主函数使用,则可以不必再说明

    若一个函数(非主函数)没有return语句,返回类型是void

    一般来说,函数的形参和实参的类型应该一致

    KEY:C

    解答:构造函数和析构函数都没有返回类型,也没有return语句。

     

    45、当一个类A 中没有声明任何成员变量与成员函数,这时sizeof(A)的值是多少?

    1

    0

    4

    运行时错误

    KEY:A

    解答:深度探索c++对象模型中是这样说的:那是被编译器插进去的一个char ,使得这个class的不同实体(object)在内存中配置独一无二的地址。也就是说这个char是用来标识类的不同对象的。

    再深究一下:

    class A {
    	
    };                            //sizeof(A)=1
    
    class A {
    	void fun() {};
    };                            //sizeof(A)=1
    
    class A {
    	virtual void fun() {};
    };                            //sizeof(A)=4

     

    46、以下哪项不属于STL container?(    )

    stack

    queue

    multimap

    string

    KEY:D

    解答:STL container分为三大类:

    • 序列容器:动态数组vector,双端队列deque(本质是动态数组加索引),链表list。
    • 关联容器:set,map,multiset,multimap,bitset(叫bit_array更合适)。
    • 容器适配器:stack,queue,priority_queue。

     

    47、执行"int x=1;int y=~x;"语句后,y的值为?

    1

    0

    -1

    -2

    KEY:D

    解答:x的补码为00000000 00000000 00000000 00000001,则y为11111111 11111111 11111111 11111110。化为源码就是:10000000 00000000 00000000 00000010,也就是-2。

     

    48、设有如下说明:

    typedef struct ST
    {
        long a;
        int  b;
        char  c[2];
    } NEW;

    则下面叙述中正确的是(  ) 。

    以上的说明形式非法

    ST是一个结构体类型

    NEW是一个结构体类型

    NEW是一个结构体变量

    KEY:C

    解答:经过typedef定义之后,struct ST和NEW等价。

     

    49、this作用域是在类内部,当在类的非静态成员函数中访问类的非静态成员的时候,编译器会自动将对象本身的地址作为一个隐含参数传递给函数。静态成员函数不拥有this指针。

    可以这么理解:对于一个类myclass,this指针的类型应该是myclass *,而对其的解引用*this就应该是一个myclass类型的变量。也就是说,this指针指向该类的实例对象,而不是该类本身。

     

    50、以下 C 语言指令:

    int a[5] = { 1,3,5,7,9 };
    int *p = (int *)(&a + 1);
    printf("%d, %d", *(a + 1) , *(p - 1));

    运行结果是什么?

    2,1

    3,1

    3,9

    运行时崩溃

    KEY:C

    解答:区分一下&a、a、&a[0]:

    • &a和a做右值时的区别:&a是整个数组的首地址,而a是数组首元素的首地址。这两个在数字上是相等的,但是意义不相同。意义不相同会导致他们在参与运算的时候有不同的表现;
    • a和&a[0]做右值时意义和数值完全相同,完全可以互相替代。

    &a表示一个指向大小为5数组的指针,那么(&a+1)就是表示一个指向大小为5的下一个数组的指针,也就是数组a最后一个元素的下一个位置,-1则指向a中最后一个元素;a是保存数组的首地址,*(a+1)保存的是数组第二个地址的值,故为3.p是保存数组a最后一个位置的下一个位置的地址,*p是指向数组a最后一个位置的下一个位置,值为-1,*(p-1)是指向数组a最后一个位置,值为9。

     

    展开全文
  •   Linux下怎样查看机器配置啊?cpu/内存/硬盘 dmesg 显示开机信息。kernel会将开机信息存储在ring buffer中。您若是开机时来不及查看信息,可利用dmesg来查看。开机信息亦保存在/var/log目录中,名称为dme
  • 使用链表结构可以克服数组链表需要预先知道数据大小的缺点,链表结构可以充分利用计算机内存空间,实现灵活的内存动态管理。但是链表失去了数组随机读取的优点,同时链表由于增加了结点的指针域,空间开销比较大。...
  • JavaScript中的垃圾回收和内存泄漏

    千次阅读 多人点赞 2019-04-30 09:13:36
    程序的运行需要内存。只要程序提出要求,操作系统或者运行时就必须供给内存。所谓的内存泄漏简单来说是不再用到的内存,没有及时释放。为了更好避免内存泄漏,我们先介绍Javascript垃圾回收机制。 在C与C++等语言中...
  • javascript中的内存管理

    千次阅读 2021-03-01 19:52:50
    在c语言中,我们需要手动分配和释放对象的内存,但是在java中,所有的内存管理都交给了java虚拟机,程序员不需要在手动进程内存的分配和释放,大大的减少了程序编写的难度。 同样的,在javascript中,内存管理也是...
  • 在进行性能测试时,肯定少不了对cpu和内存的状态进行监控:CPU使用率,内存使用率等,虽然Linux提供了很多命令iostat,top,free -m等等,但是在使用过程中并不方便。因此本人开发了这个脚本,可以很好的实时查看cpu...
  • 【C/C++】C语言特性总结

    万次阅读 多人点赞 2019-08-10 16:21:28
    常量 常量: 在程序运行过程中,其值不能被改变的量 常量一般出现在表达式或赋值语句中 整型常量 100,200,-100,0 实型常量 3.14 , 0.125,-3.123 字符型常量 ‘a’,‘b’,‘1’,‘\n’ 字符串常量 “a”,“ab”...
  • #内存泄露# linux常用内存相关命令

    万次阅读 2020-06-19 11:14:34
     free 命令会显示系统内存的使用情况,包括物理内存、交换内存(swap)和内核缓冲区内存等。 $ free total used free shared buff/cache available Mem: 8047480 6142828 154116 465584 1750536 1078080 Swap: ...
  • IO端口和IO内存的区别及分别使用的函数接口   每个外设都是通过读写其寄存器来控制的。外设寄存器也称为I/O端口,通常包括:控制寄存器、状态寄存器和数据寄存器三大类。根据访问外设寄存器的不同方式,可以把...
  • 在Android O上大面积的爆了以下这段trace,开始怀疑是出现了native内存泄漏问题,但经分析后发现是Android N和Android O在处理Bitmap的内存存储不同导致的问题,并不是内存泄漏~ trace如下(待补充): 内存申请 ...
  • 《C/C++面试200题》四年面试官精心整理

    千次阅读 多人点赞 2021-08-02 13:38:35
    int f(int n) { if (n) return f(n - 1) + n; return n; } 15 10 5 以上均不是 第二题 - 操作符 2、 以下哪个操作符不能重载 ( ( ( C ) ) ) 。 = > sizeof % 第三题 - 字符串 3、 程序char c[5]={'a','b','\0','c',...
  • 转自:https://blog.csdn.net/rebirthme/article/details/50402082想必在linux上写过程序的同学都有分析进程占用多少内存的经历,或者被问到这样的问题——你的程序在运行时占用了多少内存(物理内存)?通常我们可以...
  • 内存泄漏定位思路和方法

    千次阅读 2020-03-27 22:55:10
    本文主要针对基于Linux操作系统,提供了一种通用的内存泄漏定位分析思路和方法。 1. 查看内存概况 [root@VM_0_17_centos ~]# free total used free shared buff/cache available Mem: 1883844 376664 7...
  • 57-System V 共享内存-shmctl

    千次阅读 2017-01-12 20:19:41
    : 挂接和卸载共享内存(挂接 5 秒后,再执行 shmdt,然后退出)。 shmctl 函数的代码见 2.5 节。 2.1 创建内核对象 图1 创建内核对象,打印内核对象信息 2.2 设置内核对象 图2 ...
  • JavaScript,会在创建变量(对象,字符串等)时分配内存,并且在不再使用它们时“自动”释放内存,这个自动释放内存的过程称为垃圾回收。 因为自动垃圾回收机制的存在,让大多Javascript开发者感觉他们可以不关心...
  • http://tech.blogchina.com/123/2005-06-10/372941.html  Unix、Linux下常用监控和管理命令工具  检查编写的程序打开的文件数。  sysctl:显示(或设置)系统内核参数  sysctl -a ...
  • C\C++之字符常量与字符串常量在内存中占据的字节数 一、关于字符常量&字符串常量 二、字符串详解 三、易错实例详解 1、字符串常量"abc\n"包含几个字符? 2、字符串常量"abc\0de"包含几个字符,占据多少个字节? 3、...
  • 程序员需要了解的硬核知识之内存

    千次阅读 多人点赞 2019-10-28 13:03:02
    我们都知道,计算机是处理数据的设备,而数据的主要存储位置就是磁盘和内存,并且对于程序员来讲,CPU 和内存是我们必须了解的两个物理结构,它是你通向高阶程序员很重要的桥梁,那么本篇文章我们就来介绍一下基本的...
  • /*动态数组解决了静态数组对内存空间的耗用问题,同时由于其使用的是指针,所以 在多函数调用上快速、便捷。 用malloc和指针定义动态数组,其关键是要准确理解指针的含义和数组的操作手段。 此程序的功能是用动态...
  • 2018秋招C/C++面试题总结

    万次阅读 多人点赞 2018-09-22 18:57:43
    //"123\0"在常量区,编译器 可能 会优化为和p3的指向同一块区域 } C/C++内存分配有三种方式: (1)从静态存储区域分配。内存在程序编译的时候就已经分配好,这块内存在程序的整个运行期间都存在。例如全局变量,...
  • 堆: 首先应该知道操作系统有一个记录空闲内存地址的链表,当系统收到程序的申请时,会遍历该链表,寻找第一个空间大于所申请空间的堆结点,然后将该结点从空闲 结点链表中删除,并将该结点的空间分配给程序,另外,...
  • Android 查看内存命令总结

    千次阅读 2019-03-14 12:33:03
    文章目录内存相关概念解析dumpsys meminfoprocrankcat /proc/meminfofreeshowmapvmstat小结 内存相关概念解析 一个进程占用了多少内存,主要有下述四种说法: VSS(Virtual Set Size),虚拟内存。RSS + 未分配...
  • 文件系统,第6部分:内存映射文件和共享内存 文件系统,第7部分:可扩展且可靠的文件系统 文件系统,第8部分:从Android设备中删除预装的恶意软件 文件系统,第9部分:磁盘块示例 ...
  • 用jmap把进程内存使用情况dump到文件中,再用jhat分析查看。jmap进行dump命令格式如下:jmap-dump:format=b,file=/tmp/dump.dat21711 -------->进程idjhat-port 10099 /tmp/dump.dat运行在10099端口然后浏览器加...
  • C/C++文件操作——输入输出流

    千次阅读 多人点赞 2018-07-03 14:35:35
    file << hex<<123; file.close(); system("PAUSE"); } 操纵符 功能 输入/输出 Dec 格式化为十进制数值数据 输入和输出 ...
  • 文件系统,第6部分:内存映射文件和共享内存 文件系统,第7部分:可扩展且可靠的文件系统 文件系统,第8部分:从Android设备中删除预装的恶意软件 文件系统,第9部分:磁盘块示例 ...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 144,368
精华内容 57,747
关键字:

/123/n4567内存