linux c语言进阶 - 进程,通信方式

发布于:2025-07-23 ⋅ 阅读:(14) ⋅ 点赞:(0)

一. 进程

概述:

进程(Process)是操作系统中资源分配和调度的基本单位,是正在运行的程序的实例。每个进程拥有独立的地址空间、代码、数据和系统资源(如打开的文件、内存、CPU时间等)。进程之间相互独立,通常通过操作系统提供的机制(如进程间通信IPC)进行交互。进程的生命周期包括创建、运行、等待、就绪和终止等状态。

简而言之:进程是操作系统对正在运行程序的管理方式,概括为进程是运行的程序

C语言中:

c语言中,main函数运行后就是一个进程,程序中所有的调度都是通过main来实现。

windows进程:(win+alt+.)

linux进程 

ps aux / ps -ef 

 top / htop

二. 进程的操作:

1. 创建进程

特性:
特性 fork() vfork()
内存复制 采用写时复制 (Copy-On-Write) 完全不复制父进程地址空间
执行顺序 父子进程执行顺序不确定 父进程阻塞直到子进程退出或 exec
地址空间 子进程获得独立地址空间 子进程共享父进程地址空间
性能开销 较高(需复制页表等元数据) 极低(无内存复制开销)
安全性 安全(内存隔离) 危险(子进程可破坏父进程内存)
 特点:
函数 标准 特点 最佳场景
fork() POSIX 安全但开销大 通用进程创建
vfork() POSIX 高效但危险 立即exec的极端优化场景
posix_spawn() POSIX.1d 安全高效的exec封装 便携式高效进程创建
clone() Linux 可定制共享资源的轻量级进程 线程/特殊IPC场景
pthread_create() POSIX 纯用户态线程创建 多线程并发
差异:
维度 fork() vfork()
调度顺序 父子进程并行执行
• 谁先运行由调度器决定
严格串行化
• 父进程被强制挂起
• 子进程完全执行完毕(或调用exec/exit)后父进程才恢复
退出要求 子进程可自由退出(exitreturn 子进程必须立即调用 exec()_exit()
• 禁止从函数返回(会破坏父进程栈)
• 禁止使用exit()(会刷新共享I/O缓冲区)
阻塞行为 父进程不会被阻塞 父进程在 vfork() 调用处阻塞等
1. fork
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <string.h> // 添加头文件以使用 strlen 函数

int main(int argc, char const *argv[])
{

    int id = getpid(); // 获取当前进程的ID
    // 1.fork() 创建子进程
    pid_t pid = fork();
    if (pid < 0)
    {
        // pid<0,代表创建失败
        printf("Fork failed.\n");
        exit(EXIT_FAILURE);
    }
    else if (pid == 0)
    {
        // 子进程
        printf("这是子进程,子id = %d,父id = %d\n", id, getppid(), getppid()); // 获取父进程ID
    }
    else
    {
        // 父进程
        printf("这是父进程%d\n", id);
    }
    return 0;
}

2. vfork
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <string.h> // 添加头文件以使用 strlen 函数

int main(int argc, char const *argv[])
{
    // 1.vfork() 创建子进程
    pid_t pid = vfork();
    if (pid < 0)
    {
        // pid<0,代表创建失败
        printf("vFork failed.\n");
        exit(EXIT_FAILURE);
    }
    else if (pid == 0)
    {
        // 子进程
        printf("这是子进程,子id = %d,父id = %d\n", getpid(), getppid()); // 获取父进程ID
        _exit(0);                                                        // 使用_exit()而不是exit(),避免影响父进程的状态
    }
    else
    {
        // 父进程
        printf("这是父进程%d\n", getpid()); // 获取当前进程的ID
    }
    return 0;
}

3. clone / posix_spawn(不讲,有时间写)

2. 生命周期

1. 流程图

2. 概述 

1. 创建状态 (New)
  • 本质:进程正在被创建

  • 触发动作:系统调用(如 fork(), vfork()

  • 关键操作

    • 分配进程控制块(PCB)

    • 初始化进程数据结构

    • 分配初始资源(PID、优先级等)

  • 持续时间:瞬时状态(微秒级)


2. 就绪状态 (Ready)
  • 本质:具备运行条件,等待CPU分配

  • 进入条件

    • 进程创建完成

    • 阻塞状态结束(如I/O完成)

    • 时间片到期被剥夺CPU

  • 核心特征

    • 位于就绪队列排队

    • 只缺CPU资源

    • 可随时被调度器选中


3. 运行状态 (Running)
  • 本质:正在CPU上执行指令

  • 触发条件:被调度器选中

  • 关键行为

    • 占用CPU执行代码

    • 可能触发状态转换:

      • 时间片用完 → 回到就绪态

      • 请求资源 → 进入阻塞态

      • 执行结束 → 进入终止态

  • 持续时间:纳秒到毫秒级(取决于时间片)


4. 阻塞状态 (Blocked/Waiting)
  • 本质:等待外部事件完成

  • 常见触发原因

    • I/O操作请求(磁盘/网络)

    • 获取互斥锁失败

    • 等待信号量

    • 定时等待(如sleep()

  • 核心特征

    • 主动让出CPU

    • 移出就绪队列

    • 资源满足后自动回就绪态


5. 终止状态 (Terminated)
  • 本质:进程执行结束

  • 触发条件

    • 正常结束(执行完毕)

    • 异常终止(收到终止信号)

    • 被父进程终止

  • 关键操作

    • 释放所有资源(内存、文件、设备)

    • PCB保留退出状态(供父进程查询)

    • 最终从系统移除

3. 代码展示 

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

int main(int argc, char const *argv[])
{
    // 进程的生命周期

    // 1.创建状态
    pid_t pid = vfork(); // 使用 vfork 创建子进程
    printf("创建状态.............");

    // 2.就绪状态
    printf("就绪状态.............");

    if (pid == 0)
    {
        // 3.运行状态
        printf("运行状态.............");
        printf("这是子进程,子id = %d,父id = %d\n", getpid(), getppid());

        // 4.阻塞状态
        printf("阻塞状态.............");
        sleep(5); // 模拟阻塞状态,等待5秒

        // 5.结束状态(销毁)
        printf("子进程结束.............");
        _exit(0); // 使用 _exit() 结束子进程,避免影响父进程的状态
    }
    else if (pid > 0)
    {
        // 父进程
        printf("父进程.............");
    }
    else
    {
        // 创建失败
        perror("vfork failed");
        exit(EXIT_FAILURE);
    }

    return 0;
}

3. 进程替换 (不太明白)

进程替换(Process Replacement)是 Unix/Linux 系统中一种核心机制,它允许正在运行的进程完全替换自身,转而执行一个全新的程序。这一机制通过 exec 系列函数实现,是操作系统动态性的关键体现。

即,此进程完全复制前一个进程,但保留此进程的pid。

1. 本质与特点
特性 说明
原地替换 不创建新进程,保留原进程的 PID、父进程关系、文件描述符等属性
内存重构 完全替换地址空间(代码段、数据段、堆栈)
无返回 成功执行后永不返回原程序(函数无返回值)
效率优势 避免创建新进程的开销(无需复制页表/内存)
2. exec 函数家族
函数 参数风格 环境变量 PATH搜索 典型使用场景
execl() 参数列表 继承 固定参数的可执行程序
execv() 指针数组 继承 动态生成参数的场景
execlp() 参数列表 继承 Shell命令执行
execvp() 指针数组 继承 执行用户输入的命令
execle() 参数列表 自定义 需要特定环境变量的程序
execve() 指针数组 自定义 系统级编程(实际系统调用)
fexecve() 指针数组 自定义 通过文件描述符指定程序(Linux特有)
📌 底层真相:所有函数最终都调用 execve() 系统调用(内核入口)
 3. 代码解释
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>

int main(int argc, char const *argv[])
{
    // 进程替换
    pid_t pid = fork();

    if (pid == 0)
    {
        // 子进程
        execlp("ls", "ls", "-l", NULL);
        perror("替换失败");
        _exit(1);
    }
    else if (pid > 0)
    {
        // 父进程
        wait(NULL);
    }
    else
    {
        // 创建失败
        perror("创建失败");
        exit(EXIT_FAILURE);
    }
    return 0;
}

总结:进程替换的本质

进程替换不是创建新进程,而是进程的重生

  1. 保留外壳(PID、资源)

  2. 替换核心(程序代码)

  3. 重置状态(信号、寄存器)

  4. 重获新生(从新入口执行)

4. 进程通信

管道

本质与类型
  • 无名管道int pipe(int fd[2])

    • 内核中的循环缓冲区(默认 64KB)

    • 单向数据流:fd[0] 只读,fd[1] 只写

    • 生命周期随进程结束

  • 有名管道mkfifo()

    • 文件系统节点(inode 标识)

    • 允许无亲缘关系进程通信

特性 无名管道 有名管道
创建方式 pipe() mkfifo()
文件系统路径 有(如 /tmp/fifo
进程关系 必须亲缘关系 任意进程
生命周期 随进程结束销毁 显式调用 unlink() 删除
打开行为 直接使用文件描述符 open(),可能阻塞
持久性 临时 持久(除非删除)
典型用途 Shell 管道、父子进程通信 无亲缘关系进程间通信
区别于IO流
特性 有名管道 (FIFO) 普通文件I/O流
物理存储 无磁盘存储
数据仅在内核缓冲区中
数据持久化存储到磁盘
数据生命周期 读取后立即消失 永久存储,可重复读取
访问方式 严格遵循FIFO顺序 支持随机访问(lseek
打开行为 需要成对进程打开(读写端)否则阻塞 单进程可独立读写
存储机制 内核维护的循环缓冲区 文件系统分配的磁盘块
最大容量 管道缓冲区大小(默认64KB) 仅受磁盘空间限制
原子性保证 写操作≤PIPE_BUF(4KB)时原子 无内置原子性保证
1. 无名管道
  • 使用 pipe(int pipefd[2]) 系统调用创建

  • 无文件系统路径,仅通过文件描述符pipefd[0]读端,pipefd[1]写端)访问

int pipefd[2];
pipe(pipefd); // 创建无名管道
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <string.h> // 添加头文件以使用 strlen 函数
#include <locale.h>

/**
 * pipe 管道,用于进程之间通信(父子进程)
 */
int main(int argc, char const *argv[])
{
    setlocale(LC_ALL, ""); // 使程序使用系统 locale,支持中文utf-8
    // 1.创建无名管道
    int fd[2];

    // 创建管道
    if (pipe(fd) == -1)
    {
        perror("pipe");
        exit(EXIT_FAILURE);
    }
    // 2.创建子进程
    pid_t pid = fork();
    if (pid < 0)
    {
        perror("fork");
        exit(EXIT_FAILURE);
    }
    else if (pid == 0)
    {
        // 子进程
        close(fd[1]); // 关闭写端
        char buffer[100];
        read(fd[0], buffer, sizeof(buffer)); // 从管道读取数据
        printf("Child process received: %s\n", buffer);
        close(fd[0]); // 关闭读端
        exit(EXIT_SUCCESS);
    }
    else
    {
        // 父进程
        close(fd[0]); // 关闭读端
        const char *message = "这是父进程发送的消息";
        write(fd[1], message, strlen(message) + 1); // 向管道写入实际字符串长度+1(包含结尾\0)
        close(fd[1]);                               // 关闭写端
    }
    return 0;
}
2.有名管道
  • 使用 mkfifo(const char *pathname, mode_t mode) 创建

  • 在文件系统中有一个路径名(如 /tmp/myfifo),像普通文件一样存在

mkfifo("/tmp/myfifo", 0666); // 创建有名管道
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <fcntl.h>
#include <string.h>

/*有名管道*/
int main(int argc, char const *argv[])
{
    // 1.创建有名管道
    const char *fifo_path = "/tmp/my_fifo";
    mkfifo(fifo_path, 0666);

    // 2.创建子进程
    pid_t pid = fork();
    if (pid == 0)
    {
        // 子进程:写入数据
        int fd = open(fifo_path, O_WRONLY);
        write(fd, "Hello from child", 17);
        close(fd);
        exit(0);
    }

    // 3.父进程:读取数据
    char buffer[100];
    int fd = open(fifo_path, O_RDONLY);
    read(fd, buffer, sizeof(buffer));
    printf("父进程读取到:%s\n", buffer);
    close(fd);

    // 4.清理
    unlink(fifo_path);
    return 0;
}

信号(Signal)

本质与类型
  • 软件中断:异步事件通知机制

  • 标准信号:如 SIGINT (Ctrl+C), SIGKILL (强制终止)

  • 实时信号:支持排队

 

system v ipc

消息队列

信号量

即,信号量即是一种状态,我如果让他,一次性等于五,他会让五个进程去操作内存,如果一共,有十个,剩余的五个则等待,进入的五个进程依次退出,剩下的五个依次进入.

  1. 计数器: 信号量本质上是一个非负整数的计数器。

  2. 操作: 对信号量的操作主要是两个原子操作(Atomic Operation):

    • P 操作 (Wait/Sleep/Down):

      • 尝试将信号量的值减 1。

      • 如果信号量值大于 0,则减 1 并立即返回(进程获得资源)。

      • 如果信号量值等于 0,则进程(或线程)会被阻塞(休眠),直到信号量值变为大于 0(有别的进程释放资源),然后它才能成功执行减 1 操作并继续执行。

    • V 操作 (Signal/Post/Up):

      • 将信号量的值加 1。

      • 如果有进程因为在该信号量上执行 P 操作而被阻塞,V 操作会唤醒其中一个(或多个,取决于实现)被阻塞的进程。

  3. 目的: 信号量用来表示可用资源的数量。P 操作代表申请一个资源,V 操作代表释放一个资源。当信号量初始化为 1 时,它就变成了一个互斥锁(Mutex),用于保证同一时刻只有一个进程可以访问临界区。

  4. 进程间信号量: 我们讨论的信号量驻留在内核中,由内核维护其状态和阻塞队列。因此,不同的进程可以通过同一个信号量的标识符(key 或 name)来访问和操作它,实现跨进程的同步。

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/wait.h>
#include <semaphore.h>

#define TOTAL_PROCS 10
#define ALLOWED_AT_ONCE 5
#define SEM_NAME "/my_semaphore_example"

void child_process(int id)
{
    // 打开已存在的信号量
    sem_t *sem = sem_open(SEM_NAME, O_RDWR);
    if (sem == SEM_FAILED)
    {
        perror("子进程信号量打开失败");
        exit(1);
    }

    printf("子进程 %d (PID:%d) 等待进入...\n", id, getpid());

    // P操作:等待通行证
    sem_wait(sem);

    // 临界区开始
    printf("✅ 子进程 %d (PID:%d) 进入临界区\n", id, getpid());
    sleep(2); // 模拟工作
    printf("🚪 子进程 %d (PID:%d) 离开临界区\n", id, getpid());
    // 临界区结束

    // V操作:归还通行证
    sem_post(sem);

    sem_close(sem);
    exit(0);
}

int main()
{
    // 创建信号量 (初始5张通行证)
    sem_t *sem = sem_open(SEM_NAME, O_CREAT | O_RDWR, 0666, ALLOWED_AT_ONCE);
    if (sem == SEM_FAILED)
    {
        perror("信号量创建失败");
        exit(1);
    }

    printf("主进程 (PID:%d) 启动,创建 %d 个子进程,信号量容量: %d\n",
           getpid(), TOTAL_PROCS, ALLOWED_AT_ONCE);

    // 创建子进程
    for (int i = 0; i < TOTAL_PROCS; i++)
    {
        pid_t pid = fork();
        if (pid == 0)
        {
            child_process(i + 1); // 子进程执行
            exit(0);
        }
    }

    // 等待所有子进程结束
    for (int i = 0; i < TOTAL_PROCS; i++)
    {
        wait(NULL);
    }

    // 清理
    sem_close(sem);
    sem_unlink(SEM_NAME); // 删除信号量
    printf("\n所有子进程完成!信号量已清理\n");
    return 0;
}

网站公告

今日签到

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