什么是进程?

其实在 Linux 操作系统中,进程是程序的一个执行实例,也就是说程序运行后,操作系统会给程序分配独立地址空间、系统资源(如文件描述符、内存、CPU 时间等)和状态信息,可以简单理解为正在运行的程序,在Linux中就是进程。

简单理解:
程序 = 静态的代码文件
进程 = 动态运行中的程序实例

例如:
我们编写了一个 hello 程序。
当你在终端输入 ./hello 时,操作系统会创建一个新的执行实体来运行它,这个执行实体就是进程。

程序和进程的区别

对比项 程序 进程
性质 静态的 动态的
存在形式 磁盘上的可执行文件 内存中正在运行的实例
内容 指令和数据的集合 程序代码 + 数据 + 运行状态 + 系统资源
生命周期 长期保存,直到被删除 有创建、运行、阻塞、结束等过程
数量关系 一个程序可以被多次执行 一个程序可以对应多个进程
资源占用 不占用运行时资源 会占用 CPU、内存、文件描述符等资源

为什么需要进程

因为他有很多特点可以使用

作用 说明
提高安全性 一个进程出错,一般不会直接影响其他进程
提高效率 多个进程并发执行,可以提高系统吞吐量和响应速度
简化编程 可以把复杂任务拆成多个进程分别处理
支持多任务 操作系统可以同时运行多个程序

进程的基本特点

特点 含义
动态性 进程是程序运行后的状态,会不断变化
独立性 每个进程通常有自己独立的地址空间和资源
并发性 多个进程可以在同一时间段内同时推进执行
异步性 进程的执行顺序和速度不完全确定,受调度影响

进程在项目中有哪些应用场景呢?

这里句两个例子

保活进程:主业务逻辑作为单独进程运行,另外开发一个精简的进程用于监控主任务进程,一旦发现主任务进程挂掉或在无应答,那就进行重启&日志记录。

简化编程:例如我们要做一个大型项目,里面有网络部分,显示部分,设备控制部分,我们可以单独把这三个模块设计为三个单独的进程运行,然后通过进程通信交换状态即可,这样无论是开发、维护都更加方便。

#include <stdio.h>

int main() {
    printf("Hello, World!\n");
    while(1);
    return 0;
}

我们运行他

./hello &
运行后,通过ps指令,可以看到这个程序在运行,93899就是进程的id
leo@ubuntu:~/hello/build$ ps
   PID TTY          TIME CMD
 84398 pts/7    00:00:00 bash
 93899 pts/7    00:06:30 hello
 
如果我们需要关闭进程,我们可以使用
kill -9 93899
其中,-9表示发送强制终止的信号。

Linux的父子进程

Ubuntu内核在启动时,会创建第一个pid为1的进程,后续其它进程都是由此进程创建和管理(当然除了僵尸进程),所以我们可以理解Linux其实把所有进程都放在一个树状的结构体中进行管理,这点我们可以执行命令pstree看到

leo@ubuntu:~/hello/build$./hello &
leo@ubuntu:~/hello/build$ pstree
systemd─┬─ModemManager───2*[{ModemManager}]
        ├─NetworkManager─┬─dhclient
        │                └─2*[{NetworkManager}]
        ├─VGAuthService
        ├─accounts-daemon───2*[{accounts-daemon}]
        ├─acpid
        ├─adb───4*[{adb}]
        ....
        │         ├─gnome-terminal-─┬─bash─┬─hello
        │         │                 │      └─pstree

在这里,我们可以认为命令行窗口bash就是父进程,hello就是子进程,因为我们是在命令行窗口执行hello的。

概念 说明
父进程 创建其他进程的进程
子进程 被创建出来的新进程
类型 含义 产生原因 处理方式
僵尸进程 子进程已经结束,但其退出信息还没被父进程回收 父进程没有调用 wait()waitpid() 父进程及时回收
孤儿进程 父进程先结束,子进程还在运行 父进程提前退出 会被 1 号进程接管

接下来,我们来看看一个具体的项目需求,在做一些比较大型的项目时,我们一般会把一个项目的功能模块拆分为多个子应用,假设我们有这么一个需求,我们把一个项目分为两个程序,然后通过一个保活程序对另一个程序进行管理,包括启动,监听运行状态和重启,这时候我们应该如何实现呢?

想要实现这几个功能,我们就需要学习下如何通过程序启动另一个进程,如何对进程进行控制,接下来,我们来看看进程相关的管理函数。

进程创建的方式

fork函数

#include <unistd.h>
pid_t fork(void);
功能:fork 系统调用用于创建一个新的进程,新进程是原进程的一个副本。
子进程特性:
子进程拥有与父进程相同的代码段、数据段、堆栈段等。
子进程有自己的独立内存空间,但初始内容与父进程相同。
子进程从 fork 调用后的下一条指令开始执行。
返回值:
在父进程中返回子进程的 PID。
在子进程中返回 0。
失败时返回-1
通过fork的返回值区分父进程和子进程

示例代码如下

 #include <stdio.h>
 #include <unistd.h>
 
 int main(void)
 {
     int result;

     printf("This is a fork demo!\n\n");

     /*调用 fork()函数*/
     result = fork();
     if(result == -1) {
         /*通过 result 的值来判断 fork()函数的返回情况,首先进行出错处理*/
         printf("Fork error\n");
         return -1;
     }
     else if (result == 0) {
         /*返回值为 0 代表子进程*/
         printf("The returned value is %d, In child process!! My PID is %d\n\n", result, getpid());
     }
     else {
        /*返回值大于 0 代表父进程 resutl为子进程pid*/
         printf("The returned value is %d, In father process!! My PID is %d\n\n", result, getpid());
     }
     while(1);
     return result;
 }

然后,我们就可以通过ps查看运行后的进程情况

leo@ubuntu:~/hello/build$./hello &
leo@ubuntu:~/hello/build$ ps 
   PID TTY          TIME CMD
  2308 pts/0    00:00:00 bash
  2637 pts/0    00:00:17 hello
  2638 pts/0    00:00:17 hello

进程的终止方式

进程有两种终止方式,正常终止和异常终止。

正常终止

1、进程调用return
int main() {
    return 0; 
}

2、进程调用 exit 函数
#include <stdlib.h>
void exit(int status);
status:进程退出状态码,是进程终止时向父进程传递的一个整数值(范围为 0~255)。它的核心作用是标识进程的执行结果状态,便于父进程或调用者判断进程是否正常结束、是否发生错误。

void func() {
    exit(1);  // 在非main函数中调用,进程直接退出
}

异常终止

进程收到某些信号(如 SIGKILL、SIGSEGV)而终止。 例如前面的kill -9 93899

进程的状态监控

#include <sys/wait.h>
pid_t waitpid(pid_t pid, int *status, int options);
父进程等待子进程终止。
pid:指定等待的子进程ID:
> 0:等待 PID 等于该值的子进程。
-1:等待任意子进程(等同于 wait())。
status:同 wait(),存储子进程退出状态。
options:位掩码,常用选项:
0:阻塞等待指定子进程终止。
WNOHANG:非阻塞模式,若无子进程终止立即返回 0。

返回值:
> 0成功返回结束的子进程PID。
0 非阻塞模式(WNOHANG)下,没有子进程退出。
-1错误(如无子进程、信号中断等),通过 errno 获取具体原因。


#include <sys/wait.h>
pid_t wait(int *status);
等待任意一个子进程结束。
status:同 wait(),存储子进程退出状态。

例如,父进程等待子进程终止后,再执行

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

 int main(void)
 {
     int pid;
     printf("This is a fork demo!\n\n");
     /*调用 fork()函数*/
     pid = fork();
     if(pid == -1) {
        /*通过 pid 的值来判断 fork()函数的返回情况,首先进行出错处理*/
         printf("Fork error\n");
         return -1;
     }
     else if (pid == 0) {
         /*返回值为 0 代表子进程*/
         printf("The returned value is %d, In child process!! My PID is %d\n\n", pid, getpid());
         return 1;
     }
     else {
         /*返回值大于 0 代表父进程 resutl为子进程pid*/
         int status;
         waitpid(pid,&status,0);//阻塞等待子进程运行完成或直接调用wait(status)
         printf("child return status = %d\n",WEXITSTATUS(status));
         printf("The returned value is %d, In father process!! My PID is %d\n\n", pid, getpid());
     }
     return 0;
 }

在上面的代码中,我们可以看到,虽然我们通过fork函数创建了子进程,但是这两个进程其实是一样的,相当于子进程是父进程的复制。如果我们希望创建一个新进程,执行的是别的任务,如何创建呢?那就需要用到exec函数。

exec 系列函数:在现有进程中加载并执行新的程序。

#include <unistd.h>
int execl(const char *path, const char *arg, ...);
功能:execl 系列函数用于在当前进程中加载并执行新的程序。
参数:
path: 可执行文件的绝对路径。
arg: 可执行程序的参数列表,第一个参数通常是可执行文件的名字,最后一个参数必须是 NULL。
arg0(程序名称):通常是程序本身的名字,会作为 argv[0] 传递给新程序。
例如:execl("/bin/ls", "ls", "-l", NULL) 中的 "ls"。
后续参数(arg1, arg2, ..., NULL):从 arg1 开始是真正的命令行参数。
必须以 (char *) NULL 结尾,否则会导致未定义行为。

返回值:
成功时无返回值。
失败时返回 -1,并设置 errno。


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

int main() {
    // 执行ls -l /home
    execl("/bin/ls", "ls", "-l","/home", NULL);
    return 0;
}

如何创建一个子进程并执行新的程序:

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

int main() {
    pid_t pid = fork();
    if (pid == -1) {
        printf("fork error");
        return 1;
    } else if (pid == 0) {
        // 子进程
        printf("Child process: PID = %d\n", getpid());
        if (execl("/bin/ls", "ls", "-l", "/home/xi", NULL) < 0)//执行这个ls程序
        {
            printf("execlp error");
            return 1;
        }
    } else {
        // 父进程
        printf("Parent process: PID = %d, Child PID = %d\n", getpid(), pid);
        int status;
        waitpid(pid, &status, 0);
        printf("Child exited with status %d\n", WEXITSTATUS(status));
    }

    return 0;
}

实现一个保活进程,对另外的进程进行启动和监控。代码如下

//monitor程序
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>

#define PROGRAM1 "./hello"//需要的可执行文件

pid_t pid;

void start_program(const char *program) {
    pid = fork();
    if (pid == 0) {
        // 子进程
        execl(program, program, (char *)NULL);
        printf("execl error");  // 如果 execl 失败
        exit(1);
    } else if (pid < 0) {
        printf("fork error");
        exit(1);
    }
}

void monitor_programs() {
    while (1) {
        int status;
        pid_t result;
        usleep(10*1000);
        // 检查子进程是否已经退出
        result = waitpid(pid, &status, WNOHANG);
        if (result == 0) {
            // 返回=0意味着子进程仍在运行
        } else if (result == -1) {
            printf("waitpid error");
            exit(1);
        } else {
            // 返回>0意味子进程已退出
            printf("restart process\n");
            //加延时方便观察
            usleep(2*1000); 
            // 重新启动子进程
            start_program(PROGRAM1);
        }
    }
}

int main() {
    // 启动程序
    start_program(PROGRAM1);
    printf("start %s\n", PROGRAM1);
    // 监控程序
    monitor_programs();
    return 0;
}

最后把monitor、hello放到同一个路径下,运行monitor,我们就可以看到monitor启动了hello进程,日志如下:

Started ./hello with PID 7207
hello 
hello

这时,我们可以用kill -9 指令,杀掉进程hello,然后就可以看到monitor会监控它的状态,并重新启动它。

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐