二、WebServer-多进程开发

目录 

 2.1进程概述

01/程序和进程

程序是包含一系列信息的文件,这些信息描述了如何运行时创建一个进程:

  • 二进制格式标识:每个程序文件都包含用于描述可执行文件格式的元信息。内核利用此信息来解释文件中的其他信息。(ELF可执行连接格式)  
  • 机器语言指令:对程序算法进行编码。
  • 程序入口地址:标识程序开始执行时的起始位置。
  • 数据:程序文件包含的变量初始值和程序使用的字母量值(比如字符串)。
  • 符号表及重定位表:描述程序中函数和变量的位置及名称。这些表格有多重用途,其中包括调试和运行时的符号解析(动态链接)。
  • 共享库和动态链接信息:程序文件所包含的一些字段,列出了程序运行时需要使用的共享库,以及加载共享库的动态连接器的路径名。  
  • 其他信息:程序文件还包含许多其他信息,用以描述如何创建进程。  

进程

  • 进程是正在运行的程序的实例。是一个具有一定独立功能的程序关于某个数据集合的一次运行活动。
  • 它是操作系统动态执行的基本单元,在传统的操作系统中,进程既是基本的分配单元,也是基本的执行单元。
  • 可以用一个程序来创建多个进程,进程是由内核定义的抽象实体,并为该实体分配用以执行程序的各项系统资源。从内核的角度看,进程由用户内存空间和一系列内核数据结构组成,其中用户内存空间包含了程序代码及代码所使用的变量,而内核数据结构则用于维护进程状态信息。

02/单道、多道程序设计

  • 单道程序,即在计算机内存中只允许一个的程序运行。每次CPU上运行的程序只有一个。
  • 多道程序设计技术是在计算机内存中同时存放几道相互独立的程序。引入多道程序设计技术的的根本目的是为了提高CPU的利用率。多个进程轮流使用CPU。

03/时间片

  • 时间片(timeslice)又称为“量子(quantum)”或“处理器片(processor slice)”是操作系统分配给每个正在运行的进程微观上的一段CPU时间。
  • 时间片由操作系统内核的调度程序分配给每个进程。

04/并发和并行

  • 并行(parallel):指在同一时刻,有多条指令在多个处理器上同时执行。
  • 并发(concurrency):指在同一时刻只能有一条指令执行,但多个进程指令被快速的轮换执行,宏观上同时执行,微观上并不是同时执行,只是把时间分成若干段,使多个进程快速交替的执行。

05/进程控制块(PCB

  • 为了管理进程,内核必须对每个进程所作的事情进行清楚的描述。内核为每个进程分配一个PCB(Processing Control Block)进程控制块,维护进程相关信息,Linux内核的进程控制块是task_struct结构体。里面的内容有:
    • 进程id:系统中每个进程有唯一的id,用pid_t类型表示,其实就是一个非负整数。
    • 进程的状态:有就绪、运行、挂起、停止等状态
    • 进程切换时需要保存和恢复的一些CPU寄存器
    • 描述虚拟地址控制的信息
    • 描述控制终端的信息
    • 当前工作目录(Current Working Directory)
    • umask掩码
    • 文件描述符表,
    • 和信号相关的信息
    • 用户id、组id;会话(Session)、进程组
    • 进程可以使用的资源上限(Resource Limit)

进程是系统资源分配的最小单元,一个进程可以有多个线程。

内核和用户软件同时只有一个在运行,但是会发生任何切换。

2.2进程状态转换

01/进程的状态

进程的状态反映进程执行过程的变化。三态模型中,进程分为就绪态、运行态、阻塞态,五态模型中,进程分为新建态、就绪态、运行态、阻塞态、终止态。

  • 运行态:进程占有处理器正在运行
  • 就绪态:进程具备运行条件,等待系统分配处理器以便运行。在一个系统中处于就绪状态的进程可能有多个,通常将它们排成一个队列,称为就绪队列
  • 阻塞态:又称为等待(wait)态或睡眠(sleep)态,指进程不具备运行条件,正在等待某个事件的完成
  • 新建态:进程刚被创建时的状态,尚未进入就绪队列。
  • 终止态:进程完成任务到达正常结束点,或出现无法克服的错误而异常终止,或被操作系统及有终止权的进程所终止时所处的状态。

02/进程相关命令

  • 查看进程
ps aux / ajx  

 ​​​​​​​a:显示终端上的所有进程,包括其他用户的进程  

u:显示进程的详细信息  

x:显示没有控制终端的进程  

j:列出与作业控制相关的信息

  •  STAT参数意义: 

D 不可中断Uninterruptible(usually IO)          R 正在运行,或在队列中的进程  

S(大写) 处于休眠状态          T 停止或被追踪          Z 僵尸进程  

W 进入内存交换(从内核2.6开始无效)          X 死掉的进程          < 高优先级  

N 低优先级          s 包含子进程          + 位于前台的进程组​​​​​​​

top //实时显示进程动态 
kill [-signal] pid  //杀死进程
kill –l //列出所有信号  
kill –SIGKILL //进程ID  
kill -9 //进程ID  

杀死进程(kill名并不是去杀死一个进程,而是给进程发送某个信号)

03/进程组和相关函数

  • 每个进程都由进程号来标识,其类型为pid_t(整型),进程号的范围:0~32767,进程号总是唯一的,但可以重用。当一个进程终止后,其进程号就可以再次使用。  
  • 任何进程(除init进程)都是由另一个进程创建,该进程称为被创建进程的父进程,对应父进程号(PPID)。
  • 进程组是一个或多个进程的集合。关联的进程组有一个进程组号(PGID)。
  • 除了init进程外,每个进程都有父进程PPID。
pid_t getpid(void);  //获取当前进程ID
pid_t getppid(void);  //获取当前进程父ID
pid_t getpgid(pid_t pid); //如果传None获取当前进程的进程组id,如果传进程号获取进程号的进程组ID

2.3进程创建

01/进程创建

系统允许一个进程创建新进程,新进程即为子进程,子进程还可以创建新的子进程,形成进程树结构模型。

pid_t fork(void); //创建进程  //读时共享 写时拷贝  //fork返回值会返回两次,父和子进程
//pid_t pid = fork(); //pid > 0时,执行父进程代码,返回pid为子进程ID;pid = 0时,执行子进程代码;
//在父进程中返回创建的子进程的ID,

补充读时共享、写时拷贝的原理:  

Linux 的 fork() 使用是通过写时拷贝实现。写时拷贝是一种可以推迟甚至避免拷贝数据的技术。内核并不复制整个进程的地址空间,而是让父子进程共享同一个地址空间。只有在写入时才会复制地址空间(重新开辟一块内存),从而使各个进程拥有自己的地址空间。即资源的复制只有在写入时才会进行,在此之前,只有以只读的方式进行。  

2.4父子进程虚拟地址控制情况

 .text 代码段、.data数据段、.bss静态内存分配段;heap 堆 自下而上增长、stack 栈 自上向下增长。 

2.5父子进程关系及GDB多进程调试

 使用GDB调试的时候,GDB默认只能跟踪一个进程,可以在fork函数调用之前,通过指令设置GDB调试工具跟踪父进程或者跟踪子进程,默认跟踪父进程

set follow-fork-mode [parent(默认)| child] //设置调试父进程或者子进程
set detach-on-fork [on | off]  //设置调试模式
//默认为on,表示调试当前进程的时候,其它的进程继续运行,如果为off,调试当前进程的时候,其它进程被GDB 挂起。  

info inferiors  //查看调试的进程
inferior id  //切换当前调试的进程
detach inferiors id //使进程脱离GDB 调试

2.6 exec函数族

exec函数族的作用是根据指定的文件名找到可执行文件,并用它来取代调用进程的内容,即就是在调用进程内部执行一个可执行文件。

 ​​​​​​​exec函数族

int execl(const char *path, const char *arg, .../* (char *) NULL */);
int execlp(const char *file, const char *arg, ... /* (char *) NULL */);
int execle(const char *path, const char *arg, .../*, (char *) NULL, char * const envp[] */);
int execv(const char *path, char *const argv[]);
int execvp(const char *file, char *const argv[]);
int execvpe(const char *file, char *const argv[], char *const envp[]);
int execve(const char *filename, char *const argv[], char *const envp[]);
// path:需要指定的执行的文件的路径或者名称
// arg:是执行的可执行文件所需要点参数列表
// file:需要执行的可执行文件的文件名
// argv[] 需要的参数的一个字符串数组

2.7 进程退出、孤儿进程、僵尸进程

01/进程退出

#include <stdlib.h>
void exit(int status); //会刷新I/O缓冲
#include <unistd.h>
void _exit(int status); //不会刷新I/O缓冲

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

int main() {

    printf("hello\n");
    printf("world");

    // exit(0);  //输出hello world
    _exit(0);   //输出hello

    return 0;
}

 02/孤儿进程 Orphan Process

父进程运行结束,但子进程还在运行(未运行结束),这样的子进程就称为孤儿进程。

每当出现一个孤儿进程时,内核就把孤儿进程的父进程设置为init,而init进程会循环地wait()它的已经退出的子进程。

孤儿进程并不会有什么危害。

03/僵尸进程 Zombie Process

每个进程结束之后,都会释放自己地址空间中的用户区数据,内核区的PCB没有办法自己释放掉,需要父进程去释放。

进程终止时,父进程尚未回收,子进程残留资源(PCB)存放于内核中,变成僵尸进程。

僵尸进程不能被 kill -9 杀死,会导致一个问题,如果父进程不调用wait()或waitpid(),那么保留的那段信息就不会释放,其进程号就会一直被占用。将会没有可用的进程号而导致系统不能产生新的进程,此为僵尸进程的危害,应当避免。

总结:子进程终止时,父进程尚未回收还在运行,子进程残留资源 PCB 存放于内核中,变成僵尸进程(Z+),不能被kill -9杀死,只能父进程调用wait或手动ctrl+c终止。

孤儿进程:父死子没死,会被init进程接管回收;

僵尸进程:父活子死,无法杀死,可以杀死父进程让init进程接管回收,也可以让内核给父进程一个 SIGCHLD 信号让其回收其子。

2.8wait函数

04进程回收

在每个进程退出时,内核释放该进程所有的资源,但是仍为其保留一定的信息,这些信息主要是指进程控制块PCB的信息。

父进程可以通过调用wait或waitpid得到它的退出状态同时彻底清除掉这个进程。

wait()和waitpid()函数功能一样,区别:wait()函数会阻塞,waitpid()可以设置不阻塞,waitpid()还可以指定等待哪个子进程结束。

注意:一次wait或waitpid调用只能清理一个子进程,清理多个子进程应使用循环。

#include <sys/types.h>
#include <sys/wait.h>
pid_t wait(int *status);
  //功能:等待任意一个子进程结束,如果任意一个子进程结束了,次函数会回收子进程的资源。
pid_t waitpid(pid_t pid, int *wstatus, int options);
   //功能:回收指定进程号的子进程,可以设置是否阻塞。

2.10 进程间通信 

01/进程间通信概念

进程是一个独立的资源分配单元,不同进程(指着用户进程)之间的资源是独立的,没有关联,不能在一个进程中访问另一个进程的资源。但是进程不是孤立的,不同的进程需要进行信息的交互和状态的传递,因此需要进程间通信(IPC: Inter Processes Communication)

进程间的通信(IPC)目的:数据传输、通知事件、资源共享、进程控制。

02/Linux进程间通信的方式

 2.11 匿名管道概述

03/匿名管道

管道也叫无名(匿名)管道,它是UNIX 系统IPC(进程间通信)的最古老形式,所有的UNIX 系统都支持这种通信机制。  

统计一个目录中文件的数目命令:ls | wc –l,为了执行该命令,shell 创建了两个进程来分别执行ls 和wc。  

 04/管道的特点

管道拥有文件的特质:读操作、写操作,匿名管道没有文件实体,有名管道有文件实体,但不存储数据。

在管道中的数据的传递方向是单向的,一端用于写入,一端用于读取,管道是半双工的。

匿名管道只能在有公共祖先的进程(父进行与子进程、两个兄弟进程,具有亲缘关系)之间使用

2.12父子进程通过匿名管道通信

07/匿名管道的使用

#include <unistd.h>
int pipe(int pipefd[2]); //创建匿名管道
long fpathconf(int fd, int name); //查看管道缓冲大小函数  

ulimit -a //查看管道缓冲大小命令  
//pipefd[0] 对应的是管道的读端
//pipefd[1] 对应的是管道的写端
//管道默认是阻塞的:如果管道中没有数据,read阻塞,如果管道满了,write阻塞
//注意:匿名管道只能用于具有关系的进程之间的通信(父子进程,兄弟进程)

总结:

    读管道:

        管道中有数据,read返回实际读到的字节数。

        管道中无数据:

            写端被全部关闭,read返回0(相当于读到文件的末尾)

            写端没有完全关闭,read阻塞等待

    写管道:

        管道读端全部被关闭,进程异常终止(进程收到SIGPIPE信号)

        管道读端没有全部关闭: 

            管道已满,write阻塞

            管道没有满,write将数据写入,并返回实际写入的字节数

 2.15有名管道介绍及使用

08/有名管道

匿名管道,没有名字,只能用于亲缘关系的进程间通信。为了克服这个缺点,提出了有名管道 FIFO,也叫命名管道,FIFO文件。先入先出

有名管道 不同于匿名管道之处在于它提供了一个路径名与之关联,以FIFO的文件形式存在于文件系统中,

09/有名管道的使用

mkfifo 名字 //通过命令创建有名管道  
#include <sys/types.h>
#include <sys/stat.h>
int mkfifo(const char *pathname, mode_t mode); //通过函数创建有名管道  
// - pathname: 管道名称的路径
// - mode: 文件的权限 和 open 的 mode 是一样的 是一个八进制的数

有名管道的注意事项:        

        1.一个为只读而打开一个管道的进程会阻塞,直到另外一个进程为只写打开管道        

        2.一个为只写而打开一个管道的进程会阻塞,直到另外一个进程为只读打开管道    

        读管道:        

                管道中有数据,read返回实际读到的字节数        

                管道中无数据:            

                        管道写端被全部关闭,read返回0,(相当于读到文件末尾)            

                        写端没有全部被关闭,read阻塞等待        

        写管道:        

                管道读端被全部关闭,进行异常终止(收到一个SIGPIPE信号)        

                管道读端没有全部关闭:            

                        管道已经满了,write会阻塞            

                        管道没有满,write将数据写入,并返回实际写入的字节数。 

09/有名管道的使用​​​​​​​

2.17内存映射

10/内存映射 Memory-mapped I/O是将磁盘文件的数据映射到内存,用户通过修改内存就能修改磁盘文件。

 

#include <sys/mman.h>
void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);
//功能:将一个文件或者设备的数据映射到内存中
//参数:addr:地址;length:要映射到数据的长度;port:对申请的内存映射区的操作权限
// flags :
        //- MAP_SHARED : 映射区的数据会自动和磁盘文件进行同步,进程间通信,必须要设置这个选项
        //- MAP_PRIVATE :不同步,内存映射区的数据改变了,对原来的文件不会修改,会重新创建一个新的文件。(copy on write)
//fd: 需要映射的那个文件的文件描述符; offset:偏移量

int munmap(void *addr, size_t length);
//功能:释放内存映射

2.19 信号

01/信号的概念

2.28 共享内存

01/共享内存

共享内存允许两个或多个进程共享物理内存的同一块区域(通常称为段)。

无需内核介入。

函数

//创建一个新的共享内存段,或者获取一个既有的共享内存段的标识。新创建的内存段中的数据都会被初始化为0
int shmget(key_t key, size_t size, int shmflg);
//和当前的进程进行关联
void *shmat(int shmid, const void *shmaddr, int shmflg);
//shmid : 共享内存的标识(ID),由shmget返回值获取
//shmaddr: 申请的共享内存的起始地址,指定NULL,内核指定
//shmflg : 对共享内存的操作

//释放:
//解除当前进程和共享内存的关联
int shmdt(const void *shmaddr);
//删除共享内存,只调用一次,所有的关联进程都解除了关联才调用
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
//cmd : 要做的操作
//buf:需要设置或者获取的共享内存的属性信息

终端命令 ipcs 用法

ipcs -a //打印所有ipc信息
ipcs -m //打印共享内存ipc信息
ipcs -q //打印消息队列ipc信息
ipcs -s //打印信号ipc信息

ipcrm -M shmkey //移除shmkey创建的共享内存段
ipcrm -m shmid //移除shmid标识的共享内存段
ipcrm -Q msgkey //移除msgkey创建的消息队列
ipcrm -q msgid //移除msgid标识的消息队列
ipcrm -S semkey //移除semkey创建的信号
ipcrm -s semid //移除semid标识的信号
操作系统如何知道一块共享内存被多少个进程关联?
答:    共享内存维护了一个结构体struct shmid_ds,
    这个结构体中有一个成员 shm_nattch,
    shm_nattach 记录了关联的进程个数

2.30守护进程

守护进程 Daemon Process:在后台运行,不会被ctrl+C停止。一般采用以 d 结尾的名字。 

01/终端

在UNIX系统中,用户通过终端系统登入系统后得到一个shell进程,这个终端成为shell进程的控制终端。

02/进程组

进程组和会话在进程之间形成了一种两级层次关系:进程组是一组相关进程的集合,会话是一组相关进程组的集合。

进程组由一个或多个共享同一进程组标识符(PGID)的进程组成。

03/会话

会话是一组进程组的集合。会话首进程是创建该新会话的进程,其进程ID 会成为会话ID。新进程会继承其父进程的会话ID。  

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值