MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

Linux C语言prefork模型的资源分配策略

2024-05-086.5k 阅读

1. 引言

在Linux环境下,服务器编程中经常会遇到高并发的场景。为了高效地处理大量客户端请求,选择合适的模型和资源分配策略至关重要。Prefork模型是一种常用的服务器并发模型,特别适用于处理短连接请求。它通过预先创建一定数量的子进程来处理客户端请求,从而避免了每次请求到来时创建新进程的开销。本文将深入探讨Linux C语言中Prefork模型的资源分配策略,并通过代码示例进行详细说明。

2. Prefork模型概述

2.1 基本原理

Prefork模型的核心思想是在服务器启动时,预先创建一定数量的子进程。这些子进程处于等待状态,一旦有客户端请求到达,就由其中一个空闲的子进程来处理该请求。与传统的fork-on-demand模型(即每次有新请求时才创建子进程)相比,Prefork模型减少了进程创建的开销,因为进程创建是一个相对昂贵的操作,涉及到内存分配、文件描述符复制等操作。

2.2 优势与适用场景

  • 优势
    • 高效处理短连接:由于避免了频繁的进程创建,对于短连接请求的处理效率更高。
    • 资源预分配:可以在服务器启动时合理分配资源,避免运行时动态分配资源可能带来的性能问题。
  • 适用场景
    • HTTP服务器:处理大量短连接的HTTP请求,如Web服务器。
    • 数据库代理服务器:代理客户端对数据库的短连接请求。

3. 资源分配策略

3.1 进程资源分配

  • 子进程数量的确定:子进程数量的选择是一个关键因素。如果子进程数量过少,可能无法及时处理所有的客户端请求,导致请求堆积;如果子进程数量过多,会消耗过多的系统资源,如内存、文件描述符等。通常,子进程数量可以根据服务器的硬件资源(如CPU核心数、内存大小)和预计的并发请求数量来确定。例如,对于一个多核CPU的服务器,可以将子进程数量设置为CPU核心数的倍数,以充分利用多核性能。
  • 进程间通信(IPC):在Prefork模型中,父进程和子进程之间需要进行通信。常见的通信方式包括管道(pipe)、信号(signal)等。父进程可以通过管道向子进程发送任务信息,如客户端请求的具体内容;子进程可以通过信号向父进程报告自己的状态,如是否处于忙碌状态。
  • 示例代码
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
#include <sys/types.h>
#include <signal.h>

#define CHILD_PROCESSES 5

void handle_child_status(int signum) {
    // 处理子进程状态变化的信号
    int status;
    pid_t pid;
    while ((pid = waitpid(-1, &status, WNOHANG)) > 0) {
        if (WIFEXITED(status)) {
            printf("Child %d exited with status %d\n", pid, WEXITSTATUS(status));
        } else if (WIFSIGNALED(status)) {
            printf("Child %d terminated by signal %d\n", pid, WTERMSIG(status));
        }
    }
}

int main() {
    pid_t pids[CHILD_PROCESSES];
    int i;

    // 注册信号处理函数
    signal(SIGCHLD, handle_child_status);

    for (i = 0; i < CHILD_PROCESSES; i++) {
        pid_t pid = fork();
        if (pid < 0) {
            perror("fork");
            exit(EXIT_FAILURE);
        } else if (pid == 0) {
            // 子进程逻辑
            printf("Child %d started\n", getpid());
            // 模拟处理客户端请求
            sleep(2);
            printf("Child %d finished\n", getpid());
            exit(EXIT_SUCCESS);
        } else {
            // 父进程逻辑
            pids[i] = pid;
        }
    }

    // 父进程等待所有子进程结束
    for (i = 0; i < CHILD_PROCESSES; i++) {
        waitpid(pids[i], NULL, 0);
    }

    printf("All children have finished\n");
    return 0;
}

在上述代码中,父进程创建了5个子进程。每个子进程模拟处理客户端请求(通过sleep(2)模拟耗时操作),然后退出。父进程通过waitpid等待子进程结束,并通过信号处理函数handle_child_status处理子进程状态变化的信号。

3.2 内存资源分配

  • 共享内存:在Prefork模型中,共享内存是一种常用的内存资源分配方式。多个子进程可以共享同一块内存区域,从而实现数据的高效共享。例如,对于一些只读的数据,如配置文件内容、静态数据等,可以通过共享内存的方式让所有子进程访问,避免每个子进程都复制一份,节省内存空间。
  • 内存池:为了避免频繁的内存分配和释放操作,内存池技术在Prefork模型中也很有用。内存池是在程序启动时预先分配一块较大的内存空间,然后将其划分成多个小块。当子进程需要分配内存时,直接从内存池中获取小块内存;当子进程释放内存时,将小块内存返回给内存池。这样可以减少系统调用的开销,提高内存分配和释放的效率。
  • 示例代码(共享内存)
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
#include <sys/types.h>
#include <sys/shm.h>
#include <sys/ipc.h>
#include <string.h>

#define SHM_SIZE 1024

int main() {
    key_t key = ftok(".", 'a');
    if (key == -1) {
        perror("ftok");
        exit(EXIT_FAILURE);
    }

    int shmid = shmget(key, SHM_SIZE, IPC_CREAT | 0666);
    if (shmid == -1) {
        perror("shmget");
        exit(EXIT_FAILURE);
    }

    char *shmaddr = (char *)shmat(shmid, NULL, 0);
    if (shmaddr == (void *)-1) {
        perror("shmat");
        exit(EXIT_FAILURE);
    }

    pid_t pid = fork();
    if (pid < 0) {
        perror("fork");
        exit(EXIT_FAILURE);
    } else if (pid == 0) {
        // 子进程
        strcpy(shmaddr, "Hello from child");
        printf("Child wrote: %s\n", shmaddr);
        shmdt(shmaddr);
        exit(EXIT_SUCCESS);
    } else {
        // 父进程
        wait(NULL);
        printf("Parent read: %s\n", shmaddr);
        shmdt(shmaddr);
        shmctl(shmid, IPC_RMID, NULL);
    }

    return 0;
}

在这段代码中,父进程和子进程通过共享内存进行数据通信。父进程创建共享内存区域,子进程向共享内存写入数据,父进程等待子进程结束后读取共享内存中的数据。最后,父进程删除共享内存。

3.3 文件描述符资源分配

  • 继承与独立:在Prefork模型中,子进程会继承父进程打开的文件描述符。这在一些情况下很有用,例如父进程打开了一个日志文件,子进程可以直接使用这个文件描述符来记录日志。然而,对于一些需要独立处理的文件描述符,如客户端连接的套接字,每个子进程应该有自己独立的文件描述符,以避免相互干扰。
  • 文件描述符管理:为了有效地管理文件描述符,服务器可以使用文件描述符表或类似的数据结构。文件描述符表可以记录每个文件描述符的状态(如是否正在使用、属于哪个子进程等),从而方便进行资源的分配和回收。
  • 示例代码(文件描述符继承)
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
#include <sys/types.h>
#include <fcntl.h>

#define LOG_FILE "server.log"

int main() {
    int logfd = open(LOG_FILE, O_WRONLY | O_CREAT | O_APPEND, 0644);
    if (logfd == -1) {
        perror("open");
        exit(EXIT_FAILURE);
    }

    pid_t pid = fork();
    if (pid < 0) {
        perror("fork");
        exit(EXIT_FAILURE);
    } else if (pid == 0) {
        // 子进程
        dprintf(logfd, "This is a log message from child\n");
        close(logfd);
        exit(EXIT_SUCCESS);
    } else {
        // 父进程
        wait(NULL);
        dprintf(logfd, "This is a log message from parent\n");
        close(logfd);
    }

    return 0;
}

在这个示例中,父进程打开一个日志文件,子进程继承了这个文件描述符,并向日志文件写入一条消息。父进程等待子进程结束后,也向日志文件写入一条消息。

4. 负载均衡与资源调度

4.1 负载均衡策略

  • 轮询调度:轮询调度是一种简单的负载均衡策略。父进程按照顺序将客户端请求依次分配给每个子进程。例如,第一个请求分配给第一个子进程,第二个请求分配给第二个子进程,以此类推。当所有子进程都分配过一次后,重新从第一个子进程开始分配。这种策略实现简单,但没有考虑子进程的处理能力差异。
  • 加权轮询调度:加权轮询调度是在轮询调度的基础上,为每个子进程分配一个权重。权重表示子进程的处理能力,处理能力越强的子进程权重越高。父进程按照权重比例分配客户端请求,权重高的子进程会分配到更多的请求。
  • 最少连接调度:最少连接调度策略根据每个子进程当前处理的连接数来分配请求。父进程将新的客户端请求分配给当前连接数最少的子进程,这样可以保证每个子进程的负载相对均衡。
  • 示例代码(简单轮询调度)
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
#include <sys/types.h>
#include <signal.h>

#define CHILD_PROCESSES 5

void handle_child_status(int signum) {
    // 处理子进程状态变化的信号
    int status;
    pid_t pid;
    while ((pid = waitpid(-1, &status, WNOHANG)) > 0) {
        if (WIFEXITED(status)) {
            printf("Child %d exited with status %d\n", pid, WEXITSTATUS(status));
        } else if (WIFSIGNALED(status)) {
            printf("Child %d terminated by signal %d\n", pid, WTERMSIG(status));
        }
    }
}

int main() {
    pid_t pids[CHILD_PROCESSES];
    int i;

    // 注册信号处理函数
    signal(SIGCHLD, handle_child_status);

    for (i = 0; i < CHILD_PROCESSES; i++) {
        pid_t pid = fork();
        if (pid < 0) {
            perror("fork");
            exit(EXIT_FAILURE);
        } else if (pid == 0) {
            // 子进程逻辑
            printf("Child %d started\n", getpid());
            // 模拟处理客户端请求
            sleep(2);
            printf("Child %d finished\n", getpid());
            exit(EXIT_SUCCESS);
        } else {
            // 父进程逻辑
            pids[i] = pid;
        }
    }

    // 简单轮询调度
    for (i = 0; i < 10; i++) {
        int child_index = i % CHILD_PROCESSES;
        printf("Assigning request to child %d\n", pids[child_index]);
    }

    // 父进程等待所有子进程结束
    for (i = 0; i < CHILD_PROCESSES; i++) {
        waitpid(pids[i], NULL, 0);
    }

    printf("All children have finished\n");
    return 0;
}

在上述代码中,父进程通过简单的取模运算实现轮询调度,将10个请求依次分配给5个子进程。

4.2 资源动态调整

  • 根据负载调整子进程数量:服务器可以根据当前的负载情况动态调整子进程的数量。当系统负载较低时,可以适当减少子进程数量,以节省系统资源;当系统负载较高时,增加子进程数量,提高处理能力。例如,可以通过监控CPU使用率、内存使用率等指标来判断系统负载,并根据预设的阈值来决定是否调整子进程数量。
  • 资源回收与重新分配:当子进程处理完请求后,需要及时回收其占用的资源,如内存、文件描述符等。同时,如果有新的资源需求,如增加了新的客户端连接,需要重新分配资源给相应的子进程。
  • 示例代码(动态调整子进程数量)
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
#include <sys/types.h>
#include <signal.h>
#include <time.h>

#define INITIAL_CHILD_PROCESSES 5
#define MAX_CHILD_PROCESSES 10
#define MIN_CHILD_PROCESSES 2

pid_t pids[MAX_CHILD_PROCESSES];
int child_count = INITIAL_CHILD_PROCESSES;

void handle_child_status(int signum) {
    // 处理子进程状态变化的信号
    int status;
    pid_t pid;
    while ((pid = waitpid(-1, &status, WNOHANG)) > 0) {
        for (int i = 0; i < child_count; i++) {
            if (pids[i] == pid) {
                for (int j = i; j < child_count - 1; j++) {
                    pids[j] = pids[j + 1];
                }
                child_count--;
                break;
            }
        }
        if (WIFEXITED(status)) {
            printf("Child %d exited with status %d\n", pid, WEXITSTATUS(status));
        } else if (WIFSIGNALED(status)) {
            printf("Child %d terminated by signal %d\n", pid, WTERMSIG(status));
        }
    }
}

void adjust_child_processes() {
    // 模拟根据负载调整子进程数量
    srand(time(NULL));
    int load = rand() % 100;
    if (load > 80 && child_count < MAX_CHILD_PROCESSES) {
        // 负载高,增加子进程
        pid_t pid = fork();
        if (pid < 0) {
            perror("fork");
            return;
        } else if (pid == 0) {
            // 新子进程逻辑
            printf("New child %d started\n", getpid());
            // 模拟处理客户端请求
            sleep(2);
            printf("New child %d finished\n", getpid());
            exit(EXIT_SUCCESS);
        } else {
            pids[child_count++] = pid;
        }
    } else if (load < 20 && child_count > MIN_CHILD_PROCESSES) {
        // 负载低,减少子进程
        pid_t pid = pids[child_count - 1];
        kill(pid, SIGTERM);
        child_count--;
    }
}

int main() {
    int i;

    // 注册信号处理函数
    signal(SIGCHLD, handle_child_status);

    for (i = 0; i < INITIAL_CHILD_PROCESSES; i++) {
        pid_t pid = fork();
        if (pid < 0) {
            perror("fork");
            exit(EXIT_FAILURE);
        } else if (pid == 0) {
            // 子进程逻辑
            printf("Child %d started\n", getpid());
            // 模拟处理客户端请求
            sleep(2);
            printf("Child %d finished\n", getpid());
            exit(EXIT_SUCCESS);
        } else {
            pids[i] = pid;
        }
    }

    // 模拟动态调整子进程数量
    for (i = 0; i < 10; i++) {
        adjust_child_processes();
        sleep(5);
    }

    // 父进程等待所有子进程结束
    for (i = 0; i < child_count; i++) {
        waitpid(pids[i], NULL, 0);
    }

    printf("All children have finished\n");
    return 0;
}

在这段代码中,通过adjust_child_processes函数模拟根据负载动态调整子进程数量。根据随机生成的负载值,决定是否增加或减少子进程。

5. 错误处理与资源回收

5.1 进程相关错误处理

  • fork失败fork系统调用可能会因为多种原因失败,如系统资源不足(如达到进程数量限制)。当fork失败时,父进程应该记录错误日志,并根据具体情况采取相应的措施,如等待一段时间后重试,或者直接退出服务器。
  • 子进程异常终止:子进程可能由于各种原因异常终止,如段错误、除零错误等。父进程通过捕获SIGCHLD信号来处理子进程的异常终止。在信号处理函数中,父进程可以记录子进程的终止状态,并根据需要重新创建子进程,以保证服务器的正常运行。
  • 示例代码(fork失败处理)
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
#include <sys/types.h>
#include <signal.h>

void handle_child_status(int signum) {
    // 处理子进程状态变化的信号
    int status;
    pid_t pid;
    while ((pid = waitpid(-1, &status, WNOHANG)) > 0) {
        if (WIFEXITED(status)) {
            printf("Child %d exited with status %d\n", pid, WEXITSTATUS(status));
        } else if (WIFSIGNALED(status)) {
            printf("Child %d terminated by signal %d\n", pid, WTERMSIG(status));
        }
    }
}

int main() {
    pid_t pid = fork();
    if (pid < 0) {
        perror("fork");
        // 记录错误日志
        FILE *logfile = fopen("fork_error.log", "a");
        if (logfile != NULL) {
            fprintf(logfile, "fork failed at %ld\n", (long)time(NULL));
            fclose(logfile);
        }
        // 等待一段时间后重试
        sleep(5);
        pid = fork();
        if (pid < 0) {
            perror("retry fork");
            exit(EXIT_FAILURE);
        } else if (pid == 0) {
            // 子进程逻辑
            printf("Child started\n");
            exit(EXIT_SUCCESS);
        }
    } else if (pid == 0) {
        // 子进程逻辑
        printf("Child started\n");
        exit(EXIT_SUCCESS);
    }

    // 注册信号处理函数
    signal(SIGCHLD, handle_child_status);

    wait(NULL);
    printf("Parent finished\n");
    return 0;
}

在上述代码中,当fork失败时,父进程记录错误日志,并等待5秒后重试。如果重试仍然失败,则退出。

5.2 内存相关错误处理

  • 共享内存分配失败shmget系统调用用于分配共享内存,可能会因为多种原因失败,如系统资源不足、权限问题等。当共享内存分配失败时,程序应该释放已经分配的其他资源,并根据错误原因采取相应的措施,如调整权限、增加系统资源等。
  • 内存池分配失败:在内存池实现中,如果内存池中的内存已经耗尽,分配操作可能会失败。此时,程序可以选择从系统中分配更多的内存来扩展内存池,或者拒绝新的内存分配请求,并返回相应的错误信息。
  • 示例代码(共享内存分配失败处理)
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
#include <sys/types.h>
#include <sys/shm.h>
#include <sys/ipc.h>

#define SHM_SIZE 1024

int main() {
    key_t key = ftok(".", 'a');
    if (key == -1) {
        perror("ftok");
        exit(EXIT_FAILURE);
    }

    int shmid = shmget(key, SHM_SIZE, IPC_CREAT | 0666);
    if (shmid == -1) {
        perror("shmget");
        // 释放其他已分配资源
        // 这里假设没有其他资源需要释放
        // 根据错误原因采取措施,如调整权限
        // 这里只是示例,实际可能需要更复杂的处理
        exit(EXIT_FAILURE);
    }

    char *shmaddr = (char *)shmat(shmid, NULL, 0);
    if (shmaddr == (void *)-1) {
        perror("shmat");
        shmctl(shmid, IPC_RMID, NULL);
        exit(EXIT_FAILURE);
    }

    // 使用共享内存
    strcpy(shmaddr, "Hello");
    printf("Data in shared memory: %s\n", shmaddr);

    shmdt(shmaddr);
    shmctl(shmid, IPC_RMID, NULL);

    return 0;
}

在这段代码中,当shmgetshmat失败时,程序释放相关资源并根据错误情况采取相应措施(这里简单地退出)。

5.3 文件描述符相关错误处理

  • 文件打开失败open系统调用用于打开文件,可能会因为文件不存在、权限不足等原因失败。当文件打开失败时,程序应该记录错误日志,并根据具体情况决定是否继续运行。例如,如果是日志文件打开失败,程序可以选择使用标准输出代替日志文件,或者尝试以不同的权限打开文件。
  • 文件描述符关闭失败close系统调用用于关闭文件描述符,虽然很少失败,但也有可能因为系统错误而失败。当文件描述符关闭失败时,程序应该记录错误日志,并尽量释放其他相关资源,以避免资源泄漏。
  • 示例代码(文件打开失败处理)
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>

#define LOG_FILE "server.log"

int main() {
    int logfd = open(LOG_FILE, O_WRONLY | O_CREAT | O_APPEND, 0644);
    if (logfd == -1) {
        perror("open");
        // 记录错误日志
        FILE *errlog = fopen("open_error.log", "a");
        if (errlog != NULL) {
            fprintf(errlog, "Failed to open %s at %ld\n", LOG_FILE, (long)time(NULL));
            fclose(errlog);
        }
        // 尝试以不同权限打开
        logfd = open(LOG_FILE, O_WRONLY | O_CREAT | O_APPEND, 0777);
        if (logfd == -1) {
            perror("retry open");
            // 使用标准输出代替日志文件
            printf("Using stdout for logging\n");
        }
    }

    // 使用文件描述符
    if (logfd != -1) {
        dprintf(logfd, "This is a log message\n");
        close(logfd);
    }

    return 0;
}

在上述代码中,当open失败时,程序记录错误日志,并尝试以不同权限打开文件。如果仍然失败,则使用标准输出代替日志文件。

6. 性能优化

6.1 减少系统调用开销

  • 批量操作:尽量将多个小的系统调用合并成一个大的系统调用。例如,在向文件写入数据时,可以使用writev函数代替多次write函数调用,将多个数据块一次性写入文件,减少系统调用次数。
  • 缓存技术:使用缓存来减少对外部资源的访问次数,从而减少系统调用。例如,对于频繁读取的配置文件内容,可以在内存中缓存一份,当需要读取时先从缓存中获取,只有在缓存过期或不存在时才从文件中读取。
  • 示例代码(使用writev进行批量写入)
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/uio.h>
#include <fcntl.h>

#define LOG_FILE "server.log"

int main() {
    int logfd = open(LOG_FILE, O_WRONLY | O_CREAT | O_APPEND, 0644);
    if (logfd == -1) {
        perror("open");
        exit(EXIT_FAILURE);
    }

    char *msg1 = "This is the first part of the message\n";
    char *msg2 = "This is the second part of the message\n";

    struct iovec iov[2];
    iov[0].iov_base = msg1;
    iov[0].iov_len = strlen(msg1);
    iov[1].iov_base = msg2;
    iov[1].iov_len = strlen(msg2);

    ssize_t bytes_written = writev(logfd, iov, 2);
    if (bytes_written == -1) {
        perror("writev");
    } else {
        printf("Total bytes written: %zd\n", bytes_written);
    }

    close(logfd);
    return 0;
}

在这段代码中,通过writev函数将两个字符串一次性写入文件,减少了系统调用次数。

6.2 优化内存使用

  • 减少内存碎片:合理分配和释放内存,尽量避免产生内存碎片。例如,在使用内存池时,按照一定的规则分配和回收内存块,避免出现大量小的空闲内存块分散在内存空间中。
  • 优化数据结构:选择合适的数据结构来存储数据,以减少内存占用。例如,对于大量的稀疏数据,可以使用哈希表或稀疏矩阵等数据结构,而不是使用简单的数组,从而节省内存空间。
  • 示例代码(使用哈希表优化内存使用)
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#define HASH_TABLE_SIZE 100

typedef struct HashNode {
    char key[50];
    int value;
    struct HashNode *next;
} HashNode;

typedef struct HashTable {
    HashNode *table[HASH_TABLE_SIZE];
} HashTable;

unsigned long hash_function(const char *key) {
    unsigned long hash = 5381;
    int c;
    while ((c = *key++)) {
        hash = ((hash << 5) + hash) + c;
    }
    return hash % HASH_TABLE_SIZE;
}

void hash_table_insert(HashTable *ht, const char *key, int value) {
    unsigned long hash = hash_function(key);
    HashNode *node = ht->table[hash];
    if (node == NULL) {
        node = (HashNode *)malloc(sizeof(HashNode));
        strcpy(node->key, key);
        node->value = value;
        node->next = NULL;
        ht->table[hash] = node;
    } else {
        while (node->next != NULL) {
            node = node->next;
        }
        HashNode *new_node = (HashNode *)malloc(sizeof(HashNode));
        strcpy(new_node->key, key);
        new_node->value = value;
        new_node->next = NULL;
        node->next = new_node;
    }
}

int hash_table_search(HashTable *ht, const char *key) {
    unsigned long hash = hash_function(key);
    HashNode *node = ht->table[hash];
    while (node != NULL) {
        if (strcmp(node->key, key) == 0) {
            return node->value;
        }
        node = node->next;
    }
    return -1;
}

int main() {
    HashTable ht;
    memset(&ht, 0, sizeof(HashTable));

    hash_table_insert(&ht, "key1", 100);
    hash_table_insert(&ht, "key2", 200);

    int value = hash_table_search(&ht, "key1");
    if (value != -1) {
        printf("Value for key1: %d\n", value);
    } else {
        printf("Key not found\n");
    }

    return 0;
}

在这个示例中,使用哈希表来存储键值对,相比于简单的数组,对于稀疏数据可以更有效地节省内存空间。

6.3 提高CPU利用率

  • 多线程与多进程结合:在Prefork模型基础上,可以结合多线程技术进一步提高CPU利用率。例如,每个子进程内部可以使用多线程来处理不同的任务,充分利用多核CPU的性能。
  • 优化算法:选择高效的算法来处理业务逻辑,减少CPU计算时间。例如,在排序算法中,使用快速排序或归并排序代替冒泡排序等低效算法。
  • 示例代码(子进程内使用多线程)
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <pthread.h>
#include <sys/wait.h>
#include <sys/types.h>

#define THREADS_PER_CHILD 2

void *thread_function(void *arg) {
    int thread_id = *((int *)arg);
    printf("Thread %d in child started\n", thread_id);
    // 模拟处理任务
    sleep(1);
    printf("Thread %d in child finished\n", thread_id);
    pthread_exit(NULL);
}

int main() {
    pid_t pid = fork();
    if (pid < 0) {
        perror("fork");
        exit(EXIT_FAILURE);
    } else if (pid == 0) {
        // 子进程
        pthread_t threads[THREADS_PER_CHILD];
        int thread_args[THREADS_PER_CHILD];
        for (int i = 0; i < THREADS_PER_CHILD; i++) {
            thread_args[i] = i;
            if (pthread_create(&threads[i], NULL, thread_function, &thread_args[i]) != 0) {
                perror("pthread_create");
                exit(EXIT_FAILURE);
            }
        }
        for (int i = 0; i < THREADS_PER_CHILD; i++) {
            pthread_join(threads[i], NULL);
        }
        exit(EXIT_SUCCESS);
    } else {
        // 父进程
        wait(NULL);
        printf("Parent finished\n");
    }

    return 0;
}

在这段代码中,子进程内部创建了两个线程来处理任务,充分利用多核CPU的性能。

7. 总结

在Linux C语言的Prefork模型中,合理的资源分配策略对于服务器的性能和稳定性至关重要。从进程资源分配、内存资源分配到文件描述符资源分配,每个环节都需要精心设计。同时,负载均衡与资源调度、错误处理与资源回收以及性能优化等方面也不容忽视。通过深入理解和合理应用这些策略和技术,可以构建出高效、稳定的服务器应用程序,满足高并发场景下的需求。在实际开发中,需要根据具体的业务场景和服务器硬件资源进行灵活调整和优化,以达到最佳的性能和资源利用效果。