什么是进程?

其实在 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 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐