linux实验6(GPT问答版)

一、Q:Linux:编写一个程序,把一个文件的内容复制到另一个文件上,即实现简单的 copy 功能。要求:只用 open ()、read ()、write () 和 close () 系统调用,程序的第一个参数是源文件,第二个参数是目的文件。

A:以下是一个使用 C 编写的简单 Linux 程序,实现文件复制功能,仅使用 open()read()write()close() 系统调用:

#include <stdio.h>
#include <fcntl.h>    // 包含 open() 函数
#include <unistd.h>   // 包含 read(), write(), close() 函数
#include <stdlib.h>   // 包含 exit() 函数

#define BUFFER_SIZE 1024  // 定义缓冲区大小

int main(int argc, char *argv[]) {
    if (argc != 3) {
        // 如果参数个数不对,提示用法并退出
        fprintf(stderr, "Usage: %s <source file> <destination file>\n", argv[0]);
        exit(1);
    }

    // 打开源文件
    int source_fd = open(argv[1], O_RDONLY);
    if (source_fd < 0) {
        perror("Error opening source file");
        exit(1);
    }

    // 打开目标文件(不存在则创建)
    int dest_fd = open(argv[2], O_WRONLY | O_CREAT | O_TRUNC, 0644);
    if (dest_fd < 0) {
        perror("Error opening destination file");
        close(source_fd);
        exit(1);
    }

    // 缓冲区用于存储读取的数据
    char buffer[BUFFER_SIZE];
    ssize_t bytes_read, bytes_written;

    // 循环读取源文件内容,并写入目标文件
    while ((bytes_read = read(source_fd, buffer, BUFFER_SIZE)) > 0) {
        bytes_written = write(dest_fd, buffer, bytes_read);
        if (bytes_written != bytes_read) {
            perror("Error writing to destination file");
            close(source_fd);
            close(dest_fd);
            exit(1);
        }
    }

    // 检查是否发生读取错误
    if (bytes_read < 0) {
        perror("Error reading source file");
    }

    // 关闭文件
    close(source_fd);
    close(dest_fd);

    printf("File copied successfully.\n");
    return 0;
}

使用说明:

  1. 将以上代码保存为 copy.c 文件。
  2. 使用 GCC 编译程序:gcc -o copy copy.c
  3. 运行程序,例如:./copy source.txt destination.txt

程序的核心步骤:

  1. 检查参数:确认输入和输出文件路径是否通过命令行提供。
  2. 打开文件
    • 源文件以只读模式打开。
    • 目标文件以写模式打开。如果文件不存在,则创建它。
  3. 数据传输
    • 使用一个缓冲区循环读取源文件的内容(read()),然后写入目标文件(write())。
    • 每次操作都需要检查返回值,以确保没有发生错误。
  4. 关闭文件
    • 操作完成后,使用 close() 关闭源文件和目标文件。

注意事项:

  1. 如果目标文件已存在,程序会覆盖该文件(通过 O_TRUNC 标志)。
  2. 文件权限是 0644,即新创建的文件默认拥有用户读写权限和其他用户的只读权限。
  3. 缓冲区大小为 1024,可以根据需要调整。

这样编写的程序能满足题目要求,且具有一定的鲁棒性,适合初学者学习 Linux 系统调用。

Q:解释一下用到的系统函数

A:在上述程序中,用到的系统调用函数主要有:open()read()write()close(),下面逐一进行详细解释。


1. open()

功能:用于打开文件。

头文件

#include <fcntl.h>

函数原型

int open(const char *pathname, int flags, mode_t mode);

参数说明

  • pathname:文件路径,表示要打开的文件。
  • flags:打开方式,常用标志包括:
    • O_RDONLY:以只读方式打开文件。
    • O_WRONLY:以只写方式打开文件。
    • O_RDWR:以读写方式打开文件。
    • O_CREAT:如果文件不存在,则创建文件(需要指定 mode 参数)。
    • O_TRUNC:如果文件已存在,则清空文件内容。
    • O_APPEND:以追加方式写入文件。
  • mode:文件权限(仅在创建文件时有效,如使用了 O_CREAT 标志),通常以八进制表示,例如:
    • 0644:用户可读写,组用户和其他用户可读。
    • 0600:只有文件所有者可读写。

返回值

  • 成功:返回文件描述符(一个非负整数,用于后续的文件操作)。
  • 失败:返回 -1,并设置 errno

例子

int fd = open("file.txt", O_RDONLY);
if (fd < 0) {
    perror("Error opening file");
}

2. read()

功能:从文件中读取数据。

头文件

#include <unistd.h>

函数原型

ssize_t read(int fd, void *buf, size_t count);

参数说明

  • fd:文件描述符(通过 open() 获得)。
  • buf:指向存储读取数据的缓冲区。
  • count:指定读取的字节数。

返回值

  • 成功:返回实际读取到的字节数(可能小于 count,如读取到文件末尾)。
  • 失败:返回 -1,并设置 errno

例子

char buffer[1024];
ssize_t bytes_read = read(fd, buffer, sizeof(buffer));
if (bytes_read < 0) {
    perror("Error reading file");
}

注意

  • 当返回值为 0 时,表示已经到达文件末尾(EOF)。
  • 如果读取时发生错误,例如文件不存在或权限不足,会返回 -1

3. write()

功能:向文件中写入数据。

头文件

#include <unistd.h>

函数原型

ssize_t write(int fd, const void *buf, size_t count);

参数说明

  • fd:文件描述符。
  • buf:指向要写入的数据的缓冲区。
  • count:要写入的字节数。

返回值

  • 成功:返回实际写入的字节数(通常等于 count)。
  • 失败:返回 -1,并设置 errno

例子

const char *data = "Hello, world!";
ssize_t bytes_written = write(fd, data, strlen(data));
if (bytes_written < 0) {
    perror("Error writing to file");
}

注意

  • 如果目标文件空间不足,可能写入部分数据,这时返回的字节数会小于 count
  • 如果 write() 返回值与 count 不相等,通常需要重新写入剩余的数据。

4. close()

功能:关闭文件描述符。

头文件

#include <unistd.h>

函数原型

int close(int fd);

参数说明

  • fd:需要关闭的文件描述符。

返回值

  • 成功:返回 0
  • 失败:返回 -1,并设置 errno

例子

if (close(fd) < 0) {
    perror("Error closing file");
}

注意

  • 关闭文件描述符可以释放文件相关的资源。
  • 如果不及时关闭文件描述符,可能导致资源泄漏,甚至超过系统允许的文件描述符上限。

程序中的系统调用组合解释

  1. 打开源文件和目标文件

    • 使用 open() 以只读模式打开源文件。
    • 使用 open() 以写模式(带创建和截断标志)打开目标文件。
  2. 读取和写入文件

    • 使用 read() 从源文件中读取数据到缓冲区。
    • 使用 write() 将缓冲区中的数据写入目标文件。
    • 循环执行,直到读取完所有数据。
  3. 关闭文件

    • 使用 close() 关闭文件描述符,释放系统资源。

总结

  • open():打开文件,获取文件描述符。
  • read():读取文件内容。
  • write():写入文件内容。
  • close():关闭文件描述符。

这些系统调用是 Linux 文件 I/O 的核心,通过它们可以实现高效的文件操作,同时也避免了高级文件操作接口(如 C 标准库 fopen())带来的额外开销。

二、Q:Linux编写一个程序,首先打开一个文件,然后利用 fork () 创建一个子进程;随后,当父进程运行时先执行 write ();父子进程都打印自己和其父进程的 ID 号;并且二者都向该文件写入 (利用 write ()) 一条信息,表明置在哪个进程中。试问:若没有 wait () 调用,会出现什么情况?

A:以下是完成上述功能的 C 程序,同时对程序行为进行了分析,特别是当没有 wait() 调用时会出现的情况。


程序代码

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

int main() {
    // 打开文件,准备写入内容
    int fd = open("output.txt", O_WRONLY | O_CREAT | O_TRUNC, 0644);
    if (fd < 0) {
        perror("Error opening file");
        exit(1);
    }

    // 创建子进程
    pid_t pid = fork();

    if (pid < 0) {
        perror("Fork failed");
        close(fd);
        exit(1);
    }

    if (pid == 0) {
        // 子进程逻辑
        char child_msg[] = "This is written by the child process.\n";
        dprintf(fd, "Child process ID: %d, Parent process ID: %d\n", getpid(), getppid());
        write(fd, child_msg, sizeof(child_msg) - 1);
        printf("Child: PID = %d, Parent PID = %d\n", getpid(), getppid());
    } else {
        // 父进程逻辑
        char parent_msg[] = "This is written by the parent process.\n";
        dprintf(fd, "Parent process ID: %d, Child process ID: %d\n", getpid(), pid);
        write(fd, parent_msg, sizeof(parent_msg) - 1);
        printf("Parent: PID = %d, Child PID = %d\n", getpid(), pid);

        // 如果没有 wait(),父进程将不会等待子进程结束
        // wait(NULL);  // 可取消注释,观察加入 wait() 后的效果
    }

    // 关闭文件
    close(fd);

    return 0;
}

程序解析

  1. 打开文件

    • 使用 open() 以写模式打开文件 output.txt,如果文件不存在则创建。
    • 设置了 O_TRUNC 标志,确保文件内容在程序启动时被清空。
  2. 创建子进程

    • 使用 fork() 创建子进程。
    • fork() 的返回值:
      • 返回 0 表示当前是子进程。
      • 返回子进程的 PID 表示当前是父进程。
      • 返回负值表示进程创建失败。
  3. 父进程和子进程的行为

    • 父进程
      • 打印自己的 PID 和子进程的 PID。
      • 向文件写入父进程相关的信息。
    • 子进程
      • 打印自己的 PID 和父进程的 PID。
      • 向文件写入子进程相关的信息。
  4. 没有 wait() 的影响

    • 如果没有 wait(),父进程不会等待子进程完成,而是立即继续执行(可能先于子进程结束)。
    • 在这种情况下,父进程可能比子进程先完成,而子进程仍在运行,变成孤儿进程,由 init 进程接管。
    • 文件内容的写入顺序可能变得不可预测,因为父子进程是并发运行的。

运行结果分析

文件内容

在没有 wait() 的情况下,文件 output.txt 的内容可能会出现以下几种情况:

正常写入(无竞争)

Parent process ID: 1234, Child process ID: 1235
This is written by the parent process.
Child process ID: 1235, Parent process ID: 1234
This is written by the child process.

竞争条件(进程调度引起的写入顺序变化)

Child process ID: 1235, Parent process ID: 1234
This is written by the child process.
Parent process ID: 1234, Child process ID: 1235
This is written by the parent process.

父进程完成,子进程仍在写入
文件可能只包含子进程的内容,或者文件的写入内容不完整(具体取决于调度和缓冲行为)。

控制台输出

输出父子进程的 ID,顺序可能不确定:

Parent: PID = 1234, Child PID = 1235
Child: PID = 1235, Parent PID = 1234

或者:

Child: PID = 1235, Parent PID = 1234
Parent: PID = 1234, Child PID = 1235

若加入 wait() 的效果

在父进程中加入 wait() 后:

  1. 父进程会等待子进程结束,然后再继续执行。
  2. 文件内容的顺序将始终保持一致:
Parent process ID: 1234, Child process ID: 1235
This is written by the parent process.
Child process ID: 1235, Parent process ID: 1234
This is written by the child process.

控制台输出顺序也会固定为:

Parent: PID = 1234, Child PID = 1235
Child: PID = 1235, Parent PID = 1234

没有 wait() 的问题总结

  1. 不确定的执行顺序

    • 父子进程是并发执行的,因此文件内容的写入顺序可能不同,每次运行结果可能变化。
  2. 资源未释放

    • 如果父进程没有调用 wait(),子进程的退出状态不会被回收,子进程会变成 僵尸进程(Zombie Process)。
  3. 孤儿进程

    • 如果父进程比子进程先结束,子进程会成为孤儿进程,由 init 进程(PID 为 1)接管。

解决建议

为了避免上述问题:

  1. 使用 wait()waitpid() 来确保父进程等待子进程完成。
  2. 如果父进程不需要等待子进程,可以用 SIGCHLD 信号处理僵尸进程,或者直接使用 daemon() 函数生成孤儿进程。

三、解释下面一段代码

/* pipedemo.c——演示使用管道机制进行I/O控制 */
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
int main(int argc, char **argv){
    int p_fd[2];
    pid_t pid;
    char buf[1024];

    if (pipe(p_fd) < 0) {
        perror("pipe");
        return -1;
    }

    if ((pid = fork()) < 0) {
        perror("fork");
        return -1;
    }
    else if (pid == 0) {
        close(p_fd[1]);
        read(p_fd[0], buf, 1024);
        printf("Child read: %s", buf);
        close(p_fd[0]);
        return 0;
    }
    else {
        close(p_fd[0]);
        write(p_fd[1], "Hello, world!", 13);
        close(p_fd[1]);
        wait(NULL);
        return 0;
    }}

A:这段程序使用 管道(pipe) 机制在父进程和子进程之间传递数据。以下是对代码的详细解析和说明:


程序功能概述

  1. 使用 pipe() 创建一个管道,形成父子进程之间的通信通道。
  2. 使用 fork() 创建子进程。
  3. 父进程通过管道向子进程写入一段消息 "Hello, world!"
  4. 子进程从管道中读取这段消息并打印到标准输出。

代码关键点分析

1. pipe()

管道是 UNIX 中的一种进程间通信机制,pipe() 创建一个匿名的管道,生成两个文件描述符(p_fd[0]p_fd[1]):

  • p_fd[0]:管道的读取端。
  • p_fd[1]:管道的写入端。
if (pipe(p_fd) < 0) {
    perror("pipe");
    return -1;
}

如果 pipe() 返回值小于 0,说明管道创建失败。


2. fork()

fork() 用于创建子进程,返回值有三种情况:

  • 返回负值:创建子进程失败。
  • 返回 0:当前是子进程。
  • 返回子进程的 PID:当前是父进程。

3. 子进程逻辑

else if (pid == 0) {
    close(p_fd[1]);                 // 关闭写入端(子进程只需要读取)
    read(p_fd[0], buf, 1024);       // 从管道读取数据到缓冲区 buf
    printf("Child read: %s", buf);  // 打印读取到的内容
    close(p_fd[0]);                 // 关闭读取端
    return 0;                       // 子进程退出
}
  • 子进程关闭了管道的写入端(p_fd[1]),确保它只使用管道的读取端。
  • 使用 read() 从管道读取数据,读取到的内容存储在 buf 中。
  • 打印读取到的消息,并关闭读取端后退出。

4. 父进程逻辑

else {
    close(p_fd[0]);                 // 关闭读取端(父进程只需要写入)
    write(p_fd[1], "Hello, world!", 13); // 向管道写入数据
    close(p_fd[1]);                 // 写入完成后关闭写入端
    wait(NULL);                     // 等待子进程退出
    return 0;                       // 父进程退出
}
  • 父进程关闭了管道的读取端(p_fd[0]),确保它只使用管道的写入端。
  • 使用 write() 向管道写入一段消息 "Hello, world!"
  • 写入完成后关闭写入端,并调用 wait() 等待子进程结束,避免子进程变成僵尸进程。

程序运行示例

假设程序文件名为 pipedemo.c,可以按如下方式编译并运行:

gcc -o pipedemo pipedemo.c
./pipedemo

程序输出

Child read: Hello, world!

程序中的关键概念

1. 管道(pipe)

  • 管道是一种单向通信机制(默认情况下),数据只能从写入端流向读取端。
  • 匿名管道的特性:
    • 必须由亲缘关系的进程共享(如父子进程)。
    • 写入端关闭后,读取端会返回 EOF。

2. 同步机制

  • 在管道通信中,父进程和子进程的执行顺序是不确定的。
  • 关键同步点
    • 父进程在写入数据后关闭写入端,这会让子进程的 read() 在读取到数据后立即返回。
    • 父进程调用 wait() 等待子进程结束,避免出现子进程成为僵尸进程的情况。

如果移除 wait(NULL),会发生什么?

  1. 潜在问题

    • 如果父进程没有调用 wait(),它可能在子进程完成之前结束运行。
    • 子进程会成为 僵尸进程(Zombie Process),因为父进程未收集子进程的退出状态。
    • 僵尸进程不会消耗 CPU,但会占用系统资源(如 PID 表项)。
  2. 影响

    • 子进程的输出可能仍然会正常显示,但系统中会残留一个僵尸进程,直到父进程结束或父进程被 init 接管。

改进建议

  1. 使用 wait()

    • 始终调用 wait(),确保父进程正确收集子进程的退出状态。
  2. 更健壮的实现
    如果需要避免僵尸进程,也可以在父进程中使用 SIGCHLD 信号处理:

    signal(SIGCHLD, SIG_IGN);
    

这样,子进程退出时,系统会自动回收其资源。


总结

  • 该程序演示了使用管道在父子进程间传递数据。
  • 关键点在于正确关闭管道的读写端,避免资源泄漏或阻塞。
  • 调用 wait() 是良好实践,可以确保子进程正确退出,避免僵尸进程的问题。

四、解释代码

/* msg_q.c - 演示消息队列的写入和读出 */
#include <unistd.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define BUFSZ 512
/* 消息结构 */
struct msg {
    long type;
    char text[BUFSZ];};
int main(void) {
    int qid; /* 队列ID */
    key_t key; /* 队列键值 */
    int len1, len2;
    struct msg pmsg_w, pmsg_r;

    key = IPC_PRIVATE; /* 使用私有键值 */
    /* 创建队列 */
    if ((qid = msgget(key, IPC_CREAT | 0666)) < 0) {
        perror("msgget: create");
        exit(EXIT_FAILURE);
    }

    puts("Enter message to post:");
    /* 获取消息 */
    if (fgets(pmsg_w.text, BUFSZ, stdin) == NULL) {
        puts("Wrong, no message to post.");
        exit(EXIT_FAILURE);
    }

    pmsg_w.type = 10; /* 设置消息类型 */
    len1 = strlen(pmsg_w.text); /* 计算消息长度 */
    /* 把消息加到队列中 */
    if (msgsnd(qid, &pmsg_w, len1, IPC_NOWAIT) < 0) {
        perror("msgsnd");
        exit(EXIT_FAILURE);
    }
    puts("Message posted.");
    puts("***************");

    /* 从队列中提取并显示消息 */
    len2 = msgrcv(qid, &pmsg_r, BUFSZ, 10, IPC_NOWAIT | MSG_NOERROR);
    if (len2 > 0) {
        pmsg_r.text[len2] = '\0'; /* 确保字符串以空字符结尾 */
        printf("Reading queue id=%05d\n", qid);
        printf("Message type=%05ld\n", pmsg_r.type);
        printf("Message length=%d bytes\n", len2);
        printf("Message text=%s\n", pmsg_r.text);
    } else {
        perror("msgrcv");
        exit(EXIT_FAILURE);
    }

    exit(EXIT_SUCCESS);}

A:这段代码演示了如何使用 System V 消息队列 在 Linux 系统中实现消息的发送和接收。以下是对代码的详细解析和说明:


代码功能概述

  1. 创建一个 私有的消息队列
  2. 从标准输入读取一条消息。
  3. 将消息发送到消息队列中。
  4. 从消息队列中读取该消息并打印其内容。

代码关键点解析

1. 消息队列的基本概念

  • 消息队列 是一种进程间通信(IPC)机制,允许进程以 消息为单位 进行异步通信。
  • 关键系统调用:
    • msgget():创建或获取消息队列。
    • msgsnd():向消息队列中发送消息。
    • msgrcv():从消息队列中接收消息。
    • msgctl():控制消息队列(例如删除队列)。

2. 消息结构

struct msg {
    long type;       // 消息类型(必须是大于 0 的正整数)
    char text[BUFSZ]; // 消息正文
};
  • type 是消息的类型,接收消息时可以指定接收特定类型的消息。
  • text 是实际传递的消息内容。

3. 创建消息队列:msgget()

if ((qid = msgget(key, IPC_CREAT | 0666)) < 0) {
    perror("msgget: create");
    exit(EXIT_FAILURE);
}
  • key = IPC_PRIVATE:表示创建一个私有的消息队列,只能被当前进程和其子进程访问。
  • IPC_CREAT:如果队列不存在,则创建它。
  • 0666:设置消息队列的权限为可读写(所有用户)。

返回值:

  • 成功:返回消息队列的 ID。
  • 失败:返回 -1,通过 perror() 输出错误信息。

4. 发送消息:msgsnd()

if (msgsnd(qid, &pmsg_w, len1, IPC_NOWAIT) < 0) {
    perror("msgsnd");
    exit(EXIT_FAILURE);
}
  • msgsnd() 将消息添加到消息队列中。
  • 参数说明:
    • qid:消息队列 ID。
    • &pmsg_w:要发送的消息。
    • len1:消息正文的长度(不包括 type)。
    • IPC_NOWAIT:非阻塞模式,如果消息队列已满,立即返回错误。
  • 可能的错误:
    • 消息队列已满(EAGAIN)。
    • 其他权限或参数错误。

5. 接收消息:msgrcv()

len2 = msgrcv(qid, &pmsg_r, BUFSZ, 10, IPC_NOWAIT | MSG_NOERROR);
  • msgrcv() 从消息队列中提取消息。
  • 参数说明:
    • qid:消息队列 ID。
    • &pmsg_r:存储接收消息的结构体。
    • BUFSZ:读取消息正文的最大长度。
    • 10:指定接收类型为 10 的消息。
    • IPC_NOWAIT:非阻塞模式,如果队列为空,立即返回错误。
    • MSG_NOERROR:如果消息正文长度超过 BUFSZ,则截断消息,而不会报错。
  • 返回值:
    • 成功:返回接收到的消息正文的长度。
    • 失败:返回 -1,通过 perror() 输出错误信息。

6. 消息队列使用的步骤

  1. 创建消息队列(msgget())。
  2. 构造消息内容(struct msg)。
  3. 使用 msgsnd() 发送消息。
  4. 使用 msgrcv() 接收消息。
  5. 程序退出后,队列会自动释放(本例未调用 msgctl() 删除队列)。

程序运行示例

编译

gcc -o msg_q msg_q.c

运行

./msg_q

输入输出

  1. 程序提示输入一条消息:
    Enter message to post:
    Hello from message queue!
    
  2. 程序会将消息发送到队列并接收,输出如下内容:
    Message posted.
    ***************
    Reading queue id=XXXXX
    Message type=00010
    Message length=25 bytes
    Message text=Hello from message queue!
    

关键点与注意事项

1. IPC_PRIVATE

  • 本例使用 IPC_PRIVATE 作为消息队列的键值。
  • 这会创建一个私有消息队列,只能被当前进程访问,且在程序退出后系统会自动删除。

2. 消息类型

  • type 是消息的类型,接收消息时可以通过指定 type 提取特定类型的消息。
  • 如果 type0,会提取队列中的第一个消息(忽略类型)。

3. 缓冲区管理

  • 消息正文长度必须小于或等于 BUFSZ,否则会被截断(如使用 MSG_NOERROR 标志)。

4. 异常处理

  • 如果 msgsnd()msgrcv() 返回错误,应检查:
    • 消息队列是否已满(发送时)。
    • 消息队列是否为空(接收时)。
    • 权限问题或消息长度问题。

程序改进建议

  1. 删除消息队列
    使用 msgctl() 删除队列,避免可能的资源泄露。

    msgctl(qid, IPC_RMID, NULL);
    
  2. 可重用的键值
    使用 ftok() 创建可重用的键值,而不是 IPC_PRIVATE,例如:

    key = ftok("filename", 65);
    
  3. 多消息支持
    可以扩展程序支持发送和接收多条消息。


总结

该程序清晰演示了如何创建、发送和接收消息队列中的消息,适合作为学习 System V 消息队列 的入门示例。通过改进,可以将其扩展为多进程通信的实际应用场景。

五、Q:linux:编写一个程序,创建一个子进程;父进程向子进程发送一个信号,然后等待子进程终止;子进程接收信号,输出自己的状态信息,最后终止自己。

A:以下是一个完整的 Linux C 程序,它展示了如何创建一个子进程,父进程向子进程发送信号,子进程接收信号后输出状态信息并终止。


程序代码

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

// 子进程的信号处理函数
void signal_handler(int signo) {
    if (signo == SIGUSR1) {
        printf("Child: Received SIGUSR1 signal.\n");
        printf("Child: PID = %d, PPID = %d\n", getpid(), getppid());
        printf("Child: Terminating now.\n");
        exit(0); // 子进程终止
    }
}

int main() {
    pid_t pid;

    // 创建子进程
    pid = fork();
    if (pid < 0) {
        perror("fork failed");
        exit(1);
    }

    if (pid == 0) {
        // 子进程
        printf("Child: Started, waiting for signal...\n");

        // 注册信号处理函数
        signal(SIGUSR1, signal_handler);

        // 无限等待信号
        while (1) {
            pause(); // 等待信号
        }
    } else {
        // 父进程
        printf("Parent: PID = %d, Child PID = %d\n", getpid(), pid);

        sleep(1); // 确保子进程运行并等待信号

        printf("Parent: Sending SIGUSR1 to Child (PID = %d)...\n", pid);
        kill(pid, SIGUSR1); // 向子进程发送 SIGUSR1 信号

        // 等待子进程结束
        int status;
        waitpid(pid, &status, 0);

        if (WIFEXITED(status)) {
            printf("Parent: Child terminated with exit code %d.\n", WEXITSTATUS(status));
        } else {
            printf("Parent: Child terminated abnormally.\n");
        }

        printf("Parent: Exiting.\n");
    }

    return 0;
}

程序解析

1. 信号机制简介

信号是 Linux 系统中用于进程间通信的基本机制之一,可以在进程之间异步传递消息。程序中用到了以下信号相关功能:

  • signal():注册信号处理函数。
  • kill():向指定进程发送信号。
  • pause():使进程挂起,等待信号。

2. 子进程逻辑

  • 子进程启动后,会调用 signal(SIGUSR1, signal_handler) 注册信号处理函数。
  • 子进程使用 pause() 挂起自己,等待接收信号。
  • 收到 SIGUSR1 信号后,信号处理函数被调用,打印子进程的状态信息,然后调用 exit(0) 正常退出。

3. 父进程逻辑

  • 父进程通过 fork() 创建子进程,并获取子进程的 PID。
  • 使用 kill() 向子进程发送 SIGUSR1 信号。
  • 调用 waitpid() 等待子进程退出,并检查其退出状态。

4. 输出说明

运行程序

假设程序名为 signal_demo.c,编译并运行:

gcc -o signal_demo signal_demo.c
./signal_demo
输出示例
Child: Started, waiting for signal...
Parent: PID = 1234, Child PID = 1235
Parent: Sending SIGUSR1 to Child (PID = 1235)...
Child: Received SIGUSR1 signal.
Child: PID = 1235, PPID = 1234
Child: Terminating now.
Parent: Child terminated with exit code 0.
Parent: Exiting.

程序关键点

1. 信号处理函数

子进程使用 signal() 注册了一个信号处理函数 signal_handler,当收到 SIGUSR1 信号时会被调用。

void signal_handler(int signo) {
    if (signo == SIGUSR1) {
        printf("Child: Received SIGUSR1 signal.\n");
        printf("Child: PID = %d, PPID = %d\n", getpid(), getppid());
        printf("Child: Terminating now.\n");
        exit(0);
    }
}

信号处理函数中可以访问信号的标识符(如 SIGUSR1),并执行具体的逻辑。


2. 信号发送

父进程使用 kill(pid, SIGUSR1) 向子进程发送信号。

  • pid 是目标进程的 PID。
  • SIGUSR1 是一个用户自定义信号。

3. 父进程等待子进程

父进程使用 waitpid() 等待子进程结束,并检查退出状态:

int status;
waitpid(pid, &status, 0);
if (WIFEXITED(status)) {
    printf("Parent: Child terminated with exit code %d.\n", WEXITSTATUS(status));
}
  • WIFEXITED(status):判断子进程是否正常退出。
  • WEXITSTATUS(status):获取子进程的退出码。

程序改进建议

  1. 信号可靠性signal() 在部分实现中可能不够可靠,可以用 sigaction() 替代来确保更强的信号处理能力:

    struct sigaction sa;
    sa.sa_handler = signal_handler;
    sigemptyset(&sa.sa_mask);
    sa.sa_flags = 0;
    sigaction(SIGUSR1, &sa, NULL);
    
  2. 处理多个信号:可以扩展程序,让子进程支持接收多个不同的信号。

  3. 添加错误处理:在 fork()kill() 等系统调用后,添加详细的错误处理代码以提高鲁棒性。


总结

  • 该程序通过信号实现父子进程间的简单通信。
  • 父进程负责向子进程发送信号,子进程接收信号并完成自己的逻辑。
  • 程序展示了 fork()kill()waitpid()signal() 的基本用法,非常适合作为信号通信的入门示例。
posted @ 2025-01-09 18:01  Snowcosmos  阅读(23)  评论(0)    收藏  举报