Linux系统编程---多进程

发布于:2025-04-19 ⋅ 阅读:(21) ⋅ 点赞:(0)

一、进程的概念

1、什么是程序?什么是进程?

  • 程序:编译后产生的,格式为ELF的,存储于硬盘的文件。
  • 进程:程序中的代码和数据,被加载到内存中运行的过程。其实说白了,进程就是一个正在执行的程序。
  • 程序是静态的概念,进程是动态的概念。

        在Linux中,程序文件的格式都是ELF,这些文件在被执行的瞬间,就被载入内存,所谓的载入内存,如上图所示,就是将数据段、代码段这些运行时必要的资源拷贝到内存,另外系统会再分配相应的栈、堆等内存空间给这个进程,使之成为一个动态的实体。

2、为什么需要多进程?

        多进程(Multiprocessing)是操作系统提供的一种并发执行机制,允许系统同时运行多个进程。它的主要目的是提高计算机资源的利用率、增强程序的并发能力,并改善系统的整体性能

        单进程程序 -> 只能一行行代码去执行。

        多进程程序 -> 同时执行两行及以上的代码 -> 产生一个或多个子进程,帮自己处理另其它的事情。

3、在linux下,如何开启一个新的进程?

        直接在linux下,执行一个程序,就会开启相应的进程。

        例如:在Linux的终端执行 ./hello -> 开启一个名字为hello的进程。

4、当进程开启之后,系统会为进程分配什么资源?

1)会分配进程对应内存空间

        如int x -> 运行程序之后,就会在栈区申请4个字节的内存空间。

2)进程在系统内核中如何进行表示呢

        学生管理系统 ---> 每个学生使用结构体进行表示和管理

        linux系统 ---> 每个进程使用结构体进行表示和管理

        当进程开启之后,会为这个进程分配一个任务结构体,这个任务结构体就是用于描述这个进程的。也就是说,进程在内核中是以结构体struct task_struct{} 进行表示的。这个结构体也被称之为进程控制块。

        结构体:进程ID号、信号、文件、资源....

        /usr/src/linux-headers-5.15.0-136/include/linux ----第717行

(不同版本系统的Linux可能存放的路径不同)

5、关于进程的命令

1)pstree:查看整个Linux系统进程之间关系的命令

        在Linux系统中,除了系统的初始进程之外,其余所有进程都是通过从一个父进程(parent)复刻(fork)而来的,有点像人类社会,每个个体都是由亲代父母繁衍而来。

        因此,在Linux系统中,所有的进程都起源于相同的初始进程,它们之间形成一棵倒置的进程树,就像家族族谱,可以使用命令pstree查看这些进程的关系:

Snail@ubuntu:~/Desktop$ pstree
systemd─┬─ModemManager───2*[{ModemManager}]
        ├─NetworkManager───2*[{NetworkManager}]
        ├─VGAuthService
        ├─accounts-daemon───2*[{accounts-daemon}]
        ├─acpid
        ├─avahi-daemon───avahi-daemon
        ├─blkmapd
        ├─colord───2*[{colord}]
        ├─cron
        ├─cups-browsed───2*[{cups-browsed}]
        ├─cupsd───dbus
        ├─dbus-daemon
        ├─freshclam
        ├─fwupd───4*[{fwupd}]
        ...

2)ps -ef:查看进程ID号(静态)

        如下图所示,从最开始的系统进程叫systemd(init),这个进程的诞生比较特别,其身份信息在系统启动前就已经存在于系统分区之中,在系统启动时直接复制到内存。而其余的进程,从前面提到的pstree命令的执行效果可见,都是系统初始进程的直接或间接的后代进程

3)top:查看进程CPU的占用率(动态)

Snail@ubuntu:~/Desktop$ top

top - 18:19:03 up 31 min,  1 user,  load average: 0.10, 0.07, 0.14
Tasks: 342 total,   1 running, 341 sleeping,   0 stopped,   0 zombie
%Cpu(s):  0.7 us,  1.4 sy,  0.0 ni, 97.9 id,  0.0 wa,  0.0 hi,  0.0 si,  0.0 st
MiB Mem :   7894.6 total,   4216.9 free,   1381.5 used,   2296.2 buff/cache
MiB Swap:   1162.4 total,   1162.4 free,      0.0 used.   6187.0 avail Mem 

    PID USER      PR  NI    VIRT    RES    SHR S  %CPU  %MEM     TIME+ COMMAND  
   1935 Snail     20   0 4665680 273728 130240 S   8.6   3.4   0:43.43 gnome-s+ 
   6117 Snail     20   0  556976  52472  39952 S   3.3   0.6   0:00.97 gnome-t+ 
   2230 Snail     20   0  498040  32856  24280 S   0.7   0.4   0:02.74 fcitx    
   2507 Snail     20   0    8484   3472   3200 S   0.7   0.0   0:01.95 dbus-da+ 
     17 root      20   0       0      0      0 I   0.3   0.0   0:18.27 rcu_pre+ 
     55 root      20   0       0      0      0 I   0.3   0.0   0:02.04 kworker+ 

    ...    

二、进程的状态

1、进程从诞生到死亡会经历哪些状态?

        就绪态 TASK_RUNNING 等待CPU资源 不占用CPU资源,不运行代码。

        运行态 TASK_RUNNING 占用CPU资源,运行代码。

        暂停态 TASK_STOPPED 占用CPU资源,不运行代码。 可以到就绪态。

        睡眠态 占用CPU资源,运行代码。 可以到就绪态。

                TASK_INTERRUPTIBLE 响应信号 ----》浅度睡眠 pause()--->一直等待下一信号

                TASK_UNINTERRUPTIBLE 不响应信号 ----》深度睡眠

        僵尸态 EXIT_ZOMBIE 占用CPU资源,不运行代码。不可以到运行态。进程退出的时候,就一定会变成僵尸态。

        死亡态 EXIT_DEAD 不占用CPU资源,不运行代码。进程退出的时候,如果有人去帮自己回收资源,那么僵尸态就会变为死亡态。

2、什么是僵尸态?

        进程结束时,就从运行态变成僵尸态,所谓僵尸态,就是代表这个进程所占用的CPU资源和自身的任务结构体没有被释放,这个状态的进程就是僵尸态进程。

        总结:

1)进程在暂停态时,收到继续的信号时,是切换到就绪态,而不是运行态。

2)程序的main函数执行return 0就会导致进程的退出,一定会变成僵尸态。

3)进程不可以没有父进程,也不能同时拥有两个父进程。

4)孤儿进程特征就是失去父进程时,会马上寻找继父,而不是等到孤儿进程变成僵尸态再找。

5)祖先进程一定要帮其他的进程回收资源。

三、进程创建

1、fork:在一个正在运行的进程中创建一个子进程

#include <sys/types.h>
#include <unistd.h>

函数原型:
    pid_t fork(void);
函数作用:
    创建一个新的进程(子进程),该进程是调用进程(父进程)的副本。
    子进程会复制父进程的:
        代码段(text segment)
        数据段(data segment)、堆(heap)、栈(stack)
        文件描述符表(但共享相同的文件表项)
        进程环境(环境变量、信号处理方式等)
    写时复制(Copy-On-Write, COW)优化:
    现代操作系统不会立即复制所有内存,而是共享父进程的内存页。
    只有当父进程或子进程尝试修改某块内存时,才会真正复制该内存页,以提高效率。
返回值:
    成功时(fork() 调用一次,但返回两次):
        在父进程中:返回子进程的 PID(> 0)
        在子进程中:返回 0(因为子进程可以通过 getppid() 获取父进程的 PID)
失败时:
    返回-1,并设置 errno(常见原因:进程数达到系统限制、内存不足等)。

        在进程内部创建一个新的子进程,看看会不会同时做两件事情。

#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>

int main()
{
    /*当前是单进程的程序*/    
    printf("main process!\n");
    
    /*产生一个子进程*/
    fork();
    
    /* 一个父进程,一个子进程 */
    /* 接下来的代码,父进程会执行一遍,子进程也会执行一遍 */

    printf("after fork\n");
    
    return 0;
}

        可能的运行结果分析:

结果1:父进程先运行,子进程后运行。

        注意:只有父进程退出,才会出现命令行,子进程退出是不会出现命令行。

结果2:子进程先运行,父进程后运行。

        想要确保每次都是子进程先运行,做法: 就是让父进程先睡眠。

        子进程不用睡眠,父进程需要睡眠。 父子进程任务不一样。

        思路: 通过返回值判断。

#include<stdio.h>
#include <sys/types.h>
#include <unistd.h>

int main()
{
    printf("main....\n");
    pid_t id = fork(); //创建一个子进程
    if(id == -1) //进程创建失败
    {
        perror("fork process error");
        return -1;
    }
    else if(id > 0) //父进程 ,但是返回的ID号 是 子进程的ID号
    {
        sleep(1); 
        printf("我是父进程,我的进程ID是:%d\n", getpid());
        printf("我儿子的进程ID是:%d\n",id);
    }
    else if(id == 0)//子进程
    {
        printf("我是儿子,我的进程ID是:%d\n", getpid());   
    }
    printf("hello\n");
    
    return 0;
}

        结论:

1)父子进程执行顺序随机的。

2)fork()之后的代码,两个进程都会执行。

四、查看进程号

1、getpid

        查看当前进程号

#include <sys/types.h>
#include <unistd.h>
#include <sys/types.h>

函数原型:
    pid_t getpid(void);

函数功能:
    返回当前进程的进程 ID(PID)。
    每个进程都有一个唯一的 PID,由内核分配。

返回值:
    成功时返回当前进程的 PID(> 0)。
    不会失败(没有错误情况)。

2、getppid

        查看父进程号

#include <sys/types.h>
#include <unistd.h>
#include <sys/types.h>

函数原型:
    pid_t getppid(void);

函数功能:
    返回当前进程的父进程 ID(PPID)。
    如果父进程终止,子进程的 PPID 会变为1(init 或 systemd),这种情况称为“孤儿进程被 init 收养”。

返回值:
    成功时返回当前进程的 PPID(> 0)。
    不会失败(没有错误情况)。

3、程序实例

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

int main() {
    pid_t pid = fork();

    if (pid == -1) {
        perror("fork failed");
        return 1;
    } else if (pid == 0) {
        // 子进程
        printf("Child: PID = %d, PPID = %d\n", getpid(), getppid());
    } else {
        // 父进程
        printf("Parent: PID = %d, child PID = %d\n", getpid(), pid);
    }

    return 0;
}

五、进程回收

1、wait

#include <sys/types.h>
#include <sys/wait.h>

函数原型:
    pid_t wait(int *wstatus);
函数功能:
    阻塞当前进程;
    等待其子进程退出并回收其系统资源;
    把子进程从僵尸状态设置为死亡状态。
函数参数:
    wstatus  --> 用于存储子进程的状态信息 , 设置为NULL 则表示不需要状态信息
返回值:
    成功 返回接收到的子进程的ID 
    失败 返回 -1 ,并设置错误码

        接口说明:

  • 如果当前进程没有子进程,则该函数立即返回。
  • 如果当前进程有不止1个子进程,则该函数会回收第一个变成僵尸态的子进程的系统资源。
  • 子进程的退出状态(包括退出值、终止信号等)将被放入wstatus所指示的内存中,若wstatus指针为NULL,则代表当前进程放弃其子进程的退出状态。
  • 如果父进程如果要获取这些信息,需要用以下宏对status进程解析:

功能

WIFEXITED(status)

判断子进程是否正常退出

WEXITSTATUS(status)

获取正常退出的子进程的退出值

WIFSIGNALED(status)

判断子进程是否被信号杀死

WTERMSIG(status)

获取杀死子进程的信号的值

2、waitpid

#include <sys/types.h>
#include <sys/wait.h>

函数原型:
pid_t waitpid(pid_t pid, int *wstatus, int options);

函数功能:
挂起(阻塞)当前进程,直到指定的子进程终止或收到信号。
回收子进程资源(防止僵尸进程)。
获取子进程退出状态(通过 wstatus 返回)。

函数参数:
    pid
    > 0    等待指定pid的子进程(假设为138 那么表示等待138号进程退出)
    = 0   等待本进程组中的任意一个子进程的退出 (与wait功能一致)
    = -1  等待任意一个子进程 (与进程组无关)
    < -1  等待的是该进程组(假设 pid 为 -138 那么表示等待进程组ID为 138的任何一个进程)
    wstatus --> 存储退出状态
    options --> options的取值,可以是0,也可以是上表中各个不同的宏的位或运算取值。        
返回值:
    > 0   成功返回终止的子进程 PID。
    = 0   仅当options=WNOHANG时,表示没有子进程退出。
    -1    出错(如无子进程、被信号中断等),错误码存于errno。

        与wait()的区别:

  • 可以通过参数 pid 用来指定想要回收的子进程。
  • 可以通过 options 来指定非阻塞等待。

pid

作用

options

作用

等待组ID等于pid绝对值的进程组中的任意一个子进程

0

阻塞等待子进程的退出

-1

等待任意一个子进程

WNOHANG

若没有僵尸子进程,则函数立即返回(非阻塞)

0

等待本进程所在的进程组中的任意一个子进程

WUNTRACED

当子进程暂停时函数返回(除了退出以外子进程暂停也会结束阻塞)

>0

等待指定pid的子进程

WCONTINUED

当子进程收到信号SIGCONT继续运行时函数返回(当子进程收到继续运行是会结束阻塞)

六、进程退出

1、exit()

        exit() 是 C 标准库(stdlib.h)提供的进程终止函数,用于 正常结束程序 并返回状态码给操作系统。它比 _exit() 更安全,因为它会先执行清理工作(如刷新缓冲区)。

#include <stdlib.h>

函数原型:
    void exit(int status);

函数作用:
    先清洗缓冲区和文件指针,再退出

参数:
    status: 退出状态值
    0  -> 进程正常退出
    非0 -> 进程异常退出

返回值:
    无
    但进程的退出状态会通过系统调用 _exit(status) 传递给父进程(可通过waitpid() 获取)

2、_exit()

        _exit() 是一个系统级进程终止函数,定义在中unistd.h。它直接终止进程,不执行任何清理操作(如缓冲区刷新、atexit() 函数调用等),而是立即将控制权交还给操作系统。

#include <unistd.h>

函数原型:
void _exit(int status);

函数功能:
    不会清洗缓冲区、不会关闭文件描述符,直接终止进程

函数参数:
    status: 退出状态值
        0   -> 进程正常退出
        非0 -> 进程异常退出
        
返回值:
    无返回值
    进程的退出状态status 会传递给父进程(可通过 waitpid() 或 wait() 获取)。

3、_Exit()

        _Exit() 是C 标准库(stdlib.h)提供的立即终止进程的函数,与 _exit() 类似,但属于 C 标准(而非 POSIX)。它 不执行任何清理操作,直接终止进程。

#include <stdlib.h>

函数原型:
void _Exit(int status);

函数功能:
    立即终止当前进程,不执行任何清理操作
    直接返回状态码 status 给操作系统(父进程可通过 waitpid() 获取)

函数参数:
    status: 退出状态值
        0   -> 进程正常退出
        非0 -> 进程异常退出
                
返回值:
    无返回值
    进程的退出状态status 会传递给父进程(通过 waitpid() 获取)。

4、 缓冲区问题

#include<stdio.h>
#include<stdbool.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <stdlib.h>

int main()
{
    /*单进程的程序*/
    printf("main ");
    
    //先清洗缓冲区,再退出当前进程
    //exit(0);//return  0;
    
    //直接退出当前进程,不会帮助你清洗缓冲区
    _exit(0);
    
    printf("11112222\n");
}

5、 退出状态

        父进程监听子进程的退出状态,如果子进程正常退出的,则输出一个ok,如果子进程异常退出,则输出一个error。

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
int main(int argc,char *argv[]) //./show_bmp
{
    pid_t x;
    int state; //买一个碗
    int a = 11;

    x = fork();
    if(x > 0)
    {
        wait(&state); //将碗的地址给你
        if(state == 0)
        {
             printf("ok!\n");
        }
        else{
            printf("error!\n");
        }
    }

    if(x == 0)
    {
        sleep(5);
        if(a == 10)
        {
            exit(0); //正常退出
        }
        else{
            exit(-1); //异常退出
        }
    }
}

6、 exit与return

        例1:

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

int main(int argc,char *argv[])
{
    printf("helloworld!\n");
    return 0;
}
//输出helloworld

        例2:

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

int main(int argc,char *argv[])
{
    printf("helloworld!\n");
    exit(0);
}
//输出helloworld

        例3:

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

int fun(void)
{
    return 0;
}

int main(int argc,char *argv[])
{
    fun();
    printf("helloworld!\n");
    return 0;
}
//输出helloworld

        例4:

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

int fun(void)
{
    exit(0);  -> 整个进程马上结束
}

int main(int argc,char *argv[])
{
    fun();
    printf("helloworld!\n");
    return 0;
}
//不会输出helloworld

        结论:

        1)在main函数中,exit()与return语句作用是一样。 -> 因为main函数的返回就等价于进程的退出。

        2)不在main函数中,exit()代表进程的退出,return只是代表函数的返回。


网站公告

今日签到

点亮在社区的每一天
去签到