强上Linux内核3--一个程序从开始运行到结束的完整过程,你能说出来多少?

一个程序从开始运行到结束的完整过程,你能说出来多少?

写代码:用系统调用创建进程

  • 写代码

预编译

  • 先做预处理工作,例如将头文件嵌入到正文中,将定义的宏展开

进行编译:程序的二进制格式

  • 编译成 ELF(可执行与可链接格式) 格式的二进制文件, 有三种格式(可重定位 .o 文件; 可执行文件; 共享对象文件 .so)

  • 可重定位 .o 文件(ELF 第一种格式)

    • .h + .c 文件, 编译得到可重定位 .o 文件

    • .o 文件由: ELF 头, 多个节(section), 节头部表组成(每个节有一项纪录); 节表的位置和纪录数由 ELF 头给出.
      在这里插入图片描述

    • .o 文件只是程序部分代码片段

    • .rel.text 和 .rel.data 标注了哪些函数/数据需要重定位

    • 要函数可被调用, 要以库文件的形式存在, 最简单是创建静态链接库 .a 文件(Archives)

    • 通过 ar 创建静态链接库, 通过 gcc 提取库文件中的 .o 文件, 链接到程序中

    • 链接合并后, 就可以定位到函数/数据的位置, 形成可执行文件

  • 可执行文件(ELF 第二种格式)

    • 链接合并后, 形成可执行文件
    • 同样包含: ELF 头, 多个节, 节头部表; 另外还有段头表(包含段的描述, p_vaddr 段加载到内存的虚拟地址)
      在这里插入图片描述
    • ELF 头中有 e_entry , 指向程序入口的虚拟地址
  • 共享对象 .so 文件(ELF 第三种格式)

    • 静态链接库合并进可执行文件, 多个进程不能共享
    • 动态链接库-链接了动态链接库的程序, 仅包含对该库的引用(且只保存名称)
    • 通过 gcc 创建, 通过 gcc 链接
    • 运行时, 先找到动态链接库(默认在 /lib 和 /usr/lib 找)
    • 增加了一个段, 里面是动态链接器,运行时的链接动作都是它做
    • 增加了两个节 .plt(过程链接表)和 .got.plt(全局偏移表)
    • 一个动态链接函数对应 plt 中的一项 plt[x], plt[x] 中是代理代码, 调用 got 中的一项 got[y]
    • 起始, got 没有动态链接函数的地址, 都指向 plt[0], plt[0] 又调用 got[2], got[2]指向动态链接器入口函数
    • 动态链接器入口函数找到加载到内存的动态链接函数的地址, 并将地址存入 got[y]
      (注: PLT就是用来放代理代码的,也即stub代码的,GOT是用来存放共享对象so对应的真实代码的地址的。动态链接器虽然默认会被加载,但是也是一个共享对象so,所以会放在GOT里面。要调用这个so里面的代码,也是需要从stub里面统一调用进去的,所以要回到PLT去调用。)
      在这里插入图片描述

运行程序为进程

  • 加载 ELF 文件到内存
    • 通过系统调用 exec 调用 load_elf_binary
    • exec 是一组函数
      • 包含 p: 在 PATH 中找程序
      • 不包含 p: 需提供全路径
      • 包含 v: 以数字接收参数
      • 包含 l: 以列表接收参数
      • 包含 e: 以数字接收环境变量
        在这里插入图片描述

加餐部分!!!!!!!!!!!!

进程树

- ps -ef: 用户进程不带中括号, 内核进程带中括号
- 用户进程祖先(1号进程, systemd); 内核进程祖先(2号进程, kthreadd)
- tty ? 一般表示后台服务

在这里插入图片描述


谈谈你对动态链接库和静态链接库的理解?

静态链接就是在编译链接时直接将需要的执行代码拷贝到调用处,优点就是在程序发布的时候就不需要的依赖库,也就是不再需要带着库一块发布,程序可以独立执行,但是体积可能会相对大一些。

动态链接就是在编译的时候不直接拷贝可执行代码,而是通过记录一系列符号和参数,在程序运行或加载时将这些信息传递给操作系统,操作系统负责将需要的动态库加载到内存中,然后程序在运行到指定的代码时,去共享执行内存中已经加载的动态库可执行代码,最终达到运行时连接的目的。优点是多个程序可以共享同一段代码,而不需要在磁盘上存储多个拷贝,缺点是由于是运行时加载,可能会影响程序的前期执行性能


静态链接:后缀是.a,主要在编译的时候将库文件里面代码搬迁到可执行的文件中;

动态链接:后缀是.so,主要在执行的时候需要转换到库文件代码执行;

两种链接的优缺点

(1)静态的链接产生的可执行的文件体积比较的大;而动态链接的可执行文件的体积比较小;

(2)动态的链接的编译的效率比较的高;

(3)静态链接的可执行的文件执行的效率高

(4)静态链接的可执行的文件的“布局”比较好一点;


进程终止的方式

进程的终止

进程在创建之后,它就开始运行并做完成任务。然而,没有什么事儿是永不停歇的,包括进程也一样。进程早晚会发生终止,但是通常是由于以下情况触发的

  • 正常退出(自愿的)
  • 错误退出(自愿的)
  • 严重错误(非自愿的)
  • 被其他进程杀死(非自愿的)

正常退出

多数进程是由于完成了工作而终止。当编译器完成了所给定程序的编译之后,编译器会执行一个系统调用告诉操作系统它完成了工作。这个调用在 UNIX 中是 exit ,在 Windows 中是 ExitProcess。面向屏幕中的软件也支持自愿终止操作。字处理软件、Internet 浏览器和类似的程序中总有一个供用户点击的图标或菜单项,用来通知进程删除它锁打开的任何临时文件,然后终止。

错误退出

进程发生终止的第二个原因是发现严重错误,例如,如果用户执行如下命令

cc foo.c

为了能够编译 foo.c 但是该文件不存在,于是编译器就会发出声明并退出。在给出了错误参数时,面向屏幕的交互式进程通常并不会直接退出,因为这从用户的角度来说并不合理,用户需要知道发生了什么并想要进行重试,所以这时候应用程序通常会弹出一个对话框告知用户发生了系统错误,是需要重试还是退出。

严重错误

进程终止的第三个原因是由进程引起的错误,通常是由于程序中的错误所导致的。例如,执行了一条非法指令,引用不存在的内存,或者除数是 0 等。在有些系统比如 UNIX 中,进程可以通知操作系统,它希望自行处理某种类型的错误,在这类错误中,进程会收到信号(中断),而不是在这类错误出现时直接终止进程。

被其他进程杀死

第四个终止进程的原因是,某个进程执行系统调用告诉操作系统杀死某个进程。在 UNIX 中,这个系统调用是 kill。在 Win32 中对应的函数是 TerminateProcess(注意不是系统调用)。


守护进程、僵尸进程和孤儿进程

守护进程
指在后台运行的,没有控制终端与之相连的进程。它独立于控制终端,周期性地执行某种任务。Linux的大多数服务器就是用守护进程的方式实现的,如web服务器进程http等

创建守护进程要点:

(1)让程序在后台执行。方法是调用fork()产生一个子进程,然后使父进程退出。

(2)调用setsid()创建一个新对话期。控制终端、登录会话和进程组通常是从父进程继承下来的,守护进程要摆脱它们,不受它们的影响,方法是调用setsid()使进程成为一个会话组长。setsid()调用成功后,进程成为新的会话组长和进程组长,并与原来的登录会话、进程组和控制终端脱离。

(3)禁止进程重新打开控制终端。经过以上步骤,进程已经成为一个无终端的会话组长,但是它可以重新申请打开一个终端。为了避免这种情况发生,可以通过使进程不再是会话组长来实现。再一次通过fork()创建新的子进程,使调用fork的进程退出。

(4)关闭不再需要的文件描述符。子进程从父进程继承打开的文件描述符。如不关闭,将会浪费系统资源,造成进程所在的文件系统无法卸下以及引起无法预料的错误。首先获得最高文件描述符值,然后用一个循环程序,关闭0到最高文件描述符值的所有文件描述符。

(5)将当前目录更改为根目录。

(6)子进程从父进程继承的文件创建屏蔽字可能会拒绝某些许可权。为防止这一点,使用unmask(0)将屏蔽字清零。

(7)处理SIGCHLD信号。对于服务器进程,在请求到来时往往生成子进程处理请求。如果子进程等待父进程捕获状态,则子进程将成为僵尸进程(zombie),从而占用系统资源。如果父进程等待子进程结束,将增加父进程的负担,影响服务器进程的并发性能。在Linux下可以简单地将SIGCHLD信号的操作设为SIG_IGN。这样,子进程结束时不会产生僵尸进程。

孤儿进程
如果父进程先退出,子进程还没退出,那么子进程的父进程将变为init进程。(注:任何一个进程都必须有父进程)。

一个父进程退出,而它的一个或多个子进程还在运行,那么那些子进程将成为孤儿进程。孤儿进程将被init进程(进程号为1)所收养,并由init进程对它们完成状态收集工作。

僵尸进程
如果子进程先退出,父进程还没退出,那么子进程必须等到父进程捕获到了子进程的退出状态才真正结束,否则这个时候子进程就成为僵尸进程。

设置僵尸进程的目的是维护子进程的信息,以便父进程在以后某个时候获取。这些信息至少包括进程ID,进程的终止状态,以及该进程使用的CPU时间,所以当终止子进程的父进程调用wait或waitpid时就可以得到这些信息。如果一个进程终止,而该进程有子进程处于僵尸状态,那么它的所有僵尸子进程的父进程ID将被重置为1(init进程)。继承这些子进程的init进程将清理它们(也就是说init进程将wait它们,从而去除它们的僵尸状态)。


如何避免僵尸进程?

  • 通过signal(SIGCHLD, SIG_IGN)通知内核对子进程的结束不关心,由内核回收。如果不想让父进程挂起,可以在父进程中加入一条语句:signal(SIGCHLD,SIG_IGN);表示父进程忽略SIGCHLD信号,该信号是子进程退出的时候向父进程发送的。
  • 父进程调用wait/waitpid等函数等待子进程结束,如果尚无子进程退出wait会导致父进程阻塞。waitpid可以通过传递WNOHANG使父进程不阻塞立即返回。
  • 如果父进程很忙可以用signal注册信号处理函数,在信号处理函数调用wait/waitpid等待子进程退出。
  • 通过两次调用fork。父进程首先调用fork创建一个子进程然后waitpid等待子进程退出,子进程再fork一个孙进程后退出。这样子进程退出后会被父进程等待回收,而对于孙子进程其父进程已经退出所以孙进程成为一个孤儿进程,孤儿进程由init进程接管,孙进程结束后,init会等待回收。

第一种方法忽略SIGCHLD信号,这常用于并发服务器的性能的一个技巧因为并发服务器常常fork很多子进程,子进程终结之后需要服务器进程去wait清理资源。如果将此信号的处理方式设为忽略,可让内核把僵尸子进程转交给init进程去处理,省去了大量僵尸进程占用系统资源。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值