《Linux内核 核心知识全解析(完)【2】》

把当前栈的寄存器内容等,压到另外一个叫“内核栈”的栈里面去

把EIP指向一个叫做中断处理程序的入口,做保护现场的工作;然后执行中断处理程序;

mykernel:模拟了时钟中断——只有一个程序,隔一段时间就中断一次

在此基础上实现了一个极小的 基于时间片轮转的多进程调度 内核

 

 

 

 系统调用是一种特殊的中断,存在保护现场和恢复现场的问题

SAVE_ALL

sys_call_table:系统调用表

 

操作系统内核三大功能:

进程管理

内存管理

文件系统

 task_struct 400多行代码。。。

 

1、R
处于运行或可运行状态,即进程正在运行或在运行队列(可执行队列)中等待。只有在该状态的进程才可能在CPU上运行,同一时刻可能有多个进程处于该状态。
(注:很多教科书上将正在CPU上执行的进程的状态定义为Running,将可执行但尚未被调度执行的进程状态定义为Ready,这2种状态在Linux下统一为R状态)
2、S
处于可中断的睡眠状态,即进程在休眠中,由于在等待某个事件的完成(或等待某个条件的形成或等待某个信号等)
(注:等待socket连接、等待信号量等)而被挂起;当这些事件发生时,对应的等待队列中的一个或多个进程将被唤醒。一般情况下,进程列表中绝大多数进程都处于该状态。 
3、D
处于不可中断的睡眠状态,不可中断指的并不是CPU不响应外部硬件的中断,而是指进程不响应异步信号,无法用kill命令杀死,进程必须等待直到有中断发生。
4、T
处于暂停或跟踪状态。进程收到SIGSTOP、SIGSTP、SIGTIN、SIGTOU等信号进入暂停状态(除非进程处于不可中断的睡眠状态);当接着向进程发送1个SIGCONT信号,进程可以从暂停状态恢复到运行或能运行状态。
当进程被跟踪时,它处于被跟踪状态。“被跟踪”指进程暂停下来,等待跟踪它的进程对它进行操作。例如在GDB调试中,对被跟踪的进程设置某个断点,进程执行到断点处停下来的时候就处于被跟踪状态。

暂停与跟踪状态还是有区别的,被跟踪状态相当于在暂停状态之上多了一层保护,处于被跟踪状态的进程不能响应SIGCONT信号而被唤醒,只能等到调试进程通过ptrace系统调用执行ptrace_cont、ptrace_detach等操作(通过ptrace系统调用的参数指定操作),或调试进程退出,被调试的进程才能恢复到R状态。
5、Z
处于僵死状态,也称退出状态。它指进程已经结束,放弃了几乎所有的内存空间,没有任何可执行代码,也不能被调度,仅仅在进程列表中保留一个位置来记载该进程的退出状态等信息(task_struct结构体[保存了该进程的退出码])供其他进程收集。
6、X
进程在退出过程中可能不会保留它的task_struct。例如某个进程是多线程程序中被detach过的进程;或者父进程通过设置SIGCHLD信号的Handler为SIG_IGN,显示的忽略了SIGCHLD信号。
此时该进程被置于exit_dead退出状态,这意味着接下来的代码立即会将该进程彻底释放。故exit_dead状态非常短暂,几乎不可能通过ps命令捕捉到。
————————————————
原文链接:https://blog.csdn.net/baidu_37964071/article/details/79663658

 
   

 

 

 所有的进程用 list_head *tasks 链表保存

 mm:物理地址、逻辑地址转换 ... MMU 内存管理单元 ...  

每个进程有自己独立的进程地址空间, x86 32位,4G

进程地址空间 -> 分段 -> 分页,转换为物理地址 ... 

struct mm_struct *mm, *active_mm;

vm_area_struct *vmacache ... 

 thread_struct:

该系统调用所需要的参数pt_regs在include/asm-i386/ptrace.h文件中定义:
struct pt_regs {
long ebx;                  //可执行文件路径的指针(regs.ebx中
long ecx;                  //命令行参数的指针(regs.ecx中)
long edx;                  //环境变量的指针(regs.edx中)。
long esi;
long edi;
long ebp;
long eax;
int xds;
int xes;
long orig_eax;
long eip;
int xcs;
long eflags;
long esp;
int xss;
};
该 参数描述了在执行该系统调用时,用户态下的CPU寄存器在核心态的栈中的保存情况( SAVE_ALL )。通过这个参数,sys_execve能获得保存在用户空间的以下信息: 可执行文件路径的指针(regs.ebx中)、命令行参数的指针(regs.ecx中)和环境变量的指针(regs.edx中)。

 
 

 

 

P3704_创建的新进程是从哪里开始执行的? 

 syscall_exit 后,用户态已经是返回的子进程了 (P3704_创建的新进程是从哪里开始执行的? )

动手调试:

make rootfs

输入fork,子进程已经创建了

设置断点 b sys_clone

关于Linux中fork、vfork、clone的一些个人见解

我在网上看到很多博客上面说:

关于这三个函数的调用过程是这样的:

fork->sys_fork->do_fork vfork->sys_vfork->do_fork clone->sys_clone->do_fork

但是我很久之前在陈莉君老师写的Linux内核设计与实现 看到过这么一句话:

Linux通过clone系统调用实现fork.调用通过一系列的参数标志来指明父、子进程需要共享的资源。fork、vfork、和__clone的库函数都根据各自需要的参数标志去调用clone,然后由clone()去调用do_fork()。

然后我一直也是这么认为的,但是网上的博客看多了,有时候会去怀疑一下书本上的知识。于是我先是写了连个测试用例,分别调用了fork和 pthread_create,然后利用strace跟踪两者的调用过程,发现果然都是调用的clone。

而后,我还是不满足,便去查阅了一番glibc的源码。

pid_t
__libc_fork (void)
{
  pid_t pid;
 
    /*
     这里省去了很多和本次目的无关的代码,我也没阅读
   */

  /* We need to prevent the getpid() code to update the PID field so
     that, if a signal arrives in the child very early and the signal
     handler uses getpid(), the value returned is correct.  */
  pid_t parentpid = THREAD_GETMEM (THREAD_SELF, pid);
  THREAD_SETMEM (THREAD_SELF, pid, -parentpid);
/* 
注意,直到这里,我们发现,如果定义的是ARCH_FORK,调用的是ARCH_FORK(),至于ARCH是什么的,自己去百度吧。
下面是ARCH_FORK ()宏函数的展开:
#define ARCH_FORK() 
  INLINE_SYSCALL (clone, 5,						      
		  CLONE_CHILD_SETTID | CLONE_CHILD_CLEARTID | SIGCHLD, 0,     
		  NULL, &THREAD_SELF->tid, NULL)
我们可以清楚的看见最终是通过syscall调用的clone。
*/
#ifdef ARCH_FORK
  pid = ARCH_FORK (); 
#else
# error "ARCH_FORK must be defined so that the CLONE_SETTID flag is used"
  pid = INLINE_SYSCALL (fork, 0);  //调用sys_fork
#endif

  if (pid == 0)
    {
        身为子进程的一些处理细节
    }
  else
    {
        身为父进程的一些处理细节
    }

  return pid;
}

最终的结论是:在Linux操作系统下,

arch(X86)架构的是:fork、vfork、和__clone的库函数最终调用的都是clone系统调用。

至于其它的架构的,可能是通过fork和vfork系统调用。这和本身的实现有关。当然在现在的大多数Linux内核中,就算调用的是fork,在底层基本上传递给do_fork的参数都带有能实现写时复制的一些标志。

最后提一点,写博客的人能不能不要那么浮躁,大家都抄来抄去,一点都不负责任。

发布于 2019-03-12
 

浅谈Linux进程模型

写在前面

  • 进程基础
    • 进程概念
    • 进程描述符
    • 进程创建
    • 上下文切换
    • init进程
  • 进程应用
    • 进程间通信
    • 信号处理
    • 后台进程与守护进程
    • 浅谈nginx多进程模型
  • 常用工具介绍
    • ps: 查看进程属性
    • lsof: 查看打开的文件情况
    • netstat: 查看网络连接情况
    • strace: 查看系统调用情况

进程基础

基础概念

进程是操作系统的基本概念之一,它是操作系统分配资源的基本单位,也是程序执行过程的实体。程序是代码和数据的集合,本身是一个静态的概念,而进程是程序的一次执行的实体,是一个动态的概念。

那在Linux操作系统中,是如何描述一个进程的呢?

进程描述符

为了管理进程,内核需要对每个进程的属性和所需要做的事情,进行清楚的描述,这个就是进程描述符的作用,Linux中的进程描述符由task_struct标识。

task_struct的数据结构是相当复杂的,不仅包含了很进程属性的字段,而且也包括了指向其他数据结构的指针。大致结构如下:

  • state: 描述进程状态
  • thread_info: 进程的基本信息
  • mm: mm_struct指向内存区描述符的指针
  • tty: tty_struct终端相关的描述符
  • fs: fs_struct当前目录
  • files: files_struct指向文件描述符的指针
  • signal: signal_struct所接收的信号描述
  • 很多等等。。

总结一下,进程描述符完整的保存了一个进程的属性和生命周期内的数据、状态和行为,由一个复杂的数据结构task_struct来表示。

进程创建

Linux创建一个进程,大致经历的过程如下:

    1. 初始化进程描述符
    2. 申请相应的内存区域
    3. 设置进程状态、加入调度队列等等
    4. ...

为了完整的描述一个进程,操作系统设计了非常复杂的数据结构、也申请了大量的内存空间。但是得益于写时复制技术,这些初始化操作,并没有明显的降低进程的创建速度。

写时复制技术:当新进程(子进程)被创建时,Linux内核并不会立马将父进程的内容复制给子进程,而仅仅当进程空间的内容发生变化时,才执行复制操作。写时复制技术允许父子进程读取相同的物理页,只要两者有一个试图更改页内容,内核就会把这个页的内容拷贝到新的物理页,并把这块页分给正在写的进程

Linux中有三种系统调用可以创建进程 clone()、fork()、vfork()

  • clone(): 最基础的创建进程的系统调用,可以指明子进程的基础属性(由各种FLAG标识)、堆栈等等。
  • fork(): 通过clone()实现,它的堆栈指向的是父进程的堆栈,因此父子进程共享同一个用户态堆栈。fork的子进程需要完全copy父进程的内存空间,但是得益于写时复制技术,这个过程其实挺快。
  • vfork(): 也是基于clone()来实现的,是历史上对fork()的优化,因为fork()需要copy父进程的内存空间,并且fork()后常常执行execve()将另一个程序加载进来,在写时复制技术之前,这种不必要的copy是代价是比较高昂的。因此vfork()实现时,会指明flag告诉clone()共享父进程的虚拟内存空间,以加快进程的创建过程。

上下文切换

概念:进程创建好之后,内核必须有能力挂起正在CPU运行的进程,并切换其他进程到CPU上执行。这种过程被称作为进程切换、任务切换或者上下文切换。

这个过程包括硬件上下文切换和软件上下文切换。

硬件上下文切换:主要通过汇编指令far jmp操作,将一个进程的描述符指针,替换为另一个进程描述符指针,并改变 eip、cs、esp等寄存器,从而改变程序的执行流。

软件上下文切换:

    1. 内存地址的切换,切换页全局目录,安装新的地址空间。
    2. 内核态堆栈的切换。

进程切换发生在schedule()函数中,内核提供了一个 need_resched 的标志,来表明是否需要重新执行一次调度。当某个进程被抢占或者更高优先级的进程进入可执行状态时,内核都会设置这个标志。那什么时候,内核会检查这个标志,来重新调度程序呢?那就是从内核态切换成用户态,或者从中断返回时

执行系统调用时,会经历用户态与内核态的切换以及中断返回。也就是说,每一次执行系统调用,比如fork、read、write等,都可能触发内核调度新进程

init进程

Linux进程是以树形的结构组织的,每一个进程都有唯一的进程标识,简称PID。PID为1的常常是init进程,它相对于普通进程来说,有三个特殊之处:

  • 它没有默认的信号处理,因此如果发信号给init进程的话,会被它忽略掉,除非显示的注册过该信号。如果熟悉docker的同学,会观察到docker化的进程,如果按ctrl-c是没啥反应的,因为docker化的进程它们有独立的pid命名空间,第一个新创出的进程,pid为1,是不会理会kill signal信号的。
  • 如果一个进程退出时,它还有子进程存在,被称为孤儿进程,那么这些孤儿进程会重新成为init进程的子进程,转由init进程来管理这些子进程,包括回收退出状态、从进程表中移除等。
  • 如果init进程跪了,那么所有用户进程都会被退出。

与孤儿进程类似的是僵尸进程,清理僵尸进程的方法,是杀掉不断产生僵尸进程的父进程,然后这些僵尸进程会称为孤儿进程,由init进程接管、回收。

进程应用

进程间通信

谈到通信我们都知道,通信的双方必须存在一种可以承载信息的介质,对于计算机之间的通信来说,这种介质可以是双绞线、光纤、电磁波。那对于进程间的通信呢?这种介质有哪些呢?在Linux中,满足这种条件的介质,可以是:

  • 操作系统提供的内存介质,比如共享内存、管道、信号量等。
  • 文件系统提供的文件介质,比如UNIX域套接字、文件
  • 网络设备提供的网卡介质,比如socket套接字
  • 等等。

对于操作系统提供的介质来说,常用的有

  • 信号量机制
  • 匿名管道(仅限父子进程)与有名管道
  • SysV和POSIX
    • 消息队列
    • 共享内存
  • 等等

命名管道(FIFO)
上述管道虽然实现了进程间通信,但是它具有一定的局限性:首先,这个管道只能是具有血缘关系的进程之间通信;第二,它只能实现一个进程写另一个进程读,而如果需要两者同时进行时,就得重新打开一个管道。
为了使任意两个进程之间能够通信,就提出了命名管道(named pipe 或 FIFO)。
1、与管道的区别:提供了一个路径名与之关联,以FIFO文件的形式存储于文件系统中,能够实现任何两个进程之间通信。而匿名管道对于文件系统是不可见的,它仅限于在父子进程之间的通信
2、FIFO是一个设备文件,在文件系统中以文件名的形式存在,因此即使进程与创建FIFO的进程不存在血缘关系也依然可以通信,前提是可以访问该路径。
3、FIFO(first input first output)总是遵循先进先出的原则,即第一个进来的数据会第一个被读走。
————————————————
原文链接:https://blog.csdn.net/qq_33951180/article/details/68959819

优缺点介绍:

  • 信号量:不能传递复杂消息,只能用来同步
  • 匿名管道:容量有限速度较慢,只有父子进程能通讯
  • 有名管道:任何进程间都能通讯,但速度较慢。
  • 消息队列:容量受到系统限制,有队列的特性,先进先出。
  • 共享内存:速度快,可以控制容量大小,但需要进行同步操作。

它们的用法相对较为简单,在需要使用时查阅相关文档即可,共享内存是比较常用的做法。

信号处理

信号最早是在Unix系统被引入,它主要用于进程间的通信,同时进程可以主动注册信号处理函数,来检测或者应对系统发生的事件。比如当进程访问非法地址空间时,进程会收到操作系统发送SIGSEGV信号,默认情况下的处理方式是:该进程会退出并且把堆栈dump出来,简称出core。

总的来说信号的主要目的:

  • 让进程知道已经发生的特定事件。
  • 强迫进程处理这个特定事件。

目前Linux支持的信号,已经默认的处理函数,可以在man手册中查到,截图如下:

比较常见的信号,解释如下:

  • SIGCHLD: 一个进程通过 fork 函数创建,当它结束时,会向父进程发送SIGCHLD信号。
  • SIGHUP: 挂起信号,当检测到控制终端,或者控制进程死亡时。比如用户退出shell终端时,该shell启动的所有进程,都会收到这个信号,默认是终止进程。
  • SIGINT:当用户按下Ctrl+C组合键时,终端会向该进程发送此信号,默认是终止进程。
  • SIGKILL: 常用的kill -9指令会发送该信号,无条件终止进程,本信号无法被忽略。
  • SIGSEGV: 进程访问了非法的内存地址,默认行为是终止进程并产生core堆栈。
  • SIGTERM: 程序结束信号,该信号可以被阻塞和忽略,通常标识程序正常退出。
  • SIGSTOP: 停止进程的执行,该信号不能被忽略,默认动作为暂停进程。
  • SIGPIPE: 当往一个写端关闭的管道或socket连接中连续写入数据时会引发SIGPIPE信号,引发SIGPIPE信号的写操作将设置errno为EPIPE。在TCP通信中,当通信的双方中的一方close一个连接时,若另一方接着发数据,根据TCP协议的规定,会收到一个RST响应报文,若再往这个服务器发送数据时,系统会发出一个SIGPIPE信号给进程,告诉进程这个连接已经断开了,不能再写入数据。

其实在项目开发中,常常会和信号处理打交道。比如在处理程序优雅退出时,一般需要捕获SIGINT、SIGPIPE、SIGTERM等信号,以合理的释放资源、处理剩余链接等,防止程序意外crash,导致的一些问题。

后台进程与守护进程

在接触Linux系统时,常常会遇到后台进程与守护进程,这里简单的介绍一下这两种进程。

  • 后台进程:通常情况下,进程是放置在前台执行,并占据当前shell,在进程结束前,用户无法再通过shell做其他操作。对于那些没有交互的进程,可以将其放在后台启动,也就是启动时加一个 &,那么在该进程运行期间,我们仍是可以通过shell操作其他命令。不过当shell退出时,该后台进程也会退出。
  • 守护进程:如果一个进程总是以后台的方式启动,并且不能受shell退出的影响而退出,那么可以将其改造为守护进程。后续进程是系统长期运行的后台进程,比如mysqld、nginx等常见的服务进程。

那么这两者有啥区别呢?

  • 守护进程已经完全脱离终端,而后台进程并未完全脱离终端,即后台进程仍是可以输出到终端的。
  • 在终端关闭时,后台进程会收到信号退出,但是守护进程则不会。

举个例子,通过./spider &在后台执行抓取任务,但没过多久,终端自动断开,导致spider进程中断退出。

在进一步了解守护进程之前,还需要了解一些会话和进程组的概念。

  • 进程组:由一系列相互关联的进程组成,由PGID来标识,一般是进程组创建进程的PID。进程组的存在是为了方便对多个相关进程执行统一的操作,比如发送信号量给统一进程组的所有进程。
  • 会话:由若干个进程组组成,每一个进程组从属于一个会话,一个会话对应着一个控制终端,该终端为会话所有进程组的进程所共用,其中只有前台进程组才可以与终端交互。

那如何实现一个守护进程呢?

  1. 在后台运行:fork出子进程A,当前进程退出,保留子进程A。
  2. 脱离控制终端:目的是摆脱终端的影响,通过setsid()重新为子进程A设置新的会话。
  3. 禁止子进程A重新打开终端:因为设置新会话之后的进程A,是进程组的组长,所以它是有能力重新申请打开一个控制终端。通过再次fork子进程B,并退出进程A,B不再是进程组组长,也无法打开新的终端。
  4. 关闭已打开的文件描述符、改变工作目录等等。
  5. 处理SIGCHILD信号:由于守护进程一般是长期运行的进程,当产生子进程时,需要处理子进程退出时发送的SIGCHILD信号,不然子进程就会变成僵尸进程,从而占据系统资源。

总结来说,守护进程是一种长期运行于后台的进程,它脱离了控制终端,不受用户终端退出的影响。可以通过nohup操作,将一个进程变成守护进程执行比如nohup ./spider &,这样即使终端断开后,spider进程仍会继续执行

浅谈nginx多进程模型

nginx是一款高性能的Web服务器,由于它优秀的性能、成熟的社区、完善的文档,受到广大开发者的喜爱和支持。它的高性能与其架构是分不开的,nginx的框架如下图所示:

nginx架构图-来源于网上

Nginx是经典的多进程模型,它启动以后以守护进程的方式在后台运行,后台进程包含一个master进程,和多个worker进程。其中master进程相当于控制进程,有以下作用:

  • 接收外界信号执行指令,包括配置加载、向worker发指令、优雅退出等等。
  • 维护worker进程的状态,当worker进程退出后,自动启动新的worker。

其中 master 进程支持的信号处理如下:

  • TERM、INT:快速退出
  • QUIT:优雅退出
  • HUP: 变更配置,用新配置启动worker,优雅关闭老的worker等。
  • USR1: 重新打开日志文件
  • USR2: 升级二进制文件(nginx升级)
  • WINCH: worker进程的优雅退出

单个worker进程也支持信号处理,包括:

  • TERM、INT: 快速退出
  • QUIT: 优雅退出
  • USR1: 重新打开日志文件
  • WINCH: 终端调试等

worker进程基于异步非阻塞的模式处理每个请求,这种非阻塞的模式,大大提高了worker进程处理请求的速度。为了尽可能的提高性能,nginx对每个worker进程设置了CPU的亲和性,尽量把worker进程绑定在指定的CPU上执行,以减少上下文切换带来的开销。由于这种绑核的模式,一般推荐worker进程的数目,为CPU的核数。

nginx使用了master<->worker这种多进程的模型,有哪些好处呢?

  • worker进程间很少共享资源,在处理各自请求时,几乎不用加锁,省掉了锁带来的开销。
  • worker进程间异常不会相互影响,一个进程挂掉之后,其他进程还在工作,可以提高服务的稳定性。
  • 尽可能的利用多核特性,最大化利用系统资源。

更多内容来源于:如何理解和应用Linux进程模型?

常用工具介绍

Linux内置了许多工具,用于排查系统问题和查看资源使用情况,这里简单介绍和进程有关的几个工具。

ps: 查看进程的基本属性

lsof: 查看进程打开的文件情况

有两个场景:

  • 场景一:机器上一个文件大小不停的增长,导致磁盘空间一次又一次的爆满,如果这时候你想把写文件的罪魁祸首进程找到,那应该怎么做呢?
  • 场景二:发现磁盘已经快满了,通过rm -f 删除一些大文件,但磁盘空间并没有明显减少,这个时候应该怎么做呢?

对于这些场景,我们可以借助lsof命令,

  • 对于场景一来说:可以查看该文件被哪个进程打开,找到罪魁祸首进程,然后对其处理。
  • 对于场景二来说:如果这个文件被其他进程打开,通过rm -f是无法真正删掉一个文件的,还需要杀掉打开该文件的进程,以关闭文件描述符,那么文件才能真正被清理。

lsof的常见用法如下:

  • 查看特定用户打开的文件列表:lsof -u xxx
  • 查看特定端口打开的文件列表:lsof -i 8080
  • 查看特定端口范围打开的文件列表:lsof -i :1-1024
  • 基于TCP或者UDP查看打开的文件列表:lsof -i udp
  • 查看特定进程打开的文件列表:lsof -p $pid
  • 查看打开特定文件的进程列表:lsof -t $file_name
  • 查看打开特定目录的进程列表:lsof +D $file_path
  • 等等

netstat: 查看网络连接情况

strace: 查看系统调用情况

发布于 2019-03-27
 
 
原文地址:https://www.cnblogs.com/cx2016/p/13090377.html