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

Linux C语言多进程的资源竞争

2022-06-156.4k 阅读

Linux C 语言多进程的资源竞争概述

在 Linux 环境下使用 C 语言进行多进程编程时,资源竞争是一个常见且必须谨慎处理的问题。多进程编程通过创建多个进程来同时执行不同的任务,从而提高系统的并发处理能力。然而,当多个进程同时访问和操作共享资源时,就可能出现资源竞争的情况,这可能导致程序出现不可预测的行为,例如数据不一致、程序崩溃等。

共享资源的类型

  1. 文件资源:多个进程可能同时需要读写同一个文件。例如,一个日志记录程序可能有多个进程同时向日志文件中写入信息。如果没有正确的同步机制,可能会导致日志内容混乱,数据丢失或重复记录。
  2. 内存资源:虽然每个进程都有自己独立的地址空间,但通过某些特殊方式,如共享内存(Shared Memory),多个进程可以访问同一块内存区域。当多个进程同时对共享内存中的数据进行读写操作时,就容易引发资源竞争。例如,一个进程可能在读取共享内存中的数据时,另一个进程同时在修改这些数据,这会导致读取到的数据不准确。
  3. 设备资源:一些硬件设备,如打印机、串口等,在同一时间只能被一个进程使用。如果多个进程尝试同时访问这些设备,就会产生资源竞争。例如,多个进程同时向打印机发送打印任务,可能导致打印出来的内容混乱。

资源竞争产生的原因

进程调度的不确定性

Linux 内核使用一种调度算法来决定在某个时刻哪个进程可以运行。这个调度算法是基于时间片的,每个进程被分配一个时间片来执行。当一个进程的时间片用完后,内核会暂停该进程并调度另一个进程运行。由于进程调度的时机是不确定的,多个进程在访问共享资源时可能会出现交错执行的情况,从而引发资源竞争。

例如,假设有两个进程 P1 和 P2 同时访问一个共享变量 count。P1 读取 count 的值为 10,正准备对其加 1 时,时间片用完,内核调度 P2 运行。P2 也读取 count 的值为 10 并加 1,然后写回 count,此时 count 的值变为 11。接着 P1 恢复运行,它继续执行加 1 操作,将 count 写回为 11,而不是预期的 12。这就是由于进程调度的不确定性导致的资源竞争问题。

异步事件的影响

除了进程调度,Linux 系统中还存在各种异步事件,如信号(Signal)。信号可以在进程运行的任何时刻到达,并且会打断进程当前的执行流程。当一个进程在处理共享资源时收到信号,并且信号处理函数也访问了相同的共享资源,就可能导致资源竞争。

例如,一个进程正在向一个共享文件中写入数据,此时收到一个信号,信号处理函数也尝试向该文件写入数据。由于两个操作没有同步,可能会导致文件内容混乱。

资源竞争的危害

数据不一致

这是资源竞争最常见的危害之一。当多个进程对共享数据进行读写操作时,如果没有正确的同步机制,数据可能会被错误地修改或读取,导致数据不一致。例如,在一个银行转账的模拟程序中,一个进程负责从账户 A 中扣除金额,另一个进程负责向账户 B 中增加相同的金额。如果这两个操作没有同步,可能会出现账户 A 扣除了金额,但账户 B 没有增加金额的情况,导致整个系统的数据不一致。

程序崩溃

在严重的情况下,资源竞争可能导致程序崩溃。例如,当多个进程同时访问共享内存时,如果一个进程错误地修改了共享内存的结构,其他进程在访问该共享内存时可能会触发段错误(Segmentation Fault),导致程序崩溃。

死锁

死锁是资源竞争的一种特殊情况,当两个或多个进程相互等待对方释放资源时,就会发生死锁。例如,进程 P1 持有资源 R1 并等待资源 R2,而进程 P2 持有资源 R2 并等待资源 R1,这样两个进程就会永远等待下去,导致系统资源被浪费,程序无法继续执行。

解决资源竞争的方法

使用文件锁

在处理文件资源竞争时,文件锁是一种常用的方法。Linux 提供了 fcntl 函数来实现文件锁。文件锁分为两种类型:建议性锁(Advisory Lock)和强制性锁(Mandatory Lock)。

  1. 建议性锁:建议性锁要求所有访问共享文件的进程都遵守锁的约定。当一个进程获取了文件锁后,其他进程在访问该文件前应该先检查是否有锁存在。如果有锁,应该等待锁被释放。以下是一个使用建议性锁的示例代码:
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>

int main() {
    int fd;
    struct flock lock;

    // 打开文件
    fd = open("test.txt", O_WRONLY | O_CREAT, 0666);
    if (fd == -1) {
        perror("open");
        exit(EXIT_FAILURE);
    }

    // 设置锁的类型为写锁
    lock.l_type = F_WRLCK;
    lock.l_whence = SEEK_SET;
    lock.l_start = 0;
    lock.l_len = 0;

    // 获取锁
    if (fcntl(fd, F_SETLKW, &lock) == -1) {
        perror("fcntl");
        close(fd);
        exit(EXIT_FAILURE);
    }

    // 持有锁期间进行文件写入操作
    write(fd, "This is a test\n", 14);

    // 释放锁
    lock.l_type = F_UNLCK;
    if (fcntl(fd, F_SETLK, &lock) == -1) {
        perror("fcntl");
    }

    close(fd);
    return 0;
}
  1. 强制性锁:强制性锁由内核强制执行,即使进程不主动检查锁,内核也会阻止未持有锁的进程访问共享文件。要使用强制性锁,需要在文件系统挂载时启用 mand 选项,并且在文件的 st_flags 字段中设置 S_ISVTX 标志。以下是一个简单的设置强制性锁的示例(假设文件系统已正确挂载并支持强制性锁):
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>

int main() {
    int fd;
    struct stat st;
    struct flock lock;

    // 打开文件
    fd = open("test.txt", O_WRONLY | O_CREAT, 0666);
    if (fd == -1) {
        perror("open");
        exit(EXIT_FAILURE);
    }

    // 获取文件状态
    if (fstat(fd, &st) == -1) {
        perror("fstat");
        close(fd);
        exit(EXIT_FAILURE);
    }

    // 设置 S_ISVTX 标志
    st.st_mode |= S_ISVTX;
    if (fchmod(fd, st.st_mode) == -1) {
        perror("fchmod");
        close(fd);
        exit(EXIT_FAILURE);
    }

    // 设置锁的类型为写锁
    lock.l_type = F_WRLCK;
    lock.l_whence = SEEK_SET;
    lock.l_start = 0;
    lock.l_len = 0;

    // 获取锁
    if (fcntl(fd, F_SETLKW, &lock) == -1) {
        perror("fcntl");
        close(fd);
        exit(EXIT_FAILURE);
    }

    // 持有锁期间进行文件写入操作
    write(fd, "This is a test\n", 14);

    // 释放锁
    lock.l_type = F_UNLCK;
    if (fcntl(fd, F_SETLK, &lock) == -1) {
        perror("fcntl");
    }

    close(fd);
    return 0;
}

使用信号量

信号量(Semaphore)是一种计数器,用于控制对共享资源的访问。在 Linux 中,可以使用 semgetsemopsemctl 函数来操作信号量。信号量的值表示当前可用的资源数量,当一个进程想要访问共享资源时,它需要获取信号量(将信号量的值减 1),当访问完成后,它需要释放信号量(将信号量的值加 1)。

以下是一个使用信号量解决共享内存资源竞争的示例代码:

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

#define SHM_SIZE 1024

union semun {
    int val;
    struct semid_ds *buf;
    unsigned short *array;
};

int main() {
    key_t key;
    int semid, shmid;
    char *shmaddr;
    union semun arg;

    // 生成唯一的键值
    key = ftok(".", 'a');
    if (key == -1) {
        perror("ftok");
        exit(EXIT_FAILURE);
    }

    // 创建信号量集
    semid = semget(key, 1, IPC_CREAT | 0666);
    if (semid == -1) {
        perror("semget");
        exit(EXIT_FAILURE);
    }

    // 初始化信号量的值为 1
    arg.val = 1;
    if (semctl(semid, 0, SETVAL, arg) == -1) {
        perror("semctl");
        semctl(semid, 0, IPC_RMID);
        exit(EXIT_FAILURE);
    }

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

    // 挂载共享内存段到进程地址空间
    shmaddr = (char *)shmat(shmid, NULL, 0);
    if (shmaddr == (void *)-1) {
        perror("shmat");
        semctl(semid, 0, IPC_RMID);
        shmctl(shmid, IPC_RMID, NULL);
        exit(EXIT_FAILURE);
    }

    // 创建子进程
    pid_t pid = fork();
    if (pid == -1) {
        perror("fork");
        semctl(semid, 0, IPC_RMID);
        shmctl(shmid, IPC_RMID, NULL);
        exit(EXIT_FAILURE);
    } else if (pid == 0) {
        // 子进程
        struct sembuf sem_op;
        sem_op.sem_num = 0;
        sem_op.sem_op = -1;
        sem_op.sem_flg = SEM_UNDO;

        // 获取信号量
        if (semop(semid, &sem_op, 1) == -1) {
            perror("semop");
            exit(EXIT_FAILURE);
        }

        // 子进程写入共享内存
        sprintf(shmaddr, "This is written by child process");

        // 释放信号量
        sem_op.sem_op = 1;
        if (semop(semid, &sem_op, 1) == -1) {
            perror("semop");
            exit(EXIT_FAILURE);
        }

        // 分离共享内存
        if (shmdt(shmaddr) == -1) {
            perror("shmdt");
            exit(EXIT_FAILURE);
        }

        exit(EXIT_SUCCESS);
    } else {
        // 父进程
        struct sembuf sem_op;
        sem_op.sem_num = 0;
        sem_op.sem_op = -1;
        sem_op.sem_flg = SEM_UNDO;

        // 获取信号量
        if (semop(semid, &sem_op, 1) == -1) {
            perror("semop");
            exit(EXIT_FAILURE);
        }

        // 父进程读取共享内存
        printf("Parent process reads: %s\n", shmaddr);

        // 释放信号量
        sem_op.sem_op = 1;
        if (semop(semid, &sem_op, 1) == -1) {
            perror("semop");
            exit(EXIT_FAILURE);
        }

        // 等待子进程结束
        wait(NULL);

        // 分离共享内存
        if (shmdt(shmaddr) == -1) {
            perror("shmdt");
            exit(EXIT_FAILURE);
        }

        // 删除共享内存段和信号量集
        shmctl(shmid, IPC_RMID, NULL);
        semctl(semid, 0, IPC_RMID);
    }

    return 0;
}

使用互斥锁(Mutex)

虽然互斥锁通常用于多线程编程,但在多进程编程中,通过共享内存和文件描述符,也可以实现类似互斥锁的功能。互斥锁的基本原理是,在任何时刻只有一个进程可以持有锁,其他进程需要等待锁被释放。

以下是一个通过共享内存和文件描述符实现类似互斥锁功能的示例代码:

#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/stat.h>

#define SHM_SIZE 1024

typedef struct {
    int lock;
    char data[SHM_SIZE];
} SharedData;

int main() {
    key_t key;
    int shmid, fd;
    SharedData *shared;

    // 生成唯一的键值
    key = ftok(".", 'a');
    if (key == -1) {
        perror("ftok");
        exit(EXIT_FAILURE);
    }

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

    // 挂载共享内存段到进程地址空间
    shared = (SharedData *)shmat(shmid, NULL, 0);
    if (shared == (void *)-1) {
        perror("shmat");
        shmctl(shmid, IPC_RMID, NULL);
        exit(EXIT_FAILURE);
    }

    // 初始化锁为未锁定状态
    shared->lock = 0;

    // 创建一个文件描述符用于同步
    fd = open("lockfile", O_CREAT | O_WRONLY, 0666);
    if (fd == -1) {
        perror("open");
        shmdt(shared);
        shmctl(shmid, IPC_RMID, NULL);
        exit(EXIT_FAILURE);
    }

    // 创建子进程
    pid_t pid = fork();
    if (pid == -1) {
        perror("fork");
        shmdt(shared);
        shmctl(shmid, IPC_RMID, NULL);
        close(fd);
        unlink("lockfile");
        exit(EXIT_FAILURE);
    } else if (pid == 0) {
        // 子进程
        while (1) {
            // 尝试获取锁
            if (flock(fd, LOCK_EX | LOCK_NB) == 0) {
                if (shared->lock == 0) {
                    shared->lock = 1;
                    flock(fd, LOCK_UN);
                    break;
                }
                flock(fd, LOCK_UN);
            }
        }

        // 子进程写入共享内存
        sprintf(shared->data, "This is written by child process");

        // 释放锁
        shared->lock = 0;

        // 分离共享内存
        if (shmdt(shared) == -1) {
            perror("shmdt");
            exit(EXIT_FAILURE);
        }

        close(fd);
        exit(EXIT_SUCCESS);
    } else {
        // 父进程
        while (1) {
            // 尝试获取锁
            if (flock(fd, LOCK_EX | LOCK_NB) == 0) {
                if (shared->lock == 0) {
                    shared->lock = 1;
                    flock(fd, LOCK_UN);
                    break;
                }
                flock(fd, LOCK_UN);
            }
        }

        // 父进程读取共享内存
        printf("Parent process reads: %s\n", shared->data);

        // 释放锁
        shared->lock = 0;

        // 等待子进程结束
        wait(NULL);

        // 分离共享内存
        if (shmdt(shared) == -1) {
            perror("shmdt");
            exit(EXIT_FAILURE);
        }

        // 删除共享内存段和文件
        shmctl(shmid, IPC_RMID, NULL);
        close(fd);
        unlink("lockfile");
    }

    return 0;
}

实际应用场景中的资源竞争处理

数据库访问

在一个多进程的数据库应用程序中,多个进程可能同时需要访问数据库进行查询、插入、更新等操作。为了避免资源竞争,数据库系统通常会使用锁机制。例如,在 MySQL 数据库中,行级锁(Row - Level Lock)可以确保在同一时间只有一个进程可以修改某一行数据。在应用程序层面,可以使用数据库提供的 API 来获取和释放锁。

以下是一个简单的使用 MySQL C API 进行数据库操作并处理资源竞争的示例代码(假设已安装 MySQL 开发库并正确配置):

#include <mysql/mysql.h>
#include <stdio.h>
#include <stdlib.h>

int main() {
    MYSQL *conn;
    MYSQL_RES *res;
    MYSQL_ROW row;

    // 初始化 MySQL 连接
    conn = mysql_init(NULL);
    if (conn == NULL) {
        fprintf(stderr, "mysql_init() failed\n");
        return 1;
    }

    // 连接到 MySQL 服务器
    if (mysql_real_connect(conn, "localhost", "user", "password", "testdb", 0, NULL, 0) == NULL) {
        fprintf(stderr, "mysql_real_connect() failed\n");
        mysql_close(conn);
        return 1;
    }

    // 开始事务
    if (mysql_query(conn, "START TRANSACTION")) {
        fprintf(stderr, "START TRANSACTION query failed\n");
        mysql_close(conn);
        return 1;
    }

    // 获取行级锁
    if (mysql_query(conn, "SELECT * FROM users WHERE id = 1 FOR UPDATE")) {
        fprintf(stderr, "SELECT FOR UPDATE query failed\n");
        mysql_query(conn, "ROLLBACK");
        mysql_close(conn);
        return 1;
    }

    // 执行更新操作
    if (mysql_query(conn, "UPDATE users SET balance = balance - 100 WHERE id = 1")) {
        fprintf(stderr, "UPDATE query failed\n");
        mysql_query(conn, "ROLLBACK");
        mysql_close(conn);
        return 1;
    }

    // 提交事务
    if (mysql_query(conn, "COMMIT")) {
        fprintf(stderr, "COMMIT query failed\n");
        mysql_close(conn);
        return 1;
    }

    // 关闭连接
    mysql_close(conn);
    return 0;
}

网络服务器

在多进程的网络服务器中,例如 HTTP 服务器,多个进程可能同时处理来自不同客户端的请求。如果服务器需要访问一些共享资源,如缓存、配置文件等,就需要处理资源竞争问题。一种常见的方法是使用线程池(Thread Pool)或进程池(Process Pool)来管理请求处理,并且使用锁机制来保护共享资源。

以下是一个简单的多进程 HTTP 服务器示例,使用互斥锁来保护共享的客户端连接计数:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <pthread.h>

#define PORT 8080
#define MAX_CLIENTS 10

int client_count = 0;
pthread_mutex_t mutex;

void handle_client(int client_socket) {
    char buffer[1024] = {0};
    int valread = read(client_socket, buffer, 1024);
    if (valread < 0) {
        perror("read");
        close(client_socket);
        return;
    }

    // 加锁保护共享资源
    pthread_mutex_lock(&mutex);
    client_count++;
    printf("Current client count: %d\n", client_count);
    pthread_mutex_unlock(&mutex);

    char response[] = "HTTP/1.1 200 OK\r\nContent - Type: text/html\r\n\r\n<html><body>Hello, World!</body></html>";
    send(client_socket, response, strlen(response), 0);

    // 加锁保护共享资源
    pthread_mutex_lock(&mutex);
    client_count--;
    printf("Current client count: %d\n", client_count);
    pthread_mutex_unlock(&mutex);

    close(client_socket);
}

int main(int argc, char const *argv[]) {
    int server_fd, new_socket;
    struct sockaddr_in address;
    int opt = 1;
    int addrlen = sizeof(address);

    // 创建套接字
    if ((server_fd = socket(AF_INET, SOCK_STREAM, 0)) == 0) {
        perror("socket failed");
        exit(EXIT_FAILURE);
    }

    // 设置套接字选项
    if (setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR | SO_REUSEPORT, &opt, sizeof(opt))) {
        perror("setsockopt");
        close(server_fd);
        exit(EXIT_FAILURE);
    }

    address.sin_family = AF_INET;
    address.sin_addr.s_addr = INADDR_ANY;
    address.sin_port = htons(PORT);

    // 绑定套接字到地址和端口
    if (bind(server_fd, (struct sockaddr *)&address, sizeof(address)) < 0) {
        perror("bind failed");
        close(server_fd);
        exit(EXIT_FAILURE);
    }

    // 监听连接
    if (listen(server_fd, MAX_CLIENTS) < 0) {
        perror("listen");
        close(server_fd);
        exit(EXIT_FAILURE);
    }

    // 初始化互斥锁
    if (pthread_mutex_init(&mutex, NULL) != 0) {
        perror("mutex init failed");
        close(server_fd);
        exit(EXIT_FAILURE);
    }

    printf("Server is listening on port %d\n", PORT);

    while (1) {
        if ((new_socket = accept(server_fd, (struct sockaddr *)&address, (socklen_t *)&addrlen)) < 0) {
            perror("accept");
            continue;
        }

        pid_t pid = fork();
        if (pid == -1) {
            perror("fork");
            close(new_socket);
        } else if (pid == 0) {
            close(server_fd);
            handle_client(new_socket);
            exit(EXIT_SUCCESS);
        } else {
            close(new_socket);
        }
    }

    // 销毁互斥锁
    pthread_mutex_destroy(&mutex);
    close(server_fd);
    return 0;
}

总结资源竞争处理的要点

  1. 选择合适的同步机制:根据共享资源的类型和应用场景,选择合适的同步机制,如文件锁适用于文件资源,信号量和互斥锁适用于内存资源等。
  2. 避免死锁:在使用同步机制时,要注意避免死锁的发生。例如,在获取多个锁时,按照相同的顺序获取锁可以有效防止死锁。
  3. 性能优化:同步机制虽然可以解决资源竞争问题,但也会带来一定的性能开销。在设计程序时,要尽量减少不必要的同步操作,以提高程序的性能。

通过正确处理 Linux C 语言多进程编程中的资源竞争问题,可以确保程序的稳定性和可靠性,充分发挥多进程编程的优势。