MST

星途 面试题库

面试题:C语言在Linux多进程服务器模型中的资源分配

在Linux环境下,使用C语言创建多进程服务器模型时,通常会涉及到内存、文件描述符等资源的分配。请描述如何合理分配内存资源,以避免内存泄漏,并且说明文件描述符在多进程间共享时需要注意哪些问题?
24.1万 热度难度
编程语言C

知识考点

AI 面试

面试题答案

一键面试

合理分配内存资源避免内存泄漏

  1. 动态内存分配与释放的配对使用
    • 在C语言中,使用malloccallocrealloc等函数分配内存后,必须使用相应的free函数进行释放。例如:
    char *buffer = (char *)malloc(1024);
    if (buffer!= NULL) {
        // 使用buffer
        free(buffer);
        buffer = NULL;
    }
    
    • 对于calloc分配的内存,同样使用free释放:
    int *array = (int *)calloc(10, sizeof(int));
    if (array!= NULL) {
        // 使用array
        free(array);
        array = NULL;
    }
    
    • 当使用realloc调整内存大小时,要注意返回值。如果realloc成功,会返回新的内存指针,需要用新指针替换旧指针,并释放旧指针(如果旧指针和新指针不同)。例如:
    char *new_buffer = (char *)realloc(buffer, 2048);
    if (new_buffer!= NULL) {
        buffer = new_buffer;
        // 使用新的buffer
        free(buffer);
        buffer = NULL;
    } else {
        // realloc失败处理
    }
    
  2. 内存管理函数的错误处理
    • 所有的动态内存分配函数在失败时都会返回NULL。因此,每次调用这些函数后都应该检查返回值。例如:
    char *buffer = (char *)malloc(1024);
    if (buffer == NULL) {
        perror("malloc");
        // 处理内存分配失败的情况,比如退出程序或采取其他措施
    }
    
  3. 使用智能指针或封装内存管理
    • 虽然C语言没有像C++那样原生的智能指针,但可以通过自定义结构体和函数来模拟类似的功能。例如,可以创建一个结构体来管理内存指针及其大小,并编写函数来分配、释放内存。
    typedef struct {
        char *data;
        size_t size;
    } MemoryBlock;
    
    MemoryBlock *createMemoryBlock(size_t size) {
        MemoryBlock *block = (MemoryBlock *)malloc(sizeof(MemoryBlock));
        if (block!= NULL) {
            block->data = (char *)malloc(size);
            if (block->data!= NULL) {
                block->size = size;
                return block;
            } else {
                free(block);
                return NULL;
            }
        }
        return NULL;
    }
    
    void freeMemoryBlock(MemoryBlock *block) {
        if (block!= NULL) {
            free(block->data);
            free(block);
        }
    }
    
  4. 在函数中避免内存泄漏
    • 当函数返回动态分配的内存指针时,调用者有责任释放该内存。确保在函数设计时明确这一点。例如:
    char *getString() {
        char *str = (char *)malloc(50);
        if (str!= NULL) {
            strcpy(str, "Hello, World!");
        }
        return str;
    }
    
    int main() {
        char *result = getString();
        if (result!= NULL) {
            printf("%s\n", result);
            free(result);
        }
        return 0;
    }
    

文件描述符在多进程间共享时的注意事项

  1. 文件描述符表的独立性
    • 每个进程都有自己独立的文件描述符表。当使用fork创建子进程时,子进程会继承父进程的文件描述符表,但这并不意味着它们共享同一个文件描述符对象。父子进程的文件描述符虽然值相同,但它们是各自表中的不同项,指向内核中的同一个打开文件对象。例如:
    int fd = open("test.txt", O_RDONLY);
    if (fd == -1) {
        perror("open");
        return 1;
    }
    pid_t pid = fork();
    if (pid == -1) {
        perror("fork");
        close(fd);
        return 1;
    } else if (pid == 0) {
        // 子进程
        // 可以使用fd,但和父进程的fd是不同表中的项
        close(fd);
        return 0;
    } else {
        // 父进程
        close(fd);
        wait(NULL);
        return 0;
    }
    
  2. 文件偏移量的共享
    • 由于父子进程的文件描述符指向同一个打开文件对象,它们共享文件偏移量。如果父进程读取了一部分数据,文件偏移量会移动,子进程再读取时会从父进程移动后的位置开始。例如,在父子进程都对同一个文件进行读写操作时,要注意这种共享偏移量带来的影响。可以使用lseek函数来调整文件偏移量,以满足不同的需求。
    int fd = open("test.txt", O_RDWR);
    if (fd == -1) {
        perror("open");
        return 1;
    }
    pid_t pid = fork();
    if (pid == -1) {
        perror("fork");
        close(fd);
        return 1;
    } else if (pid == 0) {
        // 子进程
        lseek(fd, 0, SEEK_SET);
        char buffer[100];
        ssize_t read_bytes = read(fd, buffer, sizeof(buffer));
        if (read_bytes > 0) {
            buffer[read_bytes] = '\0';
            printf("Child read: %s\n", buffer);
        }
        close(fd);
        return 0;
    } else {
        // 父进程
        char write_buffer[] = "Hello from parent";
        ssize_t write_bytes = write(fd, write_buffer, strlen(write_buffer));
        if (write_bytes!= strlen(write_buffer)) {
            perror("write");
        }
        close(fd);
        wait(NULL);
        return 0;
    }
    
  3. 文件描述符的关闭
    • 为了避免资源泄漏,每个进程在使用完文件描述符后都应该及时关闭。如果父进程在fork后继续使用文件描述符,子进程也使用,要确保在各自不再需要时关闭。否则,可能会导致文件描述符耗尽等问题。例如,在上述例子中,父子进程都在使用完fd后调用close(fd)
  4. 信号处理与文件描述符
    • 当在多进程环境中处理信号时,要注意信号处理函数可能会影响文件描述符的状态。例如,如果信号处理函数中调用了可能会改变文件描述符状态的函数(如close),可能会导致主程序逻辑出现问题。在信号处理函数中操作文件描述符时要格外小心,最好遵循异步信号安全的原则。可以通过设置标志位等方式,在主程序中处理文件描述符相关的操作,而不是在信号处理函数中直接操作。
  5. 跨进程传递文件描述符
    • 在一些复杂场景下,可能需要在不同进程间传递文件描述符,例如通过Unix域套接字。这需要使用特殊的机制,如sendmsgrecvmsg函数,并设置相应的控制消息来传递文件描述符。在传递过程中,要确保接收方正确接收和处理文件描述符,并且注意传递过程中的错误处理。例如:
    // 发送方代码片段
    int sockfd = socket(AF_UNIX, SOCK_STREAM, 0);
    // 连接到接收方
    struct msghdr msg;
    memset(&msg, 0, sizeof(msg));
    struct cmsghdr *cmsg;
    char cmsgbuf[CMSG_SPACE(sizeof(int))];
    int fd_to_send = open("file.txt", O_RDONLY);
    msg.msg_control = cmsgbuf;
    msg.msg_controllen = sizeof(cmsgbuf);
    cmsg = CMSG_FIRSTHDR(&msg);
    cmsg->cmsg_len = CMSG_LEN(sizeof(int));
    cmsg->cmsg_level = SOL_SOCKET;
    cmsg->cmsg_type = SCM_RIGHTS;
    *((int *)CMSG_DATA(cmsg)) = fd_to_send;
    // 使用sendmsg发送数据和文件描述符
    // 接收方代码片段
    int recv_sockfd = socket(AF_UNIX, SOCK_STREAM, 0);
    // 接受连接
    struct msghdr recv_msg;
    memset(&recv_msg, 0, sizeof(recv_msg));
    char recv_cmsgbuf[CMSG_SPACE(sizeof(int))];
    recv_msg.msg_control = recv_cmsgbuf;
    recv_msg.msg_controllen = sizeof(recv_cmsgbuf);
    // 使用recvmsg接收数据和文件描述符
    struct cmsghdr *recv_cmsg = CMSG_FIRSTHDR(&recv_msg);
    if (recv_cmsg!= NULL && recv_cmsg->cmsg_len == CMSG_LEN(sizeof(int)) &&
        recv_cmsg->cmsg_level == SOL_SOCKET && recv_cmsg->cmsg_type == SCM_RIGHTS) {
        int received_fd = *((int *)CMSG_DATA(recv_cmsg));
        // 使用received_fd
        close(received_fd);
    }