重点:
        

fork之后会创建和父进程一模一样的子进程,子进程会拷贝父进程的文本段,数据段,堆区,栈区,二者的虚拟地址是相同的,但通过MMU分配到不同的物理地址

fork之后,父子两个进程都活着,并且会执行同样的程序(fork以下的程序),如果只想让父程序运行的话,需要先用exit释放子进程,否则子进程会同样进行与父进程一样的程序

1. 什么是进程

进程是 Linux 系统中正在运行的程序实例,是操作系统进行资源分配和调度的基本单位。简单来说,程序是静态的、存放在磁盘上的文件,而进程是程序被加载到内存并执行后的动态实体。

每个进程都有独立的地址空间、文件描述符、环境变量和运行状态。Linux 通过进程控制块(PCB,即 task_struct)来管理每个进程,其中记录了进程的 PID、状态、优先级、内存指针、打开的文件等信息。

2. 进程与线程的区别

进程是资源分配的基本单位,线程是 CPU 调度的基本单位。一个进程可以包含多个线程,它们共享进程的地址空间和资源,但各自拥有独立的栈和寄存器上下文。

  • 资源开销:创建进程需要分配独立的地址空间,开销较大;创建线程只需分配栈空间,开销较小。
  • 通信方式:进程间通信(IPC)需要借助管道、消息队列、共享内存等机制;同一进程内的线程可直接读写共享内存。
  • 独立性:进程之间相互独立,一个进程崩溃通常不影响其他进程;线程共享进程资源,一个线程出错可能导致整个进程退出。

3. 进程的状态

Linux 进程在生命周期中会经历多种状态,可以通过 ps 命令查看进程当前状态。常见状态如下:

状态码 状态名称 说明
R 运行态 进程正在运行或处于运行队列中,等待 CPU 调度。
S 可中断睡眠态 进程正在等待某个事件或资源,可被信号唤醒。
D 不可中断睡眠态 进程通常在内核态等待 I/O 完成,不能响应信号。
T 停止态 进程被暂停,可通过 SIGCONT 信号恢复运行。
Z 僵尸态 进程已结束,但父进程尚未回收其退出状态。

4. 进程的创建与退出

在 Linux 中,进程主要通过 fork()exec 系列函数创建。下面是一个简单的 C 语言示例:

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

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

    if (pid < 0) {
        perror("fork failed");
        return 1;
    } else if (pid == 0) {
        // 子进程
        printf("子进程 PID: %d\n", getpid());
    } else {
        // 父进程
        printf("父进程 PID: %d,子进程 PID: %d\n", getpid(), pid);
        wait(NULL);  // 等待子进程结束,避免产生僵尸进程
    }
    return 0;
}

编译运行后,可以看到父进程和子进程分别打印各自的 PID。子进程通过 exit() 退出,父进程通过 wait() 回收子进程资源。

4.1 fork() 详解

fork() 是 Linux 中创建进程的核心系统调用,调用一次却会返回两次:在父进程中返回子进程的 PID(大于 0),在子进程中返回 0。如果创建失败则返回 -1。fork 采用写时复制(Copy-on-Write)技术,子进程刚创建时与父进程共享同一份物理内存,只有某一方修改数据时才真正复制,因此创建开销较小。

fork 之后,父子进程的执行顺序是不确定的,由内核调度器决定。父进程和子进程拥有独立的地址空间,互不影响,但会继承父进程的文件描述符、环境变量、信号处理方式等属性。

4.2 PID 与 PPID

每个进程都有唯一的进程标识符 PID(Process ID),用于在内核中唯一标识一个进程。PID 由内核分配,通常从 1 开始递增,并在进程退出后可能被复用。进程还可以通过 getpid() 获取自身 PID,通过 getppid() 获取父进程 PID(PPID)。

在 Linux 中,PID 为 1 的进程是 init 或 systemd,它是所有进程的祖先。当某个进程的父进程先退出时,该进程会成为孤儿进程,并被 init 进程收养。

4.3 exit() 与退出状态

exit() 用于终止当前进程,并返回一个退出状态码给父进程。退出码 0 通常表示正常退出,非 0 表示异常或错误。进程退出时,内核会释放其占用的内存、文件描述符等资源,但进程控制块(PCB)会保留,直到父进程调用 wait()waitpid() 回收。

如果父进程没有回收子进程的退出状态,子进程就会变成僵尸进程(Zombie)。僵尸进程不占用 CPU 和内存,但会占用一个 PID 和 PCB 条目,长期积累会耗尽系统进程表。因此父进程应及时调用 wait() 回收子进程,避免产生僵尸进程。

4.4 exec 系列函数

exec 系列函数(如 execlexecvexecvp 等)用于在当前进程中加载并执行一个新的程序。exec 不会创建新进程,而是用新程序替换当前进程的代码段、数据段和堆栈,但 PID 保持不变。常见的组合是 fork 后立即调用 exec,让子进程执行新的程序。

exec 调用成功后不会返回;只有调用失败时才会返回 -1。因此 exec 之后通常紧跟错误处理代码。

5. 进程的查看与管理

Linux 提供了丰富的命令用于查看和管理进程,日常使用频率较高的有:

  • ps:查看当前终端或系统中的进程快照,常用组合 ps -efps aux
  • top:动态查看系统进程和资源占用情况,按 CPU 或内存排序。
  • htop:top 的增强版,支持交互式操作,显示更友好。
  • kill:向进程发送信号,默认发送 SIGTERM(15),强制结束用 kill -9
  • pstree:以树状结构展示进程之间的父子关系。

6. 进程间通信(IPC)

进程之间相互独立,若需要交换数据,必须借助内核提供的通信机制。常用的 IPC 方式包括:

  • 管道(Pipe):用于父子进程或兄弟进程之间的单向数据传输,分为匿名管道和命名管道(FIFO)。
  • 消息队列:内核维护的消息链表,进程可以向队列写入或读取消息,支持双向通信。
  • 共享内存:多个进程映射同一块物理内存,读写速度最快,但需要配合信号量解决同步问题。
  • 信号量:用于进程间的同步与互斥,常与共享内存搭配使用。
  • 套接字(Socket):不仅支持本机进程通信,还支持跨主机的网络通信。

7. 守护进程

守护进程(Daemon)是长期运行在后台、不依赖终端会话的特殊进程,常用于提供系统服务,如 sshd、nginx、crond 等。编写守护进程通常需要以下步骤:

  1. 调用 fork() 创建子进程,父进程退出,使子进程成为孤儿进程并被 init 收养。
  2. 调用 setsid() 创建新的会话,脱离控制终端。
  3. 修改工作目录为根目录 /,避免占用可卸载的文件系统。
  4. 重设文件权限掩码 umask(0),确保创建文件时权限可控。
  5. 关闭标准输入、输出和错误文件描述符,避免占用终端资源。

8. 总结

Linux 进程是系统编程的核心概念,理解进程的状态转换、创建退出机制以及进程间通信方式,是深入学习多进程编程和系统调优的基础。建议在掌握上述内容后,动手编写 fork、管道和共享内存的示例程序,通过实际运行观察进程行为,加深理解。

Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐