【Linux系统编程】Linux多进程开发-概述

Linux多进程开发

进程概述

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

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

进程:正在运行的程序的实例。即操作系统为 该程序运行 分配的一些资源。(抽象概念)

  • 是一个具有一定独立功能的程序关于某个数据集合的一次运行活动。它是操作系统动态执行的基本单元,在传统的操作系统中,进程既是基本的分配单元,也是基本的执行单元。
  • 可以用一个程序来创建多个进程,进程是由内核定义的抽象实体,并为该实体分配用以执行程序的各项系统资源。从内核的角度看,进程由用户内存空间和一系列内核数据结构组成.其中用户内存空间包含了程序代码及代码所使用的变量而内核数据结构则用于维护进程状态信息。记录在内核数据结构中的信息包括许多与进程相关的标识号(IDs)、虚拟内存表、打开文件的描述符表、信号传递及处理的有关信息、进程资源使用及限制、当前工作目录和大量的其他信息。

单道、多道程序设计

  • 单道程序,即在计算机内存中只允许一个程序运行。
  • 多道程序设计技术是在计算机内存中同时存放几道相互独立的程序,使它们在管理程序控制下,相互穿插运行.两个或两个以上程序在计算机系统中同处于开始到结束之间的状态,这些程序共享计算机系统资源。引入多道程序设计技术的根本目的是为了提高CPU的利用率。
  • 对于一个单CPU系统来说,程序同时处于运行状态只是一种宏观上的概念.他们虽然都已经开始运行,但就微观而言,任意时刻, CPU上运行的程序只有一个。
  • 在多道程序设计模型中,多个进程轮流使用CPU。而当下常见CPU为纳秒级,1秒可以执行大约 10 亿条指令。由于人眼的反应速度是毫秒级,所以看似同时在运行。

时间片(timeslice):“量子(quantum)"或“处理器片(processor slice) "

  • 是操作系统分配给每个正在运行的进程微观上的一段CPU时间。事实上虽然一台计算机通常可能有多个CPU,但是同一个CPU永远不可能真正地同时运行多个任务。在只考虑一个CPU的情况下,这些进程"看起来像"同时运行的。实则是轮番穿插地运行,由于时间片通常很短(在 Linux上为5ms - 800ms),用户不会感觉到。
  • 时间片由操作系统内核的调度程序分配给每个进程。首先,内核会给每个进程分配相等的初始时间片,然后每个进程轮番地执行相应的时 间,当所有进程都处于时间片耗尽的状态时.内核会重新为每个进程计算并分配时间片,如此往复。

(面试题:Linux上有哪些进程调度的策略/算法?)

并行和并发

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

【Linux系统编程】Linux多进程开发-概述_第1张图片

进程控制块(PCB):

  • 为了管理进程,内核必须对每个进程所做的事情进行清楚的描述。内核为每个进程分配一个PCB(Processing Control Block)进程控制块,维护进程相关的信息,Linux内核的进程控制块是 task_struct 结构体。
  • 在/usr/src/linux-headers-xxx/include/linux/sched.h文件中可以查看struct task_struct结构体定义。其内部成员有很多,我们只需要掌握以下部分即可:
    • 进程id:系统中每个进程有唯一的 id,用pid_t 类型表示,其实就是一个非负整数
    • 进程的状态:有就绪、运行、挂起、停止等状态
    • 进程切换时需要保存和恢复的一些CPU寄存器
    • 描述虚拟地址空间的信息
    • 描述控制终端的信息
    • 当前工作目录(Current working Directory)
    • umask掩码
    • 文件描述符表,包含很多指向 file 结构体的指针
    • 和信号相关的信息
    • 用户id和组id
    • 会话(Session)和进程组
    • 进程可以使用的资源上限(Resource Limit)
# 当前系统资源的上限
boyangcao@MyLinux:~$ ulimit -a
core file size          (blocks, -c) 0
data seg size           (kbytes, -d) unlimited
scheduling priority             (-e) 0
file size               (blocks, -f) unlimited
pending signals                 (-i) 15407
max locked memory       (kbytes, -l) 65536
max memory size         (kbytes, -m) unlimited
open files                      (-n) 1024
pipe size            (512 bytes, -p) 8
POSIX message queues     (bytes, -q) 819200
real-time priority              (-r) 0
stack size              (kbytes, -s) 8192
cpu time               (seconds, -t) unlimited
max user processes              (-u) 15407
virtual memory          (kbytes, -v) unlimited
file locks                      (-x) unlimited

boyangcao@MyLinux:~$ ulimit -n 2048 # (可以修改open files 上限为2048)

进程状态转换

进程状态反映进程执行过程的变化。这些状态随着进程的执行和外界条件的变化而转换。

  • 在三态模型中,进程状态分为三个基本状态,即就绪态,运行态,阻塞态。
  • 在五态模型中,进程分为新建态、就绪态,运行态,阻塞态,终止态。

【Linux系统编程】Linux多进程开发-概述_第2张图片

  • 运行态:进程占有处理器正在运行
  • 就绪态:进程具备运行条件,等待系统分配处理器以便运行。当进程已分配到除CPU以外的所有必要资源后,只要再获得CPU,便可立即执行。在一个系统中处于就绪状态的进程可能有多个,通常将它们排成一个队列,称为就绪队列
  • 阻塞态:又称为等待(wait)态或睡眠(sleep)态,指进程不具备运行条件,正在等待某个事件的完成

【Linux系统编程】Linux多进程开发-概述_第3张图片

  • 新建态:进程刚被创建时的状态,尚未进入就绪队列
  • 终止态:进程完成任务到达正常结束点,或出现无法克服的错误而异常终止,或被操作系统及有终止权的进程所终止时所处的状态。进入终止态的进程以后不再执行,但依然保留在操作系统中等待善后。一旦其他进程完成了对终止态进程的信息抽取之后,操作系统将删除该进程。

进程相关的命令

ps -aux/-ajx :显示终端上的所有进程:

ps -aux 
ps -ajx
a:显示终端上的所有进程,包括其他用户的进程
u:显示进程的详细信息
x:显示没有控制终端的进程
j:列出与作业控制相关的信息

boyangcao@MyLinux:~$ ps -aux
USER        PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
root          1  0.0  0.2 225368  9072 ?        Ss   10:20   0:02 /sbin/init splash
USER: 进程所属的用户
PID:进程ID
TTY:当前进程所属终端(可用 tty指令查询当前进程所属终端)
STAT:状态
	D	不可中断Uninterruptible (usually IO)
	R	正在运行,或在队列中的进程
	S	处于休眠状态
	T	停止或被追踪
	Z	僵尸进程
	W	进入内存交换(从内核2.6开始无效)
	X	死掉的进程
	<	高优先级
	N	低优先级
	s	包含子进程
	+	位于前台的进程组
START:开始时间
TIME: 持续时间
COMMAND:执行哪个命令开始运行的进程

boyangcao@MyLinux:~$ ps -ajx
  PPID    PID   PGID    SID TTY       TPGID STAT   UID   TIME COMMAND
     0      1      1      1 ?            -1 Ss       0   0:02 /sbin/init splash
PPID:父进程ID
PID:进程ID
PGID:进程组ID
SID:会话ID

top:实时显示进程动态

可以在使用top命令时加上 -d 来指定显示信息更新的时间间隔,在top命令执行后,可以按以下按键对显示的结果进行排序:

  • M根据内存使用量排序
  • P根据CPU占有率排序
  • T根据进程运行时间长短排序
  • u根据用户名来筛选进程
  • K输入指定的PID杀死进程

kill:杀死进程

#杀死进程
kill [-signal] pid
kill -l 列出所有信号
# 信号:9)SIGKILL 强制杀死进程
kill -SIGKILL 进程ID
kill -9 进程ID 

killall name 根据进程名杀死进程

进程号和相关函数:

  • 每个进程都由进程号来标识,其类型为pid_t (整型),进程号的范围:0~32767。进程号总是唯一的,但可以重用。当一个进程终止后,其进程号就可以再次使用。
  • 任何进程(除 init 进程(初始进程))都是由另一个进程创建,该进程称为被创建进程的父进程,对应的进程号称为父进程号(PPID) 。
  • 进程组是一个或多个进程的集合。他们之间相互关联.进程组可以接收同一终端的各种信号,关联的进程有一个进程组号(PGID)。默认情况下,当前的进程号会当做当前的进程组号。
  • 进程号和进程组相关函数:
pid_t getpid (void);
pid_t getppid (void) ;
pid_t getpgid(pid_t pid);

进程创建

fork()

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

#include 
#include 
pid_t fork (void) ;
// fork()  creates  a new process by duplicating the calling process(复制调用的进程(父进程),及虚拟地址空间).
 
#include 
#include 

pid_t fork(void);
        作用:用于创建子进程
        返回值:
            fork()的返回值会返回两次,一次在父进程中,一次在子进程中。
                - 在父进程中返回创建的子进程的PID
                - 在子进程中返回0
                如何区分父进程和子进程,通过fork的返回值。
                - 在父进程中返回-1,表示创建子进程失败,并设置errno。
        父子进程之间的关系:
        区别: 
            子进程会继续执行当前代码(fork()创建子进程开始向下执行)
            1. fork()函数的返回值不同
                父进程中: >0 返回的子进程的ID
                子进程中: =0
            2. PCB中的一些数据有区别
                当前进程的pid
                当前进程的父进程ID ppid
                信号集
            共同点:
                某些状态下:子进程刚被创建出来,还没有执行任何写数据的操作
                    - 用户区的数据相同
                    - 文件描述符表
            父子进程对变量是不是共享的?
                - 刚开始的时候是相同的,如果修改了数据就不共享了
                - "读时共享(子进程被创建,两个进程没有做任何写的操作),写时拷贝"

返回值:

  • 成功:子进程中返回 0,父进程中返回子进程ID
  • 失败:返回 -1

失败的两个主要原因:

  1. 当前系统的进程数已经达到了系统规定的上限,这时 errno 的值为 EAGAIN
  2. 系统内存不足,这时 errno 的值被设置为 ENOMEM

示例:

#include 
#include 
#include 
int main()
{
    // 创建子进程
    pid_t pid = fork();
    // 判断是子进程还是父进程
    if(pid > 0)
    {
        printf("pid: %d\n", pid);
        // 当前是父进程
        // 如果大于0,返回的是创建的子进程的进程号PID
        printf("i am parent process, pid : %d, ppid : %d\n", getpid(), getppid());
    }
    else if(pid == 0)
    {
        // 当前是子进程
        printf("i am child process, pid : %d, ppid : %d\n", getpid(), getppid());
    }
    for(int i = 0; i < 5; i++)
    {
        printf("i: %d, pid = %d\n", i, getpid());
        sleep(1);
    }
    return 0;
}

父子进程虚拟地址空间:

【Linux系统编程】Linux多进程开发-概述_第4张图片

fork()以后,子进程的用户区数据和父进程—样。内核区也会拷贝过来,但是pid会不同;并且fork()返回值也不同,父进程返回创建的子进程的进程号PID,子进程返回0。

实际上,Linux的 fork()函数是通过读时共享 写时拷贝(copy- on-write)实现的。

  • 写时拷贝是一种可以推迟甚至避免拷贝数据的技术。

    内核此时并不复制整个进程的地址空间,而是让父子进程共享同一个地址空间。

    只用在需要写入的时候才会复制地址空间,从而使各个进程拥有各自的地址空间。也就是说,资源的复制是在需要写入的时候才会进行,在此之前,只有以只读方式共享。

注意:fork之后父子进程共享文件,fork产生的子进程与父进程是相同的文件描述符,指向相同的文件表,引用计数增加,共享文件偏移指针。

GDB多进程调试

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

#1. 设置调试父进程或者子进程
set follow-fork-mode parent(默认)
set follow-fork-mode child
#2. 设置调试模式
set detach-on-fork on
set detach-on-fork off
# - 默认为on,表示调试当前进程的时候,其它的进程继续运行;
# - 如果为off,调试当前进程的时候,其它进程被GDB挂起。(fork()处挂起)
#3. 查看调试的进程:
info inferiors

(gdb) info inferiors
  Num  Description       Executable        
* 1    process 4948      /home/boyangcao/Linux/Lesson18/hello (当前调试进程)
  2    process 4952      /home/boyangcao/Linux/Lesson18/hello 
   
#4. 切换当前调试的进程
inferior id

(gdb) inferiors 2
#5. 使进程脱离 GDB调试
detach inferiors id
# 移除进程id
remove inferiors id

(gdb) detach inferiors 1

exec 函数族

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

    (ps. 因为主进程中可能还有一些内容需要继续运行,所以单纯取代调用进程内容是不合理的):

    • 可先fork()一个子进程,并通过判断让子进程运行exec(),并运行目标可执行文件(从main()开始执行);而主进程跳过并继续运行之后的代码。
  • exec函数族的函数执行成功后不会返回,因为调用进程的实体,包括代码段,数据段和堆栈等都已经被新的内容取代,只留下进程ID 等一些表面上的信息仍保持原样;只有调用失败了,它们才会返回 -1,从原程序的调用点接着往下执行。

// 标准C库函数(对execve()封装)
// l(list) 	参数地址列表,以空指针结尾
int execl (const char *path,const char *arg, ...(char*) NULL);
// p(path)		按PATH环境变量指定的目录搜索可执行文件
int execlp(const char *file,const char *arg,...(char*) NULL);
// v(vector)	存有各参数地址的指针数组的地址
int execv (const char *path,char *const argv[] );
int execvp (const char *file,char *const argv[] );
// e(environment)	存有环境变量字符串地址的指针数组的地址
int execvpe (const char *file,char *const argv[], char *const envp[]);
int execle(const char *path,const char *arg,...(char*) NULL,charconst envp[]);

// Linux/Unix 系统函数
int execve(const char *filename,char *const argv[], char *const envp[]);

#include 
extern char **environ;
int execl(const char *path, const char *arg, ...);
        参数:
            - path: 需要指定的执行文件路径/名称。
                相对路径:a.out 
                绝对路径:/home/boyangcao/a.out (推荐)
            - arg: 是执行可执行文件所需的参数列表。
                第一个参数一般没有作用 为了方便 一般写的是执行程序的名称。
                第二个参数开始往后,就是执行程序所需的参数列表。
                参数最后需要以NULL结束(NULL: 哨兵)
        返回值:
            只有当调用失败才会有返回值-1,并且设置errno
            调用成功没有返回值。
        eg. execl("/bin/ps", "ps", "-aux", NULL);

int execlp(const char *file, const char *arg, ...);
        会到环境变量中查找指定的可执行文件,如果找到了就执行,找不到就执行不成功。
        (环境变量 env -> PATH 中的路径)
        参数:
            - file: 需要执行的可执行文件的文件名
                a.out 
                ps
            - arg: 是执行可执行文件所需的参数列表。
                第一个参数一般没有作用 为了方便 一般写的是执行程序的名称。
                第二个参数开始往后,就是执行程序所需的参数列表。
                参数最后需要以NULL结束(NULL: 哨兵)
        返回值:
            只有当调用失败才会有返回值-1,并且设置errno
            调用成功没有返回值。
        eg. execlp("ps", "ps", "-aux", NULL);

int execv (const char *path,char *const argv[] );
        参数:
            - path: 需要指定的执行文件路径/名称。
                相对路径:a.out 
                绝对路径:/home/boyangcao/a.out (推荐)
            - argv: 需要的参数的一个字符串数组
        eg. 
            char* agrv[] = {"ps", "aux", "null"};
            int execv ("/bin/ps", argv);

int execve(const char *filename,char *const argv[], char *const envp[]);
        参数:
            - envp: 自行指定查找可执行文件的路径
        eg. 
            char* envp[] = {"/home/boyangcao", "home/aaa", "home/aaa"};
            char* agrv[] = {"ps", "aux", "null"};
            int execve ("ps", argv, envp);   

示例:创建一个子进程,在子进程中执行exec函数族中的函数

#include 
#include 
int main(){
    // 创建一个子进程,在子进程中执行exec函数族中的函数
    __pid_t pid = fork();
    if(pid > 0){
        // 父进程
        printf("i am parent process, pid: %d\n", getpid());
        sleep(1);
    }
    else if(pid == 0){
        // 子进程
        // execl("/home/boyangcao/Linux/Lesson19/hello", "hello", NULL);
        // 执行shell命令
        execl("/bin/ps", "ps", "-aux", NULL);
        printf("i am child process, pid = %d\n", getpid());
    }
    for(int i  = 0; i < 3; i++)
        printf("i = %d, pid = %d\n", i, getpid());
}

进程控制

进程退出

【Linux系统编程】Linux多进程开发-概述_第5张图片

// 标准C库函数
#include 
void exit (int status);

// Linux系统函数
#include 
void _exit (int status);
//status参数:进程退出时的一个状态信息,父进程回收子进程资源时可以获得。

孤儿进程

  • 父进程运行结束,但子进程还在运行(未运行结束)。这样的子进程就称为孤儿进程(orphan Process) 。
  • 每当出现一个孤儿进程的时候,内核就把孤儿进程的父进程设置为init,而 init 进程会循环地 wait() 它的已经退出的子进程。这样当一个孤儿进程结束了其生命周期的时候,init 进程就会处理它的一切善后工作(回收其PCB等资源)。因此孤儿进程并不会有什么危害。

僵尸进程

  • 每个进程结束之后,都会释放自己地址空间中的用户区数据,内核区的 PCB没有办法自己释放掉,需要父进程去释放。
  • 进程终止时,父进程尚未回收,子进程残留资源(PCB)存放于内核中,变成僵尸(zombie)进程。
  • 僵尸进程不能被 kill -9 杀死(因为不是一个正常进程)。
  • 这样就会导致一个问题,如果父进程不调用 wait() 或 waitpid() 的话,那么保留的那段信息就不会释放.其进程号就会一直被占用,但是系统所能使用的进程号是有限的,如果大量的产生僵尸进程,将因为没有可用的进程号而导致系统不能产生新的进程,此即为僵尸进程的危害,应当避免。
boyangc+   8300  0.0  0.0   4512   760 pts/4    S+   14:39   0:00 ./wait
boyangc+   8301  0.0  0.0      0     0 pts/4    Z+   14:39   0:00 [wait] <defunct>
boyangc+   8302  0.0  0.0      0     0 pts/4    Z+   14:39   0:00 [wait] <defunct>
boyangc+   8303  0.0  0.0      0     0 pts/4    Z+   14:39   0:00 [wait] <defunct>
boyangc+   8304  0.0  0.0      0     0 pts/4    Z+   14:39   0:00 [wait] <defunct>
boyangc+   8305  0.0  0.0      0     0 pts/4    Z+   14:39   0:00 [wait] <defunct>

解决办法:

  1. 将父进程kill,让其子进程成为孤儿进程被init托管,从而执行wait()实现对内核区资源的释放

进程回收

  • 在每个进程退出的时候,内核释放该进程所有的资源、包括打开的文件、占用的内存等。但是仍然为其保留一定的信息,这些信息主要主要指进程控制块PCB的信息(包括进程号、退出状态、运行时间等)。
  • 父进程可以通过调用 wait() 或 waitpid() 得到它的退出状态同时彻底清除掉这个进程。
  • wait() 和 waitpid() 函数的功能一样,区别在于:
    • wait ()函数会阻塞
    • waitpid() (默认阻塞) 但可以设置不阻塞,waitpid() 还可以指定等待哪个子进程结束。
  • 注意:一次 wait() 或 waitpid() 调用只能清理一个子进程,清理多个子进程应使用循环。
wait()
#include 
#include 
pid_t wait(int *wstatus);
        功能:等待任意一个子进程结束,如果任意一个子进程结束,此函数会回收子进程的资源
        参数:
            - int* wstatus: 进程退出时的状态信息,传入的是一个int类型地址,传出参数。
                - NULL: 不需要得到状态信息
        返回值:
            - 成功:返回被回收的子进程的id
            - 失败:返回-1.(所有子进程都结束了 & 调用函数失败)
    调用wait()函数的进程会被挂起(阻塞),直到它的一个子进程退出或收到一个不能被忽略的信号时才被唤醒(相当于继续向下执行)
    - 如果没有子进程,函数立刻返回-1.
    - 如果子进程都已经结束了,也会立即返回-1.

退出信息相关宏函数:

WIFEXITEI (status)		// 非0,进程正常退出
WEXITSTATUS (status)	// 如果上宏为真,获取进程退出的状态(exit的参数)
    
wIFSIGNALED (status)	// 非0,进程异常终止
WTERMSIG (status) 		// 如果上宏为真,获取使进程终止的信号编号
    
wIFSTOPPED (status) 	// 非0,进程处于暂停状态
wSTOPSIG (status)		// 如果上宏为真,获取使进程暂停的信号的编号
wIFCONTINUED (status)	// 非0,进程暂停后已经继续运行

示例:

#include 
#include 
#include 
#include 
#include 

int main()
{
    // 有一个父进程,创建5个子进程(兄弟)
    pid_t pid;
    // 创建5个子进程
    for(int i = 0 ; i < 5; i++)
    {
        pid = fork();
        if(pid == 0) break;
    }
    if(pid > 0)
    {
        // 主进程
        while(1)
        {
            printf("parent process, pid == %d\n", getpid());
            int st;
            int ret = wait(&st);
            if(ret == -1) break;
            if(WIFEXITED(st))
            {
                // 是不是正常退出
                printf("退出的状态码: %d\n", WEXITSTATUS(st));
            }
            if(WIFSIGNALED(st))
            {
                // 是不是异常终止
                printf("被哪个信号干掉了:%d\n", WTERMSIG(st));
            }
            printf("child die, pid = %d\n", ret);
            sleep(1);
        }
    }
    else if(pid == 0)
    {
        // 子进程
        printf("child process, pid == %d\n", getpid());
        sleep(1);
        exit(0);
    }
    return 0; // exit(0);
}
waitpid()
#include 
#include 
pid_t waitpid(pid_t pid, int *wstatus, int options);
        功能:回收指定进程号的子进程,可以设置是否阻塞。
        参数:
            - pid: 
                pid > 0:  表示具体某个子进程的pid
                pid = 0:  回收当前进程组的所有子进程pid
                pid = -1: 回收所有的子进程,相当于wait() (最常用!)
                pid < -1: 某个进程组的组id的绝对值,回收指定进程组(加负号)中的子进程。
            - options: 设置阻塞或非阻塞
                0: 阻塞
                WNOHANG: 非阻塞(当前没有进程要回收的话立即返回)
            - 返回值:
                > 0: 返回子进程的id
                = 0: options = WNOHANG, 表示还有子进程运行中
                -1:  错误,或者没有子进程了。
            eg.
                int st;
            	int ret = waitpid(-1, &st, WNOHANG);

你可能感兴趣的:(Linux高并发服务器开发,linux,c语言)