Linux fork函数详解与共享资源

1、fork函数

在Linux系统中,fork()函数是用来创建新进程的非常重要的系统调用。通过调用fork(),一个进程可以创建一个几乎完全相同的子进程。fork函数的原型如下:
#include <unistd.h>
pid_t fork(void);
返回值:fork()在父进程和子进程中返回不同的值:
在子进程中,fork()返回0,表示这个进程是子进程。
在父进程中,fork()返回新创建的子进程的PID,用于父进程识别。
如果fork()调用失败,函数返回-1,同时设置全局变量errno来指出具体的错误原因。
fork()函数用于从一个已经存在的进程中创建一个新进程,新进程被称为子进程,而原进程称为父进程。这个过程的核心步骤如下:
1)分配内存和数据结构:内核为新创建的子进程分配独立的内存块和数据结构。
2)复制父进程数据:部分父进程的数据结构会被复制到子进程中,如文件描述符表、信号处理方式等。
3)系统进程表更新:子进程被添加到系统的进程列表中,成为独立的进程。
4)返回值和调度:fork()调用结束后,父进程和子进程分别开始执行,并根据调度器的安排决定谁先运行。
示例代码:

#include <stdio.h>
#include <unistd.h>
 
int main(void) {
    pid_t pid;
    printf("Before: pid is %d\n", getpid());
 
    pid = fork(); // 创建子进程
 
    if (pid == -1) {
        perror("fork failed");
        return 1;
    }
 
    printf("After: pid is %d, fork return %d\n", getpid(), pid);
    sleep(1);  // 延迟1秒
    return 0;
}

2、共享资源

在Linux中,fork()系统调用创建的子进程默认会与父进程共享以下资源。
‌1)文件描述符及文件状态‌
子进程继承父进程的所有文件描述符副本,并指向相同的内核文件表项,共享文件偏移量、打开模式等属性。例如,子进程修改文件偏移量会影响父进程‌。
2)进程身份信息‌
包括实际用户ID、实际组ID、有效用户ID、有效组ID、附加组ID、进程组ID、会话ID等‌。
‌3)环境相关设置‌
当前工作目录和根目录‌。
文件模式创建屏蔽字(umask)‌。
控制终端‌。
‌4)信号处理配置‌
子进程继承父进程的信号屏蔽(sigprocmask设置)和信号处理函数,以及打开文件描述符的close-on-exec标志‌。
‌5)共享存储段‌
已通过shmget()或mmap()显式创建的共享内存段会继续共享‌。
注意:
‌1)文件共享与偏移量同步‌:文件描述符指向相同文件表项,因此父子进程对同一文件的读写会互相影响偏移量‌。
‌2)内存共享限制‌:默认内存独立,共享需显式调用API;共享内存需注意同步问题(如通过信号量)‌。
‌3)资源释放‌:共享资源需显式释放,否则可能导致内存泄漏‌。
示例代码1(文件描述符):

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

int main() {
    // 打开一个文件(文件需提前存在,如 test.txt)
    int fd = open("test.txt", O_RDWR);
    if (fd == -1) {
        perror("open");
        return 1;
    }

    // 父进程写入初始内容
    write(fd, "Parent writes: ABC\n", 18);

    // 创建子进程
    pid_t pid = fork();
    if (pid == -1) {
        perror("fork");
        return 1;
    }

    if (pid == 0) { // 子进程
        // 子进程写入内容
        write(fd, "Child writes: XYZ\n", 18);
        close(fd);  // 子进程关闭文件描述符
        _exit(0);
    } else {        // 父进程
        // 父进程等待子进程结束
        wait(NULL);

        // 父进程读取文件内容
        lseek(fd, 0, SEEK_SET);  // 重置文件偏移量到开头
        char buf[100];
        ssize_t n = read(fd, buf, sizeof(buf));
        if (n > 0) {
            printf("Parent read content:\n%.*s", (int)n, buf);
        }

        close(fd);  // 父进程关闭文件描述符
    }

    return 0;
}

说明:
1)父子进程对同一文件的读写会 ‌共享文件偏移量‌,操作顺序如下:
1.1)父进程写入 ABC(文件偏移量变为 18)。
1.2)子进程写入 XYZ(从偏移量 18 开始,写入后偏移量变为 36)。
1.3)父进程读取时通过 lseek 重置偏移量到 0,读取全部内容。
2)描述符副本独立‌:子进程关闭自己的 fd 不会影响父进程的 fd
3)close-on-exec 标志
// 设置 close-on-exec 标志(子进程 exec 时会自动关闭 fd)
fcntl(fd, F_SETFD, fcntl(fd, F_GETFD) | FD_CLOEXEC);
4)竞态条件,如果父子进程不协调操作顺序,可能导致数据混乱。
// 父进程和子进程同时写入(不调用 wait)

if (pid == 0) {
    write(fd, "Child writes: XYZ\n", 18);
} else {
    write(fd, "Parent writes: DEF\n", 18);
}

输出顺序不确定,可能交替出现 ABC + DEF + XYZ 或 ABC + XYZ + DEF。
如何避免数据混乱,示例代码中通过 wait(NULL) 让父进程等待子进程结束,避免读写竞争。实际开发中可能需要信号量(Semaphore)等同步机制。

示例代码2(共享内存):

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/wait.h>

#define SHM_SIZE 1024

int main() {
    int shmid;
    key_t key = 1234; // 共享内存键值

    // 创建共享内存段
    if ((shmid = shmget(key, SHM_SIZE, IPC_CREAT | 0666)) < 0) {
        perror("shmget");
        exit(1);
    }

    // 将共享内存附加到进程地址空间
    char *shm_ptr = (char *)shmat(shmid, NULL, 0);
    if (shm_ptr == (char *)-1) {
        perror("shmat");
        exit(1);
    }

    pid_t pid = fork();
    if (pid < 0) {
        perror("fork");
        exit(1);
    } else if (pid == 0) {
        // 子进程写入数据
        printf("Child writing to shared memory...\n");
        sprintf(shm_ptr, "Hello from child process!");
        shmdt(shm_ptr); // 分离共享内存
        exit(0);
    } else {
        // 父进程等待子进程结束
        wait(NULL);
        printf("Parent read: %s\n", shm_ptr);
        shmdt(shm_ptr); // 分离共享内存

        // 删除共享内存段
        shmctl(shmid, IPC_RMID, NULL);
    }

    return 0;
}

注意:
‌1)竞争条件‌
上述代码通过 wait(NULL) 让父进程等待子进程结束,避免读写竞争。实际开发中可能需要信号量(Semaphore)等同步机制。
2)共享内存生命周期‌
共享内存会持续存在直到被显式删除(shmctl(IPC_RMID))或系统重启。

示例代码3(信号):

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

void handler(int sig) {
    printf("Process %d received signal %d\n", getpid(), sig);
}

int main() {
    signal(SIGINT, handler);  // 父进程设置信号处理函数

    pid_t pid = fork();
    if (pid == 0) {
        // 子进程
        printf("Child PID: %d\n", getpid());
        while(1);  // 等待信号
    } else {
        // 父进程
        printf("Parent PID: %d\n", getpid());
        sleep(1);
        kill(pid, SIGINT);    // 向子进程发送 SIGINT
        wait(NULL);
    }
    return 0;
}

输出结果:
Parent PID: 1234
Child PID: 1235
Process 1235 received signal 2  # 子进程调用 handler

3、共享资源同步

在 Linux 系统中,‌跨进程同步‌可通过多种机制实现:‌信号量、‌管道、‌共享内存与原子操作、‌信号、‌文件锁、‌消息队列、‌套接字。
示例:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/wait.h>
#include <signal.h>

#define SHM_SIZE 1024

// 共享内存结构体(存储数据和同步标记)
typedef struct {
    volatile sig_atomic_t data_ready;  // 原子标记,表示数据是否就绪
    char message[SHM_SIZE];            // 共享数据缓冲区
} SharedData;

SharedData *shared_mem;  // 全局共享内存指针

// 信号处理函数(子进程接收信号后读取数据)
void child_signal_handler(int sig) {
    if (sig == SIGUSR1) {
        printf("Child received signal. Reading message: %s\n", shared_mem->message);
        shared_mem->data_ready = 0;  // 标记数据已读取
    }
}

int main() {
    int shmid;
    key_t key = 1234;

    // 创建共享内存
    if ((shmid = shmget(key, sizeof(SharedData), IPC_CREAT | 0666)) < 0) {
        perror("shmget");
        exit(1);
    }

    // 附加共享内存
    shared_mem = (SharedData *)shmat(shmid, NULL, 0);
    if (shared_mem == (SharedData *)-1) {
        perror("shmat");
        exit(1);
    }

    // 初始化共享内存
    shared_mem->data_ready = 0;
    sprintf(shared_mem->message, "");

    pid_t pid = fork();
    if (pid < 0) {
        perror("fork");
        exit(1);
    } else if (pid == 0) {
        // 子进程:设置信号处理函数
        struct sigaction sa;
        sa.sa_handler = child_signal_handler;
        sigemptyset(&sa.sa_mask);
        sa.sa_flags = 0;
        if (sigaction(SIGUSR1, &sa, NULL) == -1) {
            perror("sigaction");
            exit(1);
        }

        // 子进程等待信号
        while (1) {
            if (shared_mem->data_ready) {
                // 非阻塞检查(实际由信号驱动)
                pause();  // 等待父进程发送信号
            }
        }
    } else {
        // 父进程:写入数据并通知子进程
        printf("Parent writing to shared memory...\n");
        sprintf(shared_mem->message, "Hello from parent via signal!");
        shared_mem->data_ready = 1;  // 标记数据就绪

        // 发送信号通知子进程
        kill(pid, SIGUSR1);

        // 等待子进程处理数据
        sleep(1);  // 简单同步(实际应用应使用更可靠机制)

        // 清理资源
        shmdt(shared_mem);
        shmctl(shmid, IPC_RMID, NULL);
        wait(NULL);  // 等待子进程退出
    }

    return 0;
}


4、不共享资源

1)进程标识信息‌
包括进程ID(PID)、父进程ID(PPID)、资源使用统计(如tms_utime)等‌。
2)运行时状态‌
未决信号(子进程会清空未决信号集)‌。
父进程设置的定时器或警报(alarm())‌。
‌3)独立资源副本‌
子进程拥有独立的虚拟地址空间、寄存器状态、资源限制(如CPU时间限制)等‌。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

byxdaz

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值