当前位置:>首页 -> 商业

你写的代码是如何跑起来的

2022-12-25 19:43:00  来源:网络   阅读量:16898   
你写的代码是如何跑起来的

大家好,我是飞哥!

今天,我们来思考一个简单的问题。一个程序如何在Linux上运行?

我们以宇宙中最简单的Hello World程序为例。

# includeltstdio.hgtintmainprintf( "你好,世界! n ");return0

写完代码后,我们简单编译一下,然后就可以在shell命令行启动了。

#gccmain.c-ohelloworld#。/helloworldHello,世界!

那么编译、启动、运行的过程中发生了什么呢?今天我们就来仔细看看。

首先,了解可执行文件的格式

源代码编译完成后,会生成一个可执行的程序文件。我们先来了解一下编译后的二进制文件是什么样子的。

首先,让我们使用file命令来检查这个文件的格式。

# filehelloworldhelloworld:elf 64-bitLSBexecutable,x86-64,版本1,...

file命令给出了这个二进制文件的摘要信息,其中ELF 64位LSB可执行文件表示这个文件是ELF格式的64位可执行文件。X86-64表示此可执行文件支持的cpu体系结构。

LSB的全称是Linux Standard Base,是一个Linux标准规范。它的目的是制定一系列标准来增强Linux发行版的兼容性。

ELF的全称是可执行可链接格式,是一种二进制文件格式。Linux下的目标文件、可执行文件和CoreDump都是按照这种格式存储的。

ELF文件由四部分组成,即ELF文件头、程序头表、段和段头表。

接下来,我们分几个板块逐一介绍。

1.1 ELF文件头

ELF文件头记录了整个文件的属性信息。原来的二进制很不方便观察。但是,我们有一个方便的工具——readelf,可以帮助我们查看elf文件中的各种信息。

我们先来看看编译后的可执行文件的ELF文件头,可以使用-file-header选项查看。

# readelf-file-header helloworldelfheader:Magic:7f 454 c 46020101000000000000000 class:elf 64 data:2 ' s complement,little endian version:1OS/ABI:UNIX-SystemVABIVersion:0 type:EXEC(executable file)Machine:advancedmicrodevices x86-64 version:0x 1 entry point address:0x 401040 startofprogramheaders:64(字节

ELF文件的文件头包含当前可执行文件的摘要信息。我拿出几个重点来给大家讲解一下。

Magic:一串特殊的识别码,主要由外部程序用来快速识别这个文件,快速判断文件类型是否为ELF。

Class:表示这是一个ELF64文件。

类型:EXEC是一个可执行文件。其他文件类型包括REL、DYN(动态链接库)和CORE(系统调试coredump文件)。

入口地址:程序的入口地址,显示入口在0x401040。

这个头的大小:ELF文件头的大小,这里显示为占用64个字节。

以上字段是ELF头中对ELF的整体描述。此外,ELF头包含关于程序头和节头的描述信息。

程序头的开始:表示程序头的位置。

程序头的大小:每个程序头的大小

节目头数:总共有多少个节目头?

节头的开始:表示节头的开始位置。

节标题的大小:每个节标题的大小

章节标题的数量:有多少章节标题?

1.2程序标题表

在介绍程序头表之前,先介绍一对类似的概念ELF文件中的Segment和Section。

ELF文件中最重要的单元是一个接一个的节。每个部分由编译器链接器生成,有不同的用途。比如编译器会把我们写的代码编译好放进去。文本部分,并将全局变量放入。数据或。bss部分。

但是对于操作系统来说,它并不关注具体的节是什么,它只关注这个内容应该加载到内存中的权限是什么,比如读、写、执行等权限属性。因此,具有相同权限的Section可以放在一起形成一个段,便于操作系统更快地加载。

由于Segment和Section翻译成中文,它们的意思过于接近,很难理解。所以在这篇文章里,我直接用了原来的Segment和Section的概念,而不是翻译成段落或小节,太混乱了。

节目头表作为所有节目段的头信息来描述所有节目段。。

使用readelf工具的- program-headers选项来分析和查看存储在该区域中的内容。

# readelf-program-headershelloworldelfpiletypeisexecentrypoint 0x 401040 there are 11 program headers,startingatoffset 64 program headers:typeoffsetvirtaddrphysadrfilesizemsizflagsalignphdr 0x 00000000000040000000004000000000000000040000000000000000000000000000000000000...0001 . interp 02 . interp . note . GNU . build-id . note . ABI-tag . GNU . hash . dyn sym . dynstr . GNU . version . GNU . version _ r . rela . dyn . rela . PLT . text . fini 04 . rodata . eh _ frame _ HDR . eh _ frame 05 . init _ array . fini _ array . dynamic . got . PLT . data . BSS 06 . dynamic 07 . note . GNU .

上面的结果显示总共有11个程序头。

对于每个段,输出Offset、VirtAddr和描述当前段的其他信息。Offset表示当前段在二进制文件中的起始位置,FileSiz表示当前段的大小。Flag表示当前段的权限类型,R表示全部,E表示可执行,W表示可写。

在底部,它还显示了每个部分由哪些部分组成。例如,第03节由四个部分组成”。初始化。plt。文字。菲尼”。

1.3章节标题表

与程序头表不同,段头表直接描述每个段。它们都描述了不同的部分,但是它们有不同的目的,一个用于加载,另一个用于链接。

使用readelf工具的- section-headers选项来解析和查看存储在该区域中的内容。

# readelf-section-header shell worldthere 30 section headers,startingatoffset 0x5b 10:section headers:NameTypeAddressOffsetSizeEntSizeFlagsLinkInfoAlign......(13). textprogbits 0000000000401040000000104000000000000175000000000000000 ax 0016。......(23)data progbits 00000000004040200000000302000000000000000000000000000000000000000000000000000000000000000030000000000000000000000000000000000000000000000000000000000000000000000000000000......KeytoFlags:W(write),A(alloc),X(execute),M(merge),S(strings),I(info),L(linkorder),O(extroprocessingrequired),G(group),T(TLS),C(compressed),x(unknown),o(OSspecific),E(exclude),l(large),p(processorspecific)

结果显示,文件中有30个部分,每个部分在二进制文件中的位置由Offset列指示。部分的大小由“大小”列反映。

在这30个部分中,每个部分都有独特作用。我们写的代码会放到节里。编译成二进制指令后的文本。此外,我们看到。文本部分显示地址为00000000401040。回想一下,我们在ELF文件的头文件中看到,入口点地址显示的入口地址是0x401040。这表明程序的入口地址是。文本段。

还有另外两个部分值得注意:。数据和。bss。代码中的全局变量数据在编译后会占据这两段中的一些位置。如下面的简单代码所示。

//未初始化的内存区域位于。bss段intdata1//初始化的内存区域位于。数据段intdata2 = 100//代码位于intmain1.4在。供进一步查看的文本段。

接下来,我们想看看前面提到的程序条目0x401040,看看它是什么。这一次,让我们在nm命令的帮助下,仔细看看可执行文件中的符号及其地址信息。-n选项用于按地址而不是按名称对显示的符号进行排序。

# nm-nhelloworldw _ _ gmon _ start _ _ U _ _ libc _ start _ mainGLIBC _ 2 . 2 . 5 uprintfglibc _ 2 . 2 . 50000000000401040t _ start 0000000000401126 main

从上面的输出可以看出,程序条目0x401040指向_start函数的地址。在这个函数执行一些初始化操作之后,我们的入口函数main将被调用,它位于地址0x401126。

二。用户进程创建过程概述

我们写好的代码编译生成可执行程序后,下一步就是用shell加载运行了。一般来说,shell进程通过fork+execve加载并运行新的进程。简单加载helloworld命令的shell的核心逻辑如下。

//shell代码示例int main)PID = fork();If(pid0)//如果在进程中//使用exec系列函数加载运行可执行文件execve ( "Hello World ",argv,envp);其他

shell进程首先通过fork系统调用创建一个进程。然后在子进程中调用execve来加载执行的程序文件,然后就可以调用到程序文件的运行入口来运行这个程序了。

fork系统调用在内核入口处的kernel/fork.c下。

//file:kernel/fork . csyscall _ define 0 return do _ fork(SIGCHLD,0,0,NULL,NULL);

在do_fork的实现中,核心是一个copy_process函数,它复制父进程生成一个新的task_struct。

//file:kernel/fork . clong do _ fork//复制一个task_struct到struct task _ struct * p;p=copy_process(clone_flags,stack_start,stack_size,child_tidptr,NULL,trace);//将子任务加入就绪队列,等待调度器调度wake _ up _ new _ task(p);

在copy_process函数中为新进程申请task_struct,用当前进程自己的地址空间、命名空间等初始化新进程。,并为其申请工艺pid。

//file:kernel/fork . cstatics tructtask_struct * copy _ process//复制进程task _ struct结构struct task _ struct * p;p=dup_task_struct(当前);//process核心元素初始化retval = copy _ files (clone _ flags,p);retval=copy_fs(clone_flags,p);retval=copy_mm(clone_flags,p);retval = copy _ namespaces(clone _ flags,p);//申请pidampamp设置进程号PID = alloc _ PID(p-n proxy-PID _ ns);p-PID = PID _ NR(PID);p-tgid = p-PID;

执行后,进入wake_up_new_task,让新进程等待调度器调度。

但是,fork系统调用只能根据当前shell进程复制一个新进程。这个新进程中的代码和数据仍然与原始shell进程中的代码和数据完全相同。

要加载并运行另一个程序,比如我们编译的helloworld程序,您需要使用execve系统调用。

三。Linux可执行加载程序

其实Linux只能加载ELF这种可执行文件格式,不能写死。当它启动时,它将加载它支持的所有可执行文件的解析器。并使用格式双向链表来保存所有解析器。内存中格式双向链表的结构如下图所示。

我们以ELF的loader elf_format为例,看看这个loader是怎么注册的。在Linux中,每个加载器由一个linux_binfmt结构表示。它指定了用于加载二进制可执行文件的load_binary函数指针,以及用于加载崩溃文件的core_dump函数。它的完整定义如下

//file:include/Linux/bin fmts . hstructlinux _ binfmtint(struct Linux _ bin PRM *);int(* load _ shlib)(struct file *);int(* core _ dump)(structcoredump _ params * cprm);;

ELF的loader ELF _ format指定了具体的加载函数,例如load_binary成员指向具体的load_elf_binary函数。这是ELF装载的入口。

//file:fs/bin fmt _ elf . cstaticstructlinux _ binfmtelf _ format =。模块= THIS _模块,。load_binary=load_elf_binary,。load_shlib=load_elf_library,。核心转储=elf核心转储,。min_coredump=ELF_EXEC_PAGESIZE,;

register_binfmt将在初始化期间注册加载程序elf_format。

//file:fs/bin fmt _ elf . cstaticint _ _ init init _ elf _ binfmtregister _ bin fmt(amp;elf _ format);return0

而register_binfmt就是把加载器挂在全局加载器list-formats全局链表中。

//file:fs/exec . cstaticlist _ HEAD;void _ _ register _ bin fmt(struct Linux _ bin fmt * fmt,intinsert)插入?list _ add(amp;fmt-左侧,amp格式):list _ add _ tail(amp;fmt-左侧,amp格式);

Linux支持除elf文件格式之外的其他格式。在源码目录中搜索register_binfmt,可以找到Linux操作系统支持的所有加载器格式。

# grep-r " register _ bin fmt " * fs/bin fmt _ flat . c:register _ bin fmt;fs/bin fmt _ elf _ FD pic . c:register _ bin fmt(amp;elf _ FD pic _ format);fs/bin fmt _ som . c:register _ bin fmt(amp;som _ format);fs/bin fmt _ elf . c:register _ bin fmt(amp;elf _ format);fs/bin fmt _ aout . c:register _ bin fmt(amp;aout _ format);fs/bin fmt _ script . c:register _ bin fmt(amp;script _ format);fs/bin fmt _ em86 . c:register _ bin fmt(amp;em86 _ format);

以后Linux在加载二进制文件时会遍历格式链表,根据要加载的文件格式查询合适的加载器。

四。execve加载用户程序

加载可执行文件的具体工作由execve系统调用完成。

系统调用将读取用户输入的可执行文件名称、参数列表和环境变量,并开始加载和运行用户指定的可执行文件。系统调用的位置在fs/exec.c文件中。

//file:fs/exec . csys call _ define 3 struct filename * path = getname(filename);do_execve(path-name,argv,envp)int do _ exec ve()returndo _ exec ve _ common(filename,argv,envp);

execve系统调用了do_execve_common函数。我们来看看这个函数的实现。

//file:fs/exec . cstaticindo _ exec ve _ common//Linux _ bin PRM结构用于保存加载二进制文件时使用的参数structlinux _ binprm * bprm//1申请并初始化brm对象值bprm = kzaloc (sizeof (* bprm),GFP _ kernel);bprm-file =;bprm-filename =;bprm _ mm _ init(bprm)bprm-argc = count(argv,MAX _ ARG _ STRINGS);bprm-envc=count(envp,MAX _ ARG _ STRINGS);prepare _ bin PRM(bprm);//2遍历查找合适的二进制加载器search _ binary _ handler(bprm);

在该功能中申请和初始化brm对象的具体工作可以如下图所示。

在这个函数中,已经完成了三项工作。

1.使用kzalloc申请linux_binprm内核对象。这个内核对象用于保存加载二进制文件时使用的参数。应用后,参数对象被初始化。

其次,在bprm_mm_init中将申请一个全新的mm_struct对象,它将被保留给新的进程。

第三,为新进程的堆栈申请一页虚拟内存空间,记录堆栈指针。

第四,读取二进制文件的前128个字节。

让我们看一下与初始化堆栈相关的代码。

//file:fs/exec . cstaticint _ _ bprm _ mm _ init bprm-VMA = VMA = kmem _ cache _ zal loc(VM _ area _ cachep,GFP _ KERNEL);VMA-VM _ end = STACK _ TOP _ MAX;VMA-VM _ start = VMA-VM _ end-PAGE _ SIZE;bprm-p = VMA-VM _ end-sizeof(void *);

在上面的函数中,申请了一个vma对象,vm_end指向STACK_TOP_MAX(靠近地址空间顶部的一个位置),在vm_start和vm_end之间留出一个页面大小。也就是说,默认情况下,堆栈的大小为4KB。最后,堆栈的指针被记录到bprm-gt;p中等。

再看一下prepare_binprm。在这个函数中,从文件头中读取128个字节。这样做的原因是为了读取二进制文件的文件头,以便于后期判断其文件类型。

//file:include/uapi/Linux/bin fmts . h # defineBINPRM _ BUF _ SIZE 128//file:fs/exec . cint prepare _ binprmmemset(bprm-BUF,0,bin PRM _ BUF _ SIZE);returnkernel_read(bprm-file,0,bprm-buf,bin PRM _ BUF _ SIZE);

在申请并初始化brm对象值后,最后使用search_binary_handler函数遍历系统中注册的加载器,尝试解析并加载当前的可执行文件。

在3.1节中,我们介绍了系统的所有加载程序都注册在格式全局链表中。search_binary_handler函数的工作过程是遍历全局链表,根据二进制文件头中携带的文件类型数据找到解析器。找到调用解析器加载二进制文件的函数。

//file:fs/exec . cint search _ binary _ handler fortry = 0;try2try++list_for_each_entry(fmt,ampformats,LH)int(* fn)(struct Linux _ bin PRM *)= fmt-load _ binary;retval = fn(bprm);//如果加载成功,则返回If(retval = 0)return retval;//加载失败继续循环以尝试加载

上面代码中的list_for_each_entry是遍历格式的全局链表,遍历时判断每个链表元素是否有load_binary函数。如果有,调用它并尝试加载它。

回想一下3.1可执行文件加载程序的注册。对于ELF文件加载器elf_format,load_binary函数的指针指向load_elf_binary。

//file:fs/bin fmt _ elf . cstaticstructlinux _ binfmtelf _ format =。模块= THIS _模块,。load_binary=load_elf_binary,;

然后加载工作会进入load_elf_binary函数。这个函数很长。可以说,所有的程序加载逻辑都体现在这个函数中。根据这个功能的主要工作,我分以下五个小部分给大家介绍。

在介绍的过程中,为了表达清楚,我会稍微调整一下源代码的位置,可能和内核源代码的行顺序不一样。

4.1 ELF文件头读取

在load_ELF_binary中,将首先读取ELF文件的头。

文件头包含了当前文件格式类型等一些数据,所以在读取文件头后会做出一些合法性判断。如果不合法,退出并返回。

//file:fs/bin fmt _ ELF . cstaticintload _ ELF _ binary//4.1 ELF文件头解析//定义结构标题并申请内存保存ELF文件头structstructelfhdrelf _ exstructelfhdrinterp _ elf _ ex* locloc=kmalloc(sizeof(*loc),GFP _ KERNEL);//获取二进制头loc-gt;elf _ ex = *((structelfhdr *)bprm-gt;buf);//头上做一系列合法性判断,退出if(loc-gt;elf_ex.e_type!= ET _ EXECampamp...)gotoout...4.2程序标题读取

程序头的数目记录在ELF文件的头中,紧接在ELF文件头之后的是程序头表。这样内核就可以读出所有的程序头。

//file:fs/bin fmt _ elf . cstaticintload _ elf _ binary//4.1 elf文件头解析//4.2ProgramHeader读取//elf_ex.e_phnum保存程序头个数//然后根据program header size size of(struct elf _ phdr)//计算所有程序头大小,读入size = loc-elf _ ex。e _ phnum * sizeof(struct elf _ phdr);elf_phdata=kmalloc(size,GFP _ KERNEL);kernel_read(bprm-file,loc-elf_ex.e_phoff,(char*)elf_phdata,size);4.3清空父进程继承的资源。

fork系统调用创建的进程包含了原进程的很多信息,比如旧的地址空间、信号表等等。这些新程序在运行时毫无用处,所以需要清理。

工作包括初始化新进程的信号表,应用新的地址空间对象等。

//file:fs/bin fmt _ Elf . cstaticintload _ Elf _ binary//4.1 Elf文件头解析//4.2ProgramHeader读取//4.3清除父进程继承的资源retval = flush _ old _ exec(bprm);current-mm-start _ stack = bprm-p;

清空父进程继承的资源后,直接将之前准备的进程栈的地址空间指针设置为mm对象。以便将来可以使用该堆栈。

4.4执行分段加载

接下来,加载程序会将ELF文件中所有加载类型的段加载到内存中。使用elf_map在虚拟地址空间中分配虚拟内存。最后,适当地设置虚拟地址空间mm_struct中的地址空间相关指针,如start_code、end_code、start_data和end_data。

我们来看看具体的代码:

//file:fs/bin fmt _ Elf . cstaticintload _ Elf _ binary//4.1 Elf文件头解析//4.2ProgramHeader读取//4.3清除父进程继承的资源//4.4执行段加载进程//遍历可执行文件的program header for(I = 0,elf _ ppnt = ppnt iltloc-gt;elf _ ex.e _ phnum++,elf _ ppnt++)//只加载LOAD类型的段,否则跳过if(elf_ppnt-p_type!=PT_LOAD)继续;//为段建立内存mmap,将程序文件的内容映射到虚拟内存空间。//这样以后就可以访问程序中的代码和数据了。Error = elf _ map (bprm-file,load _ bias+vaddr,elf _ ppnt,elf _ prot,elf _ flags,0);//start_code=计算mm_struct所需的每个成员地址的;start _ data = end _ code =end _ data =;当前-mm-end _ code = end _ code;当前-mm-start _ code = start _ code;当前-mm-start _ data = start _ data;current-mm-end _ data = end _ data;

其中load_bias是要加载到内存中的段的基址。这个参数有几种可能性

值为0表示直接根据ELF文件中的地址在内存中映射。

为了将该值与整数页的开头对齐,物理文件对于可执行文件的大小来说可能足够紧凑,而不考虑对齐问题。然而,为了高效地运行,操作系统需要在整数页的开始处加载段。

4.5数据存储应用放大器;堆初始化

因为进程的数据段需要写权限,所以需要使用set_brk系统调用为数据段申请虚拟内存。

//file:fs/bin fmt _ Elf . cstaticintload _ Elf _ binary//4.1 Elf文件头解析//4.2ProgramHeader读取//4.3清除父进程继承的资源//4.4执行段加载进程//4.5申请数据内存amp初始化retval=set_brk(elf_bss,elf _ brk);

set_brk函数中做了两件事:第一件是为数据段申请虚拟内存,第二件是初始化进程堆的开始指针和结束指针。

//file:fs/bin fmt _ ELF . cstaticintset _ brk//1为数据段申请虚拟内存start = ELF _ page align(start);end = ELF _ page align(end);if(end start)unsignedlongaddr;addr=vm_brk(start,end-start);//2初始化堆的指针current-mm-start _ brk = current-mm-brk = end;return0

因为程序初始化时堆还是空的。因此,当堆指针被初始化时,堆的start_brk address _ brk和end地址brk都被设置为相同的值。

4.6跳转到程序入口执行。

程序的入口地址记录在ELF文件的头中。在非动态链接加载的情况下,入口地址是这样的。

但是如果是动态链接,也就是说有INTERP类型的段,这个动态链接器会先加载运行,然后回调到程序的代码入口地址。

# readelf-program-headershelloworldProgramHeaders:typeoffsetvirtaddrphysadrfilesizmemsizflagsaligninterp0x 0000000000002 a 80000000000004002 a 80000000000000001 c 0x 0000000000000001 c 0x 0 x 1

对于动态加载器类型,您需要首先将动态加载器加载到地址空间中。

加载完成后,计算动态加载程序的入口地址。下面我展示这段代码,不耐烦的同学可以跳过。反正只要知道这里是一个程序的入口地址就行了。

//file:fs/bin fmt _ Elf . cstaticintload _ Elf _ binary//4.1 Elf文件头解析//4.2ProgramHeader读取//4.3清除父进程继承的资源//4.4执行段加载//4.5申请数据内存amp初始化//4.6跳转到程序入口执行//第一次遍历programheadertable//只对PT_INTERP类型的段进行预处理//该段保存动态加载器在文件系统中的路径信息for(I = 0;iltloc-gt;elf _ ex.e _ phnum++) ...//第二次遍历programheadertable并做一些特殊处理elf _ ppnt = elf _ phdatafor(I = 0;iltloc-gt;elf _ ex.e _ phnum++,elf _ ppnt++)...//如果程序中指定了动态链接器,则读出动态链接器程序if(elf_interpreter)// Load并返回动态链接器代码段elf_entry=load_elf_interp的地址(amploc-gt;interp_elf_ex,解释器,放大器;interp_map_addr,load _ bias);//计算动态链接器入口地址elf _ entry+= loc-gt;interp _ elf _ ex.e _ entryelse elf _ entry = loc-gt;elf _ ex.e _ entry//跳转到门户启动start _ thread (regs,elf _ entry,bprm-gt;p);...五.总结

看似简单的一行helloworld代码,但要想看清楚它的运行过程,需要很大的内功。

本文首先带领大家认识和理解二进制可运行ELF文件格式。ELF文件由四部分组成,即ELF文件头、程序头表、段和段头表。

当Linux初始化时,所有受支持的加载程序都将在一个全局链表中注册。对于ELF文件,其加载器在内核中定义为elf_format,其二进制加载入口为load_elf_binary函数。

一般来说,shell进程通过fork+execve加载并运行新的进程。执行fork系统调用的作用是创建一个新的进程。但是,fork创建的新进程的代码和数据与原来的shell进程完全相同。要加载并运行另一个程序,需要使用execve系统调用。

在execve系统调用中,将首先应用一个linux_binprm对象。在初始化linux_binprm的过程中,会申请一个全新的mm_struct对象,并为新进程预留。还将为新进程的堆栈准备一页虚拟内存。还会读取可执行文件的前128个字节。

下一步是调用ELF加载器的load _ ELF _ binary函数进行实际加载。将大致执行以下步骤:

ELF文件头解析

程序头读取

清空父进程继承的资源,使用新的mm_struct和新的堆栈。

执行段加载,将ELF文件中加载类型的所有段加载到虚拟内存中。

为数据段申请内存,初始化堆的开始指针。

最后计算,跳转到程序入口执行。

当用户进程启动时,我们可以通过proc伪文件查看进程中的每个段。

# cat/proc/46276/maps 00400000-00401000 r-p 000000000 FD:01396999/root/work _ temp/hello world 00401000-00402000 r-XP 00001000 FD:01396999/root/work _ temp/hello world 00402000-004030000......7 f 01231 c 0000-7 f 01231 c 1000 r-p 0002 a 000 FD:011182554/usr/lib 64/LD-2.32 . so 7 f 01231 c 1000-7 f 01231 c 3000 rw-p 0002 b 000 FD:011182554/usr/lib 64/LD-2.32 . so 7 ffdf 059000......

虽然这篇文章很长,但它仍然只是列出了一般的加载和启动过程。如果你在以后的工作和学习中遇到了想找出来的问题,可以按照本文的思路,在源代码中找到具体的问题,然后帮你找到工作中问题的解决方案。

最后,细心的读者可能会发现,在这个例子中,加载一个新程序来运行的过程实际上存在一些浪费。fork系统调用首先复制父进程的大量信息,当execve加载可执行程序时,重新赋值。因此,在实际的shell程序中,一般使用vfork。它的工作原理和fork的基本相同,不同的是它会复制较少execve系统调用中不使用的信息,从而提高加载性能。

声明:以上内容为本网站转自其它媒体,相关信息仅为传递更多企业信息之目的,不代表本网观点,亦不代表本网站赞同其观点或证实其内容的真实性。投资有风险,需谨慎。

热门文章