进程的创建与可执行程序的加载

一、进程试探
    
编程实现一个简单的shell程序

点击(此处)折叠或打开

  1. #include<stdio.h>
  2. #include<stdlib.h>
  3. #include<unistd.h>
  4. #include<string.h>
  5. #include<sys/types.h>
  6. #define NUM 1024
  7. int mystrtok(char *argv[], char* string)
  8. {//遍历字符串,截取空格之间的各个字符子串,保存入参数数组argv中
  9.         int i=0;
  10.         char delim[]=" ";
  11.         argv[0] = strtok(string,delim);//将字符串string根据间隔符delim分成一个个片段
  12.         while(argv[i]!=NULL)
  13.         {
  14.                 argv[++i] = strtok(NULL,delim);
  15.         }
  16.         return 0;
  17. }
  18. int main()
  19. {
  20.         char str[NUM];
  21.         int status;
  22.         pid_t pid;
  23.         char * argv[NUM];
  24.         while(1)
  25.         {
  26.                 printf("n$:");
  27.                 fgets(str,NUM,stdin);//从键盘中读取命令字符,直到遇到换行符
  28.                 str[strlen(str)-1]='0';//读取的字符串中添加字符串结束符’0’
  29.                 status = mystrtok(argv,str);//截取命令及参数存入argv数组中
  30.                 if(status!=0)//截取失败
  31.                 {
  32.                         printf("fail to get command!\n");
  33.                 }
  34.                 pid = fork();
  35.                 if(pid==-1)
  36.                         printf("fork failure!\n");
  37.                 else if(pid==0)//子进程
  38.                 {
  39.                         execvp(argv[0],argv);
  40. //execvp()会从PATH 环境变量所指的目录中查找符合参数argv[0] 的文件名,找到后便执行该文件,然后将第二个参数argv传给该欲执行的文件
  41.                 }
  42.                 else//父进程
  43.                 {
  44.                         wait();
  45.                 }
  46.         }
  47. }
运行结果如下:

二、C代码嵌入汇编代码

1、c代码嵌入一般汇编代码

点击(此处)折叠或打开

  1. #include<stdio.h>
  2. int main()
  3. {
  4.         unsigned int val1=1;
  5.         unsigned int val2=2;
  6.         unsigned int val3=0;
  7.         printf("vala:%d,val2:%d,val3:%d\n",val1,val2,val3);
  8.         asm volatile(
  9.         "movl $0,%%eax\n\t"
  10.         "addl %1,%%eax\n\t"
  11.         "addl %2,%%eax\n\t"
  12.         "movl %%eax,%0\n\t"
  13.         :"=m"(val3)
  14.         :"c"(val1),"d"(val2)
  15.         );
  16.         printf("val1:%d+val2:%d=val3:%d\n",val1,val2,val3);
  17. }

2、C代码嵌入系统调用汇编代码

点击(此处)折叠或打开

  1. #include<stdio.h>
  2. #include<time.h>
  3. int main()
  4. {
  5.         time_t tt;
  6.         struct tm *t;
  7.         int ret;
  8. #if 0
  9.         time(&tt);
  10.         printf("tt:%ld\n",tt);
  11. #else
  12.         //没有使用常规寄存器%ebx传参
  13.         asm volatile(
  14.         "mov $0,%%ebx\n\t" //没有使用参数tt
  15.         "mov $0xd,%%eax\n\t"
  16.         "int $0x80\n\t"
  17.         "mov %%eax,%0\n\t"
  18.         :"=m"(tt)
  19.         );
  20.         printf("tt:%ld\n",tt);
  21.         t=localtime(&tt);
  22.         printf("time:%d:%d:%d:%d:%d:%d\n",t->tm_year+1900,t->tm_mon,t->tm_mday,t->tm_hour,t->tm_min,t->tm_sec);
  23.         //使用常规寄存器%ebx传参
  24.         asm volatile(
  25.         "mov %1,%%ebx\n\t" //使用参数tt
  26.         "mov $0xd,%%eax\n\t"
  27.         "int $0x80\n\t"
  28.         "mov %%eax,%0\n\t"
  29.         :"=m"(ret)
  30.         :"b"(&tt)
  31.         );
  32.         printf("tt:%ld\n",tt);
  33.         t=localtime(&tt);
  34.         printf("time:%d:%d:%d:%d:%d:%d\n",t->tm_year+1900,t->tm_mon,t->tm_mday,t->tm_hour,t->tm_min,t->tm_sec);
  35. #endif
  36.         return 0;
  37. }
     首先看看time的汇编代码,执行上述代码中的time函数,然后反汇编如下:
    
     对time的反汇编来看gcc编译器也没有使用ebx寄存器。
    然后执行上述代码中else的部分进行 测试,结果如下图:
    
    输出不用%ebx传递参数的 结果与用%ebx传递参数的运行结果完全一样。

    正如我们看到的那样,tiem系统调用函数没有使用%ebx进行参数传递,显然没有遵守系统调用参数传递的一般方法。

三、 分析fork和exec系统调用在内核中的执行过程

1、task_struct进程控制块

    为了描述和控制进程的运行,操作系统为每个进程定义了一个数据结构,即进程控制块(Process Control Block,PCB)。我们通常所说的进程实体包含程序段,数据段和PCB三部分。PCB在进程实体中占据重要的地位。所谓的创建进程,实质上就是创建PCB的过程;而撤销进程,实质上也就是对PCB的撤销。 在Linux内核中,PCB对应着一个具体的结构体—task_struct,也就是所谓的进程描述符(process descriptor)。该数据结构中包含了与一个进程相关的所有信息,比如包含众多描述进程属性的字段,以及指向其他与进程相关的结构体的指针。include/linux/sched.h包含有struct task_struct的定义:



    进程描述符中有指向mm_struct结构体的指针mm,这个结构体是对该进程用户空间的描述;也有指向fs_struct结构体的指针fs,这个结构体是对进程当前所在目录的描述;也有指向files_struct结构体的指针files,这个结构体是对该进程已打开的所有文件进行描述;另外还有一个小型的进程描述符(low-level information)—thread_info。在这个结构体中,也有指向该进程描述符的指针task。因此,这两个结构体是相互关联的。

    Linux为每个进程分配一个8KB大小的内存区域,用于存放该进程两个不同的数据结构:Thread_info 和进程的内核堆栈,示意图如下:



2、fork系统调用在内核中的执行过程

    当执行f ork 系统调用时,操作系统会执行以下动作:




    (1) 内核确保有创建新进程所需的充足的系统资源。其完成过程如下:

    ① 内核确保系统可以处理多个将要调度的进程,而且调度程序上的负载是可以管理的。

    ② 内核确保这个特定的用户当前没有运行过多垄断使用现有资源的进程。

    ③ 内核确保为新进程提供足够的内存空间。
    操作系统已经知道:此时新进程和父进程在各个方面都是相同的。这还包括内存要求。在交换系统中,整个内存都要是可用的。在纯分页系统中,需要大量用于保存整个地址空间和页面映射表的内存空间。在请求分页调度中,启动进程,至少页面映射表必不可少。在请求分页调度方法中,地址空间中更多的页面可以通过缺页错误累计得到。
如果内存空间不足,内核检查磁盘上是否有空间,如果有,就占用该空间的交换区。像前面在进程状态转移中讨论的一样,据此确定子进程的状态。

    (2) 内核现在从进程表中找到一个位置,然后开始构造子进程的上下文。

    (3) 内核维护"下一个可用的ID号"的全局值。任何时候,当fork系统调用创建新进程时,内核将该ID分配给新的子进程,并将该编号加1。内核还要设置一个最大值,当设置超过这个值的时候,系统就不能处理任何进程。如果该编号等于或大于这个最大值,内核从0重新分配编号,但是另一方面希望pid等于0的进程已经终止运行。

    (4) 内核初始化子进程的进程表插槽中的字段,如下:

    ① 内核将真实有效的用户ID从父进程的进程表插槽中复制到子进程对应的位置。

    ② 内核还要将父进程的准确值复制给子进程。

    ③ 内核通过将父进程ID复制到子进程插槽,从而链接进程树结构中的子进程。

    ④ 内核初始化子进程插槽中不同的调度字段和统计字段,如初始优先级、CPU使用情况等。

    ⑤ 内核将该子进程的状态设置为"正在创建"。

    (5) 现在,内核搜索父进程u区(进程信息交换区)中的文件描述符,并沿着指针从用户打开文件描述符到文件表条目,同时将文件表中那些条目的引用计数增加1。

    (6) 内核为子进程的u区、区域表、页表等分配内存空间。

    (7) 现在,除了子进程u区指向进程表插槽的指针要做适当的调整之外,内核将父进程的u区复制给子进程。这是因为父进程和子进程在进程表中有两个不同的条目。因此,指向这两个不同条目的指针也不相同。此时,所有其他内容是相同的。

    (8) 内核将数据和堆栈区(非共享的部分)复制到子进程的另一个内存区,并调整区域表条目。然而,它只保存文本区的一个副本,因为文本区是共享的。诚如所示,此时文本包含相同的程序代码。

    (9) 内核在子进程上下文的静态部分后面创建动态内容。它复制父进程上下文包含保存Fork系统调用的寄存器和内核堆栈的第一层。此时,父进程和子进程的内核堆栈的内容完全相同。

    (10) 内核创建子进程第2层的伪程序上下文,这个伪程序上下文包括第1层保存的寄存器上下文。它在寄存器内容保存区中设置程序计数器(PC)和其他寄存器,这样就可以在适当的位置"重新开始"执行子进程。

    (11) 现在,内核将子进程状态从"准备就绪"改变成"准备运行"(根据情况,要么在内存中,要么被交换)。它将子进程ID返回给用户。

    (12) 调度程序最终调度子进程。在程序中,调度程序检查它是不是子进程。因为如果是子进程,它会执行"Exec"系统调用,由此将新程序加载到子进程的地址空间中。下一节将介绍"Exec"这个系统调用。上图给出了复制的进程地址空间(包括共享的文本区)、u区(包括指向文件表的相同指针)、内核堆栈等内容。

3、exec系统调用在内核中的执行过程

    内核通过以下步骤实现该系统调用:

    (1) exec系统调用要求以参数形式提供可执行文件名,并存储该参数以备将来使用。连同文件名一起,还要提供和存储其他参数,例如如果shell命令是"ls -l",那么ls作为文件名,-l作为选项,一同存储起来以备将来使用。

    (2) 现在,内核解析文件路径名,从而得到该文件的索引节点号。然后,访问和读取该索引节点。内核知道对任何shell命令而言,它要先在/bin目录中搜索。

    (3) 内核确定用户类别(是所有者、组还是其他)。然后从索引节点得到对应该可执行文件用户类别的执行(X)权限。内核检查该进程是否有权执行该文件。如果不可以,内核提示错误消息并退出。

    (4) 如果一切正常,它访问可执行文件的头部。

    (5) 现在,内核要将期望使用的程序(例如本例中的ls)的可执行文件加载到子进程的区域中。但"ls"所需的不同区域的大小与子进程已经存在的区域不同,因为它们是从父进程中复制过来的。因此,内核释放所有与子进程相关的区域。这是准备将可执行镜像中的新程序加载到子进程的区域中。在为仅仅存储在内存中的该系统调用存储参数后释放空间。进行存储是为了避免"ls"的可执行代码覆盖它们而导致它们丢失。根据实现方式的不同,在适当的地方进行存储。例如,如果"ls"是命令,"-l"是它的参数,那么就将"-l"存储在内核区。/bin目录中"ls"实用程序的二进制代码就是内核要加载到子进程内存空间中的内容。

    (6) 然后,内核查询可执行文件(例如ls)镜像的头部之后分配所需大小的新区域。此时,建立区域表和页面映射表之间的链接。

    (7) 内核将这些区域和子进程关联起来,也就是创建区域表和P区表之间的链接。

    (8) 然后,内核将实际区域中的内容加载到分配的内存中。

    (9) 内核使用可执行文件头部中的寄存器初始值创建保存寄存器上下文。

    (10) 此时,子进程("ls"程序)已经运行。因此,内核根据子进程优先级,将其插到"准备就绪"进程列表的合适位置。最终,调度这个子进程。

    (11) 在调度该子进程后,由前述(9)中介绍的保存寄存器上下文生成该进程的上下文。然后,PC、SP等就有了正确的值。

    (12) 然后,内核跳转到PC指明的地址。这就是要执行的程序中第一个可执行指令的地址。现在开始执行"ls"这样的新程序。 内核从步骤(5)中存储的预先确定的区域中得到参数,然后生成所需的输出。如果子进程在前台执行,父进程会一直等到子进程终止;否则它会继续执行。

    (13) 子进程终止,进入僵尸状态,期望使用的程序已经完成。现在,内核向父进程发送信号,指明"子进程死亡",这样现在就可以唤醒父进程了。

如果这个子进程打开新文件,那么这个子进程的用户文件描述符表、打开文件列表和inode表结构就和父进程的不同。如果该子进程调用另一个子程序,就会重复执行/分支进程。这样就会创建不同深度层次的进程结构。


三、分析在fork产生新进程 ELF文件格式与进程地址空间的联系


1、进程的虚拟地址空间

    每个程序都有自己的虚拟地址空间(Virtual Address Space),大小由硬件平台(CPU位数)决定。 如32位平台下每个程序都有4G虚拟空间。但4G空间不是都分配给程序的用户空间,还有系统的虚拟空间。
如Linux系统默认情况下高1G为系统的虚拟地址空间,低3G为用户空间。 这也就是说每个进程原则上最多可使用3G的虚拟空间。

2、  进程装载

    覆盖装入(Overlay)和页映射(Paging)是两种典型的动态装载方法。现在前者已经不用了。   

    创建一个进程,然后装载相应的可执行文件并且执行。上述过程最开始只需要做三件事情:

①创建一个独立的虚拟地址空间。主要是分配一个页目录(Page Directory)。

②读取可执行文件的头,并且建立虚拟空间和可执行文件的映射关系。主要是把可执行文件映射到虚拟地址空间,即做虚拟页和物理页的映射,以便“缺页”时载入。

③将CPU的指令寄存器设置成可执行文件的入口地址,启动运行。从ELF文件中的入口地址开始执行程序。


3、过程分析

    
在bash下执行一个程序时,Linux是怎样装载这个ELF文件并执行的呢?
     首先bash调用fork()系统调用创建一个新的进程,然后新的进程调用execve()系统调用执行指定的ELF文件。 bash进程继续返回等待新进程执行结束,然后重新等待用户输入命令。execve()系统调用被定义在unistd.h,它的原型如下:
     int execve(const char *filenarne, char *const argv[], char *const envp[]);
     它的三个参数分别是被执行的程序文件名、执行参数和环境变最。Glibc对execvp()系统调用进行了包装,提供了execl(), execlp(), execle(), execv()和execvp()等5个不同形式的exec系列API,它们只是在调用的参数形式上有所区别,但最终都会调用到execve()这个系统中。

    调用execve()系统调用之后,再调用内核的入口sys_execve()。 sys_execve()进行一些参数的检查复制之后,调用do_execve()。 因为可执行文件不止ELF一种,还有java程序和以“#!”开始的脚本程序等, 所以do_execve()会首先检查被执行文件,读取前128个字节,特别是开头4个字节的魔数,用以判断可执行文件的格式。 如果是解释型语言的脚本,前两个字节“#!"就构成了魔数,系统一旦判断到这两个字节,就对后面的字符串进行解析,以确定程序解释器的路径。

    当do_execve()读取了这128个字节的文件头部之后,然后调用search_binary_handle()去搜索和匹配合适的可执行文件装载处理过程。Linux中所有被支持的可执行文件格式都有相应的装载处理过程,search_binary_handle()会通过判断文件头部的魔数确定文件的格式,并且调用相应的装载处理过程。如ELF用load_elf_binary(),a.out用load_aout_binary(),脚本用load_script()。其中ELF装载过程的主要步骤是:
    ①检查ELF可执行文件格式的有效性,比如魔数、程序头表中段(Segment)的数量。
    ②寻找动态链接的”.interp”段(该段保存可执行文件所需要的动态链接器的路径),设置动态链接器路径。
    ③根据ELF可执行文件的程序头表的描述,对ELF文件进行映射,比如代码、数据、只读数据。
    ④初始化ELF进程环境,比如进程启动时EDX寄存器的地址应该是DT_FINI的地址(结束代码地址)。
    ⑤将系统调用的返回地址修改成ELF可执行文件的入口点,这个入口点取决于程序的链接方式,对于静态链接的ELF可执行文件,这个程序入口就是ELF文件的文件头中e_enEry所指的地址;对于动态链接的ELF可执行文件,程序入口点是动态链接器。
    当ELF被load_elf_binary()装载完成后,函数返回至do_execve()在返回至sys_execve()。在load_elf_binary()中(第5步)系统调用的返回地址已经被改成ELF程序的入口地址了。 所以当sys_execve()系统调用从内核态返回到用户态时,EIP寄存器直接跳转到了ELF程序的入口地址,于是新的程序开始执行,ELF可执行文件装载完成。


四、实验心得


    在UNIX中,fork是进程创建另一个进程的唯一方法。只有第一个进程也就是被称作"init"的进程需要"手工创建"。所有其他进程都是用fork这个系统调用创建的。fork系统调用只是复制了父进程的数据和堆栈,并在这两个进程之间共享文本区。fork系统调用采用比较聪明的方式—"写时拷贝(copy-on-write)"技术,使得fork结束后并不立刻复制父进程的内容,而是到了真正实用的时候才复制,这样使效率大大提高。fork函数创建了一个子进程后,子进程会调用exec族函数执行另外一个程序。
    随着硬件MMU的诞生,多进程、多用户、虚拟存储的操作系统出现以后,可执行文件的装载过程变得非常复杂。引入了进程的虚拟地址空间;然后根据操作系统如何为程序的代码、数据、堆、栈在进程地址空间中分配,它们是如何分布的;最后以页映射的方式将程序映射至进程虚拟地址空间。 
    动态链接是一种与静态链接程序不同的概念,即一个单一的可执行文件模块被拆分成若干个模块,在程序运行时进行链接的一种方式。然后根据实际例子do_exece()分析了ELF装载的大致过程,中间实现了动态链接。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/351449.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

chrome面板介绍

Chrome开发者工具详解(1)&#xff1a;Elements、Console、Sources面板 Chrome 开发者工具详解(2)&#xff1a;Network 面板 Chrome开发者工具详解(3)&#xff1a;Timeline面板 Chrome 开发者工具详解(4)&#xff1a;Profiles 面板 Chrome开发者工具详解 (5)&#xff1a;Applica…

菜鸟php ajax,AJAX ASP/PHP

AJAX ASP/PHP 实例AJAX 用于创造动态性更强的应用程序。AJAX ASP/PHP 实例下面的例子将为您演示当用户在输入框中键入字符时&#xff0c;网页如何与 web 服务器进行通信&#xff1a; 请在下面的输入框中键入字母(A - Z)&#xff1a;实例Start typing a name in the input field…

没有系列化导致错误:java.io.NotSerializableException: com.bjpowernode.bean.Team

java.io.NotSerializableException: com.bjpowernode.bean.Team Cause: java.io.NotSerializableException: com.bjpowernode.bean.Player 有一个没有实现接口的转载于:https://www.cnblogs.com/Koma-vv/p/10367855.html

CPU-bound(计算密集型) 和I/O bound(I/O密集型)

I/O bound 指的是系统的CPU效能相对硬盘/内存的效能要好很多&#xff0c;此时&#xff0c;系统运作&#xff0c;大部分的状况是 CPU 在等 I/O (硬盘/内存) 的读/写&#xff0c;此时 CPU Loading 不高。 CPU bound 指的是系统的 硬盘/内存 效能 相对 CPU 的效能 要好很多&#x…

stringutils_番石榴分配器vs StringUtils

stringutils因此&#xff0c;我最近写了一篇关于旧的&#xff0c;可靠的Apache Commons StringUtils的文章 &#xff0c;该文章引起了很多评论&#xff0c;其中之一是Google Guava提供了更好的连接和拆分字符串的机制。 我必须承认&#xff0c;这是我尚未探索的番石榴的一个角落…

workerman php访问,workerman 配置域名访问 (本地)

我确定我在我的liunx 终端 运行了 php start.php start 我为workerman在本地指定了一个域名 打开域名提示 Usage: php yourfile.php {start|stop|restart|reload|status} 请问是什么情况 清楚吗&#xff1f;配置如下server {listen 80; #监听端口&#xff0c;前面可加IPserver_…

使用Spring Integration Java DSL与Rabbit MQ集成

我最近参加了在拉斯维加斯举行的2016年Spring会议 &#xff0c;很幸运地看到了我在软件世界中长期敬佩的一些人。 我亲自遇到了其中的两个人&#xff0c;他们实际上合并了几年前我与Spring Integration相关的一些小贡献– Gary Russel和Artem Bilan &#xff0c;他们启发了我重…

深度学习工程搭建

一个完整的深度学习工程项目包含数据标注&#xff0c;数据训练&#xff0c;数据预测三部分&#xff0c;逻辑结构图和技术架构如下所示&#xff1a; 这里分别从这3个方面进行展开&#xff1a; 1.数据标注&#xff1a;包含原始数据的处理和标注&#xff0c;输出为训练预测可用的网…

多线程还是多进程的选择及区别

http://blog.csdn.net/pingd/article/details/17895933 原文&#xff1a;http://blog.csdn.net/lishenglong666/article/details/8557215 最原始的博主我没有找到&#xff0c;只能把我从何处转的此篇博文的链接发出来。感觉这篇博文写的很棒&#xff0c;特此转载了 鱼还是熊掌…

php.ini settimelimit,PHP-set_time_limit()和ini_set('max_execution_time',...)之间的区别...

两种模式“ set_time_limit(5)”和“ ini_set(max_execution_time&#xff0c;5)”都重置时间&#xff0c;这是一个实用而清晰的示例&#xff1a;//-----------------------------------------------------------//test "max_execution_time":ini_set(max_execution_…

java集合的功能_功能性Java集合

java集合的功能如今&#xff0c;在功能上大肆宣传&#xff0c;因此至少在Java集合方面&#xff0c;我会简要概述一下。 我个人喜欢标准 集合API&#xff0c;但在某些情况下可能会很尴尬并添加其他详细信息。 在Java 8的更高版本中&#xff0c;这应该不是问题。 在那里&#xf…

除非另外还指定了 TOP 或 FOR XML,否则,ORDER BY 子句在视图、内联函数、派生表、子查询和公用表表达式中无效。...

原始&#xff1a; SELECT * from ( SELECT * from test order by id desc) a 解决办法&#xff1a; SELECT * from ( SELECT Top 100 Percent * from test order by id desc) a 转载于:https://www.cnblogs.com/zhaogaojian/p/10385368.html

nodejs 本地php服务器,Nodejs搭建本地http服务器

由于不做php相关的东西&#xff0c;懒得装apache&#xff0c;干脆利用nodejs搭建一个本地的服务器用于测试。nodejs这玩意儿吧&#xff0c;对做前端的介入后端简直就是一把利器。而且目前&#xff0c;nodejs也越来越有商用价值。nodejs其实是非常底层的&#xff0c;从功能上说&…

windows下如何查看磁盘IO性能

通常&#xff0c;我们很容易观察到数据库服务器的内存和CPU压力。但是对I/O压力没有直观的判断方法。磁盘有两个重要的参数&#xff1a; Seek time、 Rotational latency。正常的I/O计数为&#xff1a;①1000/(Seek timeRotational latency)*0.75&#xff0c;在此范围内属正常。…

C++ 贪吃蛇小游戏

。。 转载于:https://www.cnblogs.com/HonkerYblogs/p/10385687.html

使用Lambda,Api Gateway和CloudFormation在AWS云上使用Java

在上一篇文章中&#xff0c;我们实现了基于Java的aws lambda函数&#xff0c;并使用CloudFront进行了部署。 由于我们已经设置了lambda函数&#xff0c;因此我们将使用AWS API Gateway将其与http端点集成。 Amazon API Gateway是一项完全托管的服务&#xff0c;使开发人员可以…

oracle 索引字典,oracle数据字典、索引、序列

*************************数据字典*******************************普通的表 是用来存放 应用程序 需要的数据的student表 是存放学生信息的category 表 是存放 商品种类信息的oracle 也是一个应用程序, oracle这个系统级应用程序本身也需要存放一些数据,这些数据存到哪张表中…

从内核文件系统看文件读写过程

阅读目录 系统调用虚拟文件系统I/O 缓冲区Page CacheAddress Space文件读写基本流程 回到顶部系统调用 操作系统的主要功能是为管理硬件资源和为应用程序开发人员提供良好的环境&#xff0c;但是计算机系统的各种硬件资源是有限的&#xff0c;因此为了保证每一个进程都能安全的…

打印沙漏

7-1打印沙漏 &#xff08;20 分&#xff09; 本题要求你写个程序把给定的符号打印成沙漏的形状。例如给定17个“*”&#xff0c;要求按下列格式打印 ***** *** * *** ***** 所谓“沙漏形状”&#xff0c;是指每行输出奇数个符号&#xff1b;各行符号中心对齐&#xff1b;相…

oracle数据库读取工具,用Oracle导入导出工具实现Oracle数据库移植

用Oracle导入导出工具实现Oracle数据库移植.很不错的方法,建议使用![more]用Oracle导入导出工具实现Oracle数据库移植Oracle数据库作为目前市场的主流数据库之一&#xff0c;许多应用都在其上进行开发&#xff0c;由于Oracle数据库更新换代的原因和不同的应用程序提供商&#x…