linux-用户进程时间统计

1 用户进程耗时

1.1 准备工作

​ 默认环境在sdk包上在busybox工具是不支持bootchartd功能在,因此需要重新配置、编译,然后替换掉rootfs里的busybox可执行文件。

1.2 编译busybox源码

​ 在v536的环境中,busybox的源码在此路劲下out/v536-cdr/compile_dir/target/busybox-1.27.2/,然后配置busybox,通过make menuconfig命令来配置(也可通过在顶层目录下的.config文件来设置),需要配置如下选项:

CONFIG_BOOTCHARTD=y

CONFIG_FEATURE_BOOTCHARTD_BLOATED_HEADER=y

CONFIG_FEATURE_BOOTCHARTD_CONFIG_FILE=y

CONFIG_FEATURE_SEAMLESS_GZ=y

CONFIG_GUNZIP=y

CONFIG_GZIP=y

CONFIG_FEATURE_GZIP_LONG_OPTIONS=y

CONFIG_TAR=y

CONFIG_FEATURE_TAR_CREATE=y

CONFIG_FEATURE_TAR_AUTODETECT=y

CONFIG_FEATURE_TAR_FROM=y

CONFIG_FEATURE_TAR_OLDGNU_COMPATIBILITY=y

CONFIG_FEATURE_TAR_GNU_EXTENSIONS=y

CONFIG_FEATURE_TAR_LONG_OPTIONS=y

CONFIG_FEATURE_TAR_TO_COMMAND=y

CONFIG_FEATURE_TAR_UNAME_GNAME=y

CONFIG_FEATURE_TAR_NOPRESERVE_TIME=y

CONFIG_BOOTCHARTD=y

CONFIG_FEATURE_BOOTCHARTD_BLOATED_HEADER=y

CONFIG_DMESG=y

1.3 编译

make 

1.4 增加并支持bootchartd命令

​ 在target/allwinner/v536-cdr/busybox-init-base-files/目录下创建sbin文件夹,并进入sbin文件夹创建软件链接,命令如下:

cd target/allwinner/v536-cdr/busybox-init-base-files
mkdir sbin
cd sbin
ln -s ../bin/busybox ./bootchartd

​ 在此target/allwinner/v536-cdr/busybox-init-base-files/中创建的软链接会被拷贝到rootfs中的sbin文件夹。

1.5 添加bootchartd的配置文件

​ 在rootfs中的etc目录下添加配置bootchartd.conf,在v536的工程中要在此目录下添加target/allwinner/v536-cdr/busybox-init-base-files/etc/,此目录下会被拷贝在out/v536-cdr/compile_dir/target/rootfs/etc的目录下,bootchartd.conf文件的内容如下:

# supported options: ## Sampling period (in seconds)
SAMPLE_PERIOD=0.2
# tmpfs size# (32 MB should suffice for ~20 minutes worth of log #data, but YMMV)
TMPFS_SIZE=32m # Whether to enable and store BSD process accounting information. The
# kernel needs to be configured to enable v3 accounting
# (CONFIG_BSD_PROCESS_ACCT_V3). accton from the GNU accounting utilities
# is also required.
PROCESS_ACCOUNTING="no" # Tarball for the various boot log files
BOOTLOG_DEST=/var/log/bootchart.tgz
# Whether to automatically stop logging as the boot process completes. AUTO_STOP_LOGGER="
yes" # Whether to automatically generate the boot chart once the boot logger
AUTO_RENDER="no"

1.6 把bootchartd作为初始进程

在cmdline中指定init=bootchared

需要注意的是,如果内核里启动了initramdisk,那么kernel command line指定的初始进程就会失效。为避免这种情况,需要修改内核配置项:CONFIG_BLK_DEV_INITRD=n

1.7 抓取数据

​ 准备好了前面的6个步骤后,重新编译make -j128,然后打包:pack,再把固件包烧录到板子,重启。系统启动后,等一会后,系统就会生成数据文件,文件所在目录/var/log/bootlog.tgz。将此文件拷贝到编译服务器上,用pybootchartgui.py分析

1.8 分析数据

pybootchartgui.py的获取途径,使用以下命令获取:

git clone https://github.com/xrmx/bootchart.git

​ 然后进入源码目录,执行make进行编译。编译完成之后,可将pybootchartgui.py所在的路径添加到

环境变量PATH里,方便使用。准备好后,则使用如下命令来分析数据:

pybootchartgui.py --show-all ./bootlog.tgz

如果分析解析数据失败,则有可能没有安装python-cairo 库,则使用以下命令安装:

sudo apt-get install python-cairo
 

分析数据成功后,会在源码目录下生成bootchartd.png的图片

结论:从bootchartd工具抓取的图像可以看出,大概从4.3s左右开始执行sdvcam应用程序,后续通过打印知道到6.4s左右出图通过图片分析在运行sdvcam这段时间cpu和i/o占用率特别高,此处主要在把sdvcam目标程序和动态库加载到内存然后解压缩

5.9 linux加载用户进程的流程

从编译和运行的角度看,应用程序和库程序的链接有两种方式。

  • 1、静态链接,就是把需要用到的库函数的目标代码(二进制)代码从程序库中抽取出来,链接进应用软件的目标映像中;
  • 2、动态链接,是指库函数的代码并不被编译进应用软件的目标映像,应用软件在编译/链接阶段并不完成跟库函数的链接,而是把函数库的映像也交给用户,到启动应用软件目标映像运行时才把程序库的映像也装入用户空间(并加以定位),再完成应用软件与库函数的连接。

linux内核及支持静态链接的elf也支持动态链接的elf,而且加载和启动elf映像必须由内核完成,是指库函数的代码并不进入应用软件的目标映像,应用软件在编译/链接阶段并不完成跟库函数的链接,而是把函数库的映像也交给用户,到启动应用软件目标映像运行时才把程序库的映像也装入用户空间(并加以定位),再完成应用软件与库函数的连接。

  • GNU把对于动态链接ELF映像的支持作了分工:
    • 把ELF映像的装入/启动入在Linux内核中;而把动态链接的实现放在用户空间,并为此提供一个称为"解释器"的工具软件,tina的这个解释器为ld-musl-armhf.so.1,而解释器的装入/启动也由内核负责
    • 而应用程序多用到的动态链接库的载入则由解释器来完成

5.9.1 linux可运行的elf文件

​ 内核对所支持的每种可执行的程序类型都有个struct linux_binfmt的数据结构来表示:

/*
  * This structure defines the functions that are used to load the binary formats that
  * linux accepts.
  */
struct linux_binfmt {
    struct list_head lh;
    struct module *module;
    int (*load_binary)(struct linux_binprm *);
    int (*load_shlib)(struct file *);
    int (*core_dump)(struct coredump_params *cprm);
    unsigned long min_coredump;     /* minimal dump size */
 };

在fs/binfmt.c中,已经初始化上面的结构体,如下:

static struct linux_binfmt elf_format = {
    .module      = THIS_MODULE,
    .load_binary = load_elf_binary,
    .load_shlib      = load_elf_library,
    .core_dump       = elf_core_dump,
    .min_coredump    = ELF_EXEC_PAGESIZE,
    .hasvdso     = 1
};

​ 要支持ELF文件的运行,则必须向内核登记注册elf_format这个linux_binfmt类型的数据结构,加入到内核支持的可执行程序的队列中。内核提供两个函数来完成这个功能,一个注册,一个注销,即:

int register_binfmt(struct linux_binfmt * fmt)
int unregister_binfmt(struct linux_binfmt * fmt)

​ 当系统执行应用程序时,会调用到execv()或者execve()函数,这两个最终调用的是do_execve(),这个函数先打开目标映像文件,并从目标文件的头部(第一个字节开始)读入若干(当前Linux内核中是128)字节(实际上就是填充ELF文件头),然后调用另一个函数search_binary_handler(),在此函数里面,它会搜索我们上面提到的Linux支持的可执行文件类型队列,让各种可执行程序的处理程序前来认领和处理。如果类型匹配,则调用load_binary函数指针所指向的处理函数来处理目标映像文件。在elf文件的格式中,处理函数是load_elf_binary函数,下面就分析以下此函数的执行流程,此函数主要做了以下几个步骤:

  • 填充并检查ELF目标程序的ELF头部
  • 使用load_elf_phdrs加载目标程序的程序头表
  • 检查目标程序的程序头表中是否需要动态链接的解释器,如果有则寻找和处理解释器段
  • 打开解释器文件检查并读取解释器的程序表头,并把解释器加载到内存
  • 装入目标程序的segment
  • 如果有解释器就填写程序的入口地址为解释器的入口地址,如果没有填入目标程序的入口地址
  • create_elf_tables填写目标文件的参数环境变量等必要信息
  • start_thread来进入到解释器和目标程序运行

下面对load_elf_binary函数进行源码解释,分段分析,有删减:

  • 填充并检查程序的elf头部

    	/*从寄存器回获取信息*/
    	struct pt_regs *regs = current_pt_regs();
    	struct {
    		struct elfhdr elf_ex;
    		struct elfhdr interp_elf_ex;
    	} *loc;
    	struct arch_elf_state arch_state = INIT_ARCH_ELF_STATE;
    
    	loc = kmalloc(sizeof(*loc), GFP_KERNEL);
    	if (!loc) {
    		retval = -ENOMEM;
    		goto out_ret;
    	}
    	
    	/* Get the exec-header,获取可执行程序的elf128字节的头部 */
    	loc->elf_ex = *((struct elfhdr *)bprm->buf);
    
    	retval = -ENOEXEC;
    	/* First of all, some simple consistency checks,检查头部的4个字节是否为"\177ELF" */
    	if (memcmp(loc->elf_ex.e_ident, ELFMAG, SELFMAG) != 0)
    		goto out;
    
    	/*检查执行程序是静态还是有动态库*/
    	if (loc->elf_ex.e_type != ET_EXEC && loc->elf_ex.e_type != ET_DYN)
    		goto out;
    	if (!elf_check_arch(&loc->elf_ex))
    		goto out;
    	if (!bprm->fle->f_op->mmap)
    		goto out;
    
  • load_elf_phdrs加载程序的程序表头

    /*  
            load_elf_phdrs函数就是通过kernel_read读入整个program header table
            从函数代码中可以看到,一个可执行程序必须至少有一个段(segment),
            而所有段的大小之和不能超过64K
     */
    	elf_phdata = load_elf_phdrs(&loc->elf_ex, bprm->file);
    	if (!elf_phdata)
    		goto out;
    
  • 判断目标程序的头部是否有动态链接,如果有则寻找解释器

    	for (i = 0; i < loc->elf_ex.e_phnum; i++) {
            /*判断目标程序是否需要解释器*/
    		if (elf_ppnt->p_type == PT_INTERP) {
    			/* This is the program interpreter used for
    			 * shared libraries - for now assume that this
    			 * is an a.out format binary
    			 */
    			......
    			/*根据其位置的p_offset和大小p_filesz把整个"解释器"段的内容读入缓冲区  */
    			retval = kernel_read(bprm->file, elf_ppnt->p_offset,
    					     elf_interpreter,
    					     elf_ppnt->p_filesz);
    			
       
    			if (elf_interpreter[elf_ppnt->p_filesz - 1] != '\0')
    				goto out_free_interp;
    			 /*通过open_exec打开解释器文件*/
    			interpreter = open_exec(elf_interpreter);
    			retval = PTR_ERR(interpreter);
    			if (IS_ERR(interpreter))
    				goto out_free_interp;
    			......
    			/* Get the exec headers,获取解释器的头部,也就是解释器的前128个字节 */
    			retval = kernel_read(interpreter, 0,
    					     (void *)&loc->interp_elf_ex,
    					     sizeof(loc->interp_elf_ex));
    			break;
    		}
    		elf_ppnt++;
    	}
    

    ​ 我们来看下sdvcam的ELF的链接信息,从图中可以看到sdvcam是需要动态链接的,解释器为ld-musl-armhf.so.1

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-CRPlIFol-1626503101855)(boot0-uboot流程分析.assets/image-20210311142754491.png)]

  • 检查并读取解释器的程序表头并把解释器头部加载到内存中

    	/* Some simple consistency checks for the interpreter */
    	if (elf_interpreter) {
    		retval = -ELIBBAD;
    		/* Not an ELF interpreter */
    		if (memcmp(loc->interp_elf_ex.e_ident, ELFMAG, SELFMAG) != 0)
    			goto out_free_dentry;
    		/* Verify the interpreter has a valid arch */
    		if (!elf_check_arch(&loc->interp_elf_ex))
    			goto out_free_dentry;
    
    		/* Load the interpreter program headers */
    		interp_elf_phdata = load_elf_phdrs(&loc->interp_elf_ex,
    						   interpreter);
    		if (!interp_elf_phdata)
    			goto out_free_dentry;
    
    		/* Pass PT_LOPROC..PT_HIPROC headers to arch code */
    		elf_ppnt = interp_elf_phdata;
    		for (i = 0; i < loc->interp_elf_ex.e_phnum; i++, elf_ppnt++)
    			switch (elf_ppnt->p_type) {
    			case PT_LOPROC ... PT_HIPROC:
    				retval = arch_elf_pt_proc(&loc->interp_elf_ex,
    							  elf_ppnt, interpreter,
    							  true, &arch_state);
    				if (retval)
    					goto out_free_dentry;
    				break;
    			}
    	}
    

    ​ 到这里我们已经把目标程序和解释器的表头已经加载到内存。

  • 装入目标程序的段segment

    • 这段代码从目标映像的程序头中搜索类型为PT_LOAD的段(Segment)。在二进制映像中,只有类型为PT_LOAD的段才是需要装入的。当然在装入之前,需要确定装入的地址,只要考虑的就是页面对齐,还有该段的p_vaddr域的值。确定了装入地址后,就通过elf_map()建立用户空间虚拟地址空间与目标映像文件中某个连续区间之间的映射,其返回值就是实际映射的起始地址。
    for(i = 0, elf_ppnt = elf_phdata;
    	    i < loc->elf_ex.e_phnum; i++, elf_ppnt++) {
    		int elf_prot = 0, elf_flags;
    		unsigned long k, vaddr;
    		unsigned long total_size = 0;
    
    		if (elf_ppnt->p_type != PT_LOAD)
    			continue;
    
    		if (unlikely (elf_brk > elf_bss)) {
    			unsigned long nbyte;
    	            
    			/* There was a PT_LOAD segment with p_memsz > p_filesz
    			   before this one. Map anonymous pages, if needed,
    			   and clear the area.  */
    			retval = set_brk(elf_bss + load_bias,
    					 elf_brk + load_bias);
    			if (retval)
    				goto out_free_dentry;
    			nbyte = ELF_PAGEOFFSET(elf_bss);
    			if (nbyte) {
    				nbyte = ELF_MIN_ALIGN - nbyte;
    				if (nbyte > elf_brk - elf_bss)
    					nbyte = elf_brk - elf_bss;
    				if (clear_user((void __user *)elf_bss +
    							load_bias, nbyte)) {
    					/*
    					 * This bss-zeroing can fail if the ELF
    					 * file specifies odd protections. So
    					 * we don't check the return value
    					 */
    				}
    			}
    		}
        
        error = elf_map(bprm->file, load_bias + vaddr, elf_ppnt,
    				elf_prot, elf_flags, total_size);
    
  • 填写程序的入口地址

    • 如果有解释器就填入解释器的入口地址,如果没有就填入目标程序的入口地址
    	if (elf_interpreter) {
    		unsigned long interp_map_addr = 0;
    		elf_entry = load_elf_interp(&loc->interp_elf_ex,
    					    interpreter,
    					    &interp_map_addr,
    					    load_bias, interp_elf_phdata);
    	} else {
    		elf_entry = loc->elf_ex.e_entry;
    		if (BAD_ADDR(elf_entry)) {
    			retval = -EINVAL;
    			goto out_free_dentry;
    		}
    	}
    
  • 调用create_elf_tables填写目标文件的参数环境

        retval = create_elf_tables(bprm, &loc->elf_ex,
                  load_addr, interp_load_addr);
        if (retval < 0)
            goto out;
        /* N.B. passed_fileno might not be initialized? */
        current->mm->end_code = end_code;
        current->mm->start_code = start_code;
        current->mm->start_data = start_data;
        current->mm->end_data = end_data;
    
    
  • 调用start_thread宏准备进入新的程序入口(此时如果是动态链接则直接进入解释器加载动态库后再跳到程序入口执行)

    start_thread(regs, elf_entry, bprm->p);
    

你可能感兴趣的:(linux,linux)