「共享与协作:进程间通信的奥秘」

2023年 9月 21日 83.9k 0

一.进程间通信概述

1.1概念

进程间通信(Inter-Process Communication,简称 IPC)是指不同进程之间进行数据交换和信息传递的机制和技术。在现代操作系统中,同时运行着多个进程,它们可能需要相互协作、共享数据或进行通信来完成特定任务。

进程间通信允许进程在同一台计算机上或不同计算机上进行交流和协作。通过进程间通信,进程可以通过共享数据、消息传递、信号量、管道等方式进行相互沟通和协作。

常见的进程间通信机制包括以下几种:

  • 无名管道(Pipe):管道是一种单向通信机制,可以通过创建一个内核缓冲区来实现父子进程间的通信。
  • 有名管道(Named Pipe):类似于管道,但允许无关的进程进行通信,且可以通过文件系统进行通信。
  • 信号(Semaphore):信号用于控制对共享资源的并发访问,在多个进程之间进行同步。
  • 共享内存(Shared Memory):共享内存是一种将一块内存区域映射到多个进程的地址空间,从而实现进程间的数据共享。
  • 消息队列(Message Queue):消息队列是一种通过消息传递方式进行进程间通信的机制,允许多个进程发送和接收消息。
  • 信号灯集(Signal):信号是一种用于进程间通知和处理异步事件的机制,一个进程可以发送信号给另一个进程来触发相应的处理动作。
  • 套接字(Socket):套接字可以用于在网络上不同计算机的进程之间进行通信,实现分布式进程间通信。
  • 选择适当的进程间通信机制取决于应用程序的需求和环境。不同的机制具有不同的特性、效率和复杂性。正确使用进程间通信可以实现并发处理、资源共享和协作,从而提高应用程序的效率和可靠性。

    1.2功能

    进程之间进行通信的主要原因有以下几个:

  • 合作和协同工作:在复杂的应用程序中,不同的进程可能需要相互协作和共同完成特定任务。通过进程间通信,进程可以共享信息、传递消息、协调操作等,从而实现合作和协同工作。

  • 数据共享:多个进程可能需要共享数据,以实现信息共享和统一的数据视图。进程间通信可以使得多个进程能够访问和修改相同的数据,从而实现数据共享。

  • 任务分解和并行处理:将一个大型任务分解为多个子任务,并通过多个进程并行处理,可以提高系统的处理能力和效率。进程间通信可以用于分发任务、传递结果等,实现任务分解和并行处理。

  • 资源共享与管理:不同进程之间可能需要共享系统资源,如共享内存、文件、设备等。进程间通信可以协调资源的分配和管理,确保资源的正确使用和避免冲突。

  • 事件通知和处理:进程间通信可以用于事件的通知和处理。一个进程可以向另一个进程发送消息或信号,通知某个事件的发生,从而触发相应的处理动作。

  • 通过进程间通信,不同进程之间可以实现信息交换、资源共享、任务协调和事件处理,从而提高系统的效率、可靠性、并发性和扩展性。进程间通信是构建复杂应用程序和多进程系统的重要基础。

    二.无名管道

    2.1特点

    无名管道(Unnamed Pipe)是一种最简单的进程间通信机制,用于在具有父子关系的进程之间进行单向通信。无名管道是一个字节流,用于在一个进程中将输出连接到另一个进程的输入。以下是无名管道的一些详细介绍:

  • 单向通信:无名管道是单向的,只能从管道的一端读取数据,从另一端写入数据。一般来说,管道具有读取端(读取管道的数据)和写入端(向管道写入数据)。

  • 父子进程间通信:无名管道通常用于具有父子关系的进程间通信。当一个进程创建一个子进程时,子进程会继承父进程的文件描述符(包括管道),从而可以使用管道进行通信。

  • 匿名性:无名管道不需要特定的名称或标识符来标识管道,所以被称为“无名”。因此,无名管道只能在具有共同祖先的进程之间使用。

  • FIFO(先进先出)效果:无名管道保持了一个基本的先进先出的数据传输顺序。在写入管道时,先写入的数据先被读取。

  • 管道大小限制:管道具有固定的缓冲区大小,通常比较小。如果管道的写入端向管道写入数据超过缓冲区大小,写入操作会被阻塞,直到有足够的空间来写入数据。

  • 关闭和资源管理:在使用完无名管道后,应当适时地关闭管道。关闭管道后,无法再使用它进行通信。

  • 无名管道提供了一种简单而有效的进程间通信机制。然而,由于只支持单向通信且限制比较多,无名管道适用于特定的通信需求,如父子进程之间的简单数据交换。当需要双向通信、多个进程间通信或非相关进程间通信时,应该选择其他更复杂但适用的进程间通信机制。

    2.2注意事项

    注意事项:

    1.当管道中无数据时,读操作会阻塞;管道中无数据,将写端关闭,读操作会立即返回
    2.管道中装满(管道大小64K)数据写阻塞,一旦
    3.有4k空间,写继续,直到写满为止
    3.只有在管道的读端存在时,向管道中写入数据才有意义。否则,会导致管道破裂,向管道中写入数据的进程将收到内核传来的SIGPIPE信号 (通常Broken pipe错误)。

    2.3编程实战

    #include 
    #include 
    #include 
    
    int main() {
        int pipefd[2]; // 用于存储无名管道的文件描述符
    
        if (pipe(pipefd) == -1) {
            perror("无名管道创建失败");
            exit(EXIT_FAILURE);
        }
    
        pid_t pid = fork();
    
        if (pid > 0) {
            // 父进程
            close(pipefd[0]); // 关闭读取端
    
            char message[] = "Hello, 子进程!";
            write(pipefd[1], message, sizeof(message)); // 写入数据到管道
    
            close(pipefd[1]); // 关闭写入端
        } else if (pid == 0) {
            // 子进程
            close(pipefd[1]); // 关闭写入端
    
            char buffer[512];
            ssize_t numRead = read(pipefd[0], buffer, sizeof(buffer)); // 从管道读取数据
    
            printf("子进程接收到的数据:%.*s\n", (int)numRead, buffer);
    
            close(pipefd[0]); // 关闭读取端
        } else {
            perror("进程创建失败");
            exit(EXIT_FAILURE);
        }
    
        return 0;
    }
    

    这段代码使用 中的 pipe() 函数创建了一个无名管道。接着,使用 fork() 函数创建了一个子进程。
    在父进程中,关闭了管道的读取端,然后使用 write() 函数向管道写入数据。最后,关闭了管道的写入端。

    在子进程中,关闭了管道的写入端,然后使用 read() 函数从管道中读取数据到 buffer 缓冲区,并打印出来。最后,关闭了管道的读取端。

    注意,在实际应用中,你可能需要添加错误检查和更完善的逻辑来处理数据的读取和写入。

    三.有名管道

    3.1有名管道特点

    有名管道(Named Pipes)是一种在文件系统中有名字的特殊文件,用于进程间通信。以下是有名管道的几个特点:

  • 有名:与无名管道不同,有名管道在文件系统中具有唯一的路径名,可以通过路径名进行访问和引用。

  • 持久性:有名管道是持久的,与进程无关。即使创建它的进程退出,有名管道仍然存在于文件系统中,可以由其他进程打开和使用。

  • 双向通信:有名管道可以支持双向通信,其中一个进程可以从管道中读取数据,而另一个进程可以向管道中写入数据。

  • 阻塞和非阻塞:默认情况下,对有名管道的读取和写入操作是阻塞的,即如果没有数据可读或管道已满,进程将被阻塞。但是,可以通过设置文件描述符为非阻塞模式来实现非阻塞的读写操作。

  • 管道容量有限:有名管道的容量是有限的,取决于系统的设置。如果管道已满,进程写入操作可能会被阻塞。

  • 进程间独立:有名管道不会自动提供同步和互斥机制。进程需要自己协调读写操作,以避免数据竞争和同步问题。

  • 由于有名管道在文件系统中具有唯一路径名,使得它们非常适合需要持久性进程间通信的应用程序,例如客户端和服务器应用程序之间的通信。

    补充

    1.有名管道可以使互不相关的两个进程互相通信。
    2.有名管道可以通过路径名来指出,并且在文件系统中可见,但内容存放在内存中。
    3.进程通过文件IO来操作有名管道
    4.有名管道遵循先进先出规则,不支持lseek()操作
    5.半双工通信

    3.2有名管道注意事项

    在使用有名管道(Named Pipes)进行进程间通信时,以下是一些需要注意的事项:

  • 创建和删除:在使用有名管道之前,必须通过调用 mkfifo() 函数来创建管道。使用 rm 命令或 unlink() 函数来删除有名管道。确保正确处理有名管道的创建和删除,以避免潜在的资源占用和权限问题。

  • 文件权限:有名管道在文件系统中以特殊文件的形式存在,因此需要正确设置文件权限以控制进程对管道的访问。确保正确地为有名管道设置合适的权限,以保护管道的安全和保密性。

  • 同步和阻塞:有名管道的读取和写入操作默认是阻塞的,即进程在读取或写入时会被阻塞,直到操作完成。因此,需要注意防止进程陷入死锁和无限阻塞情况。可以使用非阻塞模式设置文件描述符,或使用多线程或多进程机制来处理并发读写操作。

  • 缓冲区大小:有名管道的容量是有限的,取决于系统的设置。如果管道已满,写入操作可能被阻塞。因此,在实现进程间通信时,需要适当设置和管理缓冲区大小,以避免数据丢失和性能问题。

  • 错误处理:使用有名管道时,需要适当处理错误和异常情况,并进行错误检查。例如,在打开管道文件、读写数据和处理管道关闭等情况下,需要检查系统调用或库函数的返回值,以确保操作正确完成。

  • 进程间同步:有名管道本身不提供进程间同步和互斥机制。如果多个进程同时读取和写入管道,可能会导致数据竞争和不确定行为。因此,需要使用其他同步机制(如信号量、互斥锁等)来确保进程之间的正确协调和数据一致性。

  • 关闭管道:及时关闭有名管道文件描述符,以及在不使用管道时适当地删除管道。这样可以释放系统资源,并避免潜在的问题和错误。

  • 总之,在使用有名管道进行进程间通信时,需要考虑文件权限、同步和阻塞、缓冲区大小、错误处理等方面的因素,以确保通信的正确性、可靠性和安全性。

    补充

    1.以只写方式打开有名管道,写阻塞(open),直到另一个进程将读打开
    2.以只读方式打开有名管道,读阻塞(open),直到另一个进程将写打开
    3.以可读可写方式打开有名管道,管道中无数据,读阻塞,当管道中写满数据,写阻塞

    3.3mkfifo函数

    mkfifo() 是一个系统调用函数,用于创建有名管道(Named Pipes)。以下是该函数的原型和用法:

    #include 
    
    int mkfifo(const char *pathname, mode_t mode);
    

    参数说明:

    • pathname:要创建的有名管道的路径名。
    • mode:创建的有名管道的权限(权限掩码)。使用 chmod 命令中的权限表示法,如 0666

    返回值:

    • 成功时,返回0。
    • 失败时,返回-1,并设置 errno 来指示具体错误。

    示例用法:

    #include 
    #include 
    #include 
    
    int main() {
        const char *pathname = "/tmp/my_fifo";
    
        int result = mkfifo(pathname, 0666);
        if (result == 0) {
            printf("有名管道创建成功\n");
        } else {
            perror("有名管道创建失败");
            exit(EXIT_FAILURE);
        }
    
        return 0;
    }
    

    在上述示例中,mkfifo() 函数将创建一个 /tmp/my_fifo 的有名管道。如果创建成功,将打印一条成功消息;否则,将打印错误信息。

    请确保确保创建的有名管道路径名的有效性和权限设置的正确性,并根据需要进行错误检查处理。

    3.4编程实战

    好的,以下是一个使用有名管道进行进程间通信的简单例子:

    发送消息的进程(writer.c):

    #include 
    #include 
    #include 
    #include 
    #include 
    
    int main() {
        const char *fifoPath = "/tmp/myfifo";
        int fd;
    
        // 创建有名管道
        mkfifo(fifoPath, 0666);
    
        // 打开管道以进行写操作
        fd = open(fifoPath, O_WRONLY);
    
        // 发送消息到管道
        const char *message = "Hello, reader!";
        write(fd, message, strlen(message) + 1);
        close(fd);
    
        return 0;
    }
    

    接收消息的进程(reader.c):

    #include 
    #include 
    #include 
    #include 
    #include 
    
    int main() {
        const char *fifoPath = "/tmp/myfifo";
        int fd;
        char buffer[256];
    
        // 打开管道以进行读操作
        fd = open(fifoPath, O_RDONLY);
    
        // 从管道读取消息
        read(fd, buffer, sizeof(buffer));
        printf("接收到的消息: %s\n", buffer);
        close(fd);
    
        return 0;
    }
    

    在上述例子中,发送消息的进程使用 mkfifo() 函数创建了一个有名管道,并使用 open() 函数以只写模式打开了管道。然后,使用 write() 函数将消息写入管道中。

    接收消息的进程同样使用 open() 函数以只读模式打开了相同的管道,并使用 read() 函数从管道中读取消息。然后,打印出接收到的消息。

    请确保在运行这两个进程之前,在命令行中先编译它们:

    gcc writer.c -o writer
    gcc reader.c -o reader
    

    然后,可以分别运行 ./writer./reader 来观察进程间通过有名管道进行的简单消息通信。

    四.信号

    4.1信号特点

    信号(Signal)在操作系统中是一种进程间通信机制,用于向进程发送异步通知。以下是信号的几个特点:

  • 异步通知:信号是异步发送给进程的,即进程在接收到信号时会中断当前的执行流程,转而去处理信号的处理函数。这个特点使得信号非常适合处理一些突发事件或异步事件。

  • 中断处理:当进程接收到信号时,会立即中断当前的执行,并跳转到预先设置好的信号处理函数(Signal Handler)去执行特定的操作。可以使用系统提供的函数(如 signal()sigaction())来注册信号处理函数。

  • 非透明性:信号的传达是通过内核来实现的,对进程本身来说是不可见的。进程无法得知信号是由谁发送的,也无法直接向特定进程发送信号,而只能向整个进程组或进程所属的进程组发送信号。

  • 有限数量:每个操作系统都规定了一定的信号数量,不同的信号具有不同的含义和用途。例如,SIGINT 用于终止进程,SIGSEGV 用于表示段错误等。可以使用 kill 命令或相关函数来发送信号。

  • 默认处理行为:每个信号都有一个默认的处理行为,例如终止进程、忽略信号或产生核心转储(Core Dump)。然而,可以使用信号处理函数来自定义处理行为,从而实现对信号的控制。

  • 无法排队:如果同一种信号在进程尚未处理完毕时再次发送,通常情况下,只有一个信号会被接收到。这是因为信号无法排队,进程只会接收到一次同一类型的信号。

  • 可靠性限制:在信号的传递过程中,可能会存在信号丢失或信号合并等问题。一些信号在多次发送时可能只会被接收到一次。这使得信号在某些场景下的可靠性受到一定的限制。

  • 需要注意,不同的操作系统可能支持不同的信号,并且有不同的信号编号和默认处理行为。因此,在使用信号时应仔细了解操作系统的信号机制和规范。

    总的来说,信号提供了一种简单而有效的进程间通信机制,可以用于处理异步事件和相应外部通知。但是,使用信号时需要注意处理函数的可重入性、信号处理函数的执行上下文切换以及信号的可靠性等问题。

    4.2信号注意事项

    在处理信号时,以下是一些需要注意的事项:

  • 可重入性:信号处理函数在接收到信号时会被异步调用,因此需要保证信号处理函数的可重入性。避免在信号处理函数中使用不可重入的函数,以及全局变量的不安全访问等操作。推荐使用线程安全的函数和数据结构。

  • 非阻塞原则:信号处理函数应该尽可能地短小和简单,避免执行耗时的操作,以减少信号堆积和处理延迟。长时间的信号处理函数可能会导致其他信号被阻塞或丢失。

  • 信号屏蔽:在需要临时关闭某个信号的处理时,可以使用信号屏蔽操作。通过调用 sigprocmask() 函数可临时阻塞或解除阻塞某个信号,确保在特定的代码区域内不会收到特定的信号。

  • 可靠信号处理:为了保证信号的可靠性,可以使用 sigaction() 函数来注册信号处理函数。sigaction() 允许对信号的处理方式进行更加精确的控制,并提供了一些可靠性增强的选项,可以避免信号的丢失和合并。

  • 重新注册处理函数:某些信号在接收到后会将默认处理行为重置回去,例如终止进程的信号。如果要持续地处理这些信号,需要在信号处理函数中重新注册处理函数,以确保持续接收和处理该信号。

  • 对可中断的系统调用的处理:在一些系统调用(如 read()write()sleep() 等)中,当进程接收到信号时,这些系统调用可能会被中断,返回错误(例如 EINTR)。需要在合适的地方对这些可中断的系统调用进行重试。

  • 与其他异步机制的交互:注意信号处理函数与其他异步事件机制(如线程、定时器、异步IO等)之间的交互。需要合理地进行同步和互斥操作,以避免资源竞争和不确定行为。

  • 信号的处理次数:注意同一种信号在进程中的处理次数限制。有些信号在多次触发时可能只能被处理一次,这可能导致其中某些信号被丢弃。了解并考虑系统对信号处理次数的限制。

  • 不可移植性:不同的操作系统可能具有不同的信号机制和限制。因此,在处理信号时要注意确保代码的可移植性,并遵循操作系统的信号规范。

  • 以上是一些在处理信号时需要注意的事项。合理处理信号可以帮助应用程序与操作系统之间进行有效的通信和响应外部事件。然而,信号处理也是一个复杂的主题,需要仔细考虑各种因素和风险。

    4.3信号响应方式

    在处理信号时,有三种主要的信号响应方式:

  • 默认响应(Default Action):每个信号都有一个默认的处理行为。例如,SIGINT 的默认行为是终止进程,SIGTERM 的默认行为是终止进程,SIGKILL 的默认行为是无法被阻塞和处理的直接终止进程等。对于大多数信号,默认行为是终止或终止并生成核心转储。可以使用 man 命令来查看特定信号的默认行为。

  • 忽略信号(Ignore Signal):可以通过设置信号处理函数为 SIG_IGN 来忽略某个特定的信号。忽略信号意味着对该信号的接收和处理完全忽略,不会触发任何操作。例如,可以忽略 SIGINT 信号以防止进程被键盘中断(Ctrl+C)终止。

  • 自定义信号处理函数(Signal Handling Function):可以为一个信号指定一个自定义的信号处理函数(Signal Handler)。信号处理函数是一个用户定义的函数,用于处理接收到的信号。可以使用 signal()sigaction() 函数来注册信号处理函数。在接收到信号时,进程会中断当前执行,并跳转到相应的信号处理函数进行操作。在处理函数中可以执行一些自定义的操作,如记录日志、改变标志位、发送信号给其他进程等。

  • 要选择适当的信号响应方式,需要根据实际需求和特定的业务逻辑来决定。例如,对于某个关键的信号,可以选择自定义信号处理函数来执行特定任务,而对于其他一些信号可以选择忽略或采用默认行为。

    需要注意的是,使用自定义信号处理函数时应保持函数的简洁和高效,避免耗时和不可重入的操作,以及需要合理处理信号处理函数与其他并发机制(如线程)之间的同步和互斥。

    另外,还应注意某些无法被阻塞或处理的信号,如 SIGKILLSIGSTOP,不能被忽略或自定义信号处理函数。这些信号的默认行为是直接终止进程或暂停进程,无法被拦截或处理。

    4.4怎么产生信号

    在操作系统中,有几种常见的方法可以产生信号:

  • 来自键盘或终端:键盘上的某些组合键,如Ctrl+C(SIGINT)、Ctrl+Z(SIGTSTP)等,可以通过终端或控制台产生相应的信号。例如,使用Ctrl+C可以发送SIGINT信号,这通常用于终止当前的运行进程。

  • 使用 kill 命令:可以使用kill命令(或其它类似的命令)向指定的进程发送信号。kill命令的语法是 kill [-] 。其中,信号可以是信号名称(如INT、TERM、HUP等),也可以是信号编号(如2、9、15等),进程ID是要发送信号的目标进程的ID。

  • 硬件异常:当出现硬件异常(如除零错误,非法指令访问等)时,操作系统会向产生异常的进程发送相应的信号。这些信号通常用于指示进程发生了错误或异常情况,并让进程或操作系统采取适当的措施。

  • 软件异常:有些特殊的函数调用,如abort()、raise()等,可以在程序中主动产生信号。例如,使用raise()函数可以向当前进程发送指定的信号。

  • 定时器:操作系统提供了一些定时器功能,可以用来在特定时间间隔内发送指定信号。最常见的是使用timer_create()和timer_settime()函数来创建和设置定时器,并使用SIGALRM信号来触发定时器的信号。

  • 需要注意的是,生成信号的能力通常受到用户的权限和操作系统对信号的限制。具体可生成的信号类型和权限可能因操作系统和用户的不同而有所不同。此外,特定信号的具体处理方式也由接收进程的信号处理机制决定。

    4.5常用信号

    以下是一些常见的信号及其默认编号及含义:

  • SIGINT (2):键盘中断信号。通常由用户按下Ctrl+C键产生,用于终止正在运行的进程。

  • SIGTERM (15):终止信号。用于请求进程正常终止,给进程一个机会进行清理和保存状态。

  • SIGKILL (9):强制终止信号。无法被阻塞、处理或忽略,用于强制终止进程。

  • SIGSTOP (19):暂停信号。用于暂停进程的执行,进程将被挂起直到收到继续执行的信号。

  • SIGCONT (18):继续信号。用于恢复之前暂停的进程的执行。

  • SIGHUP (1):终端挂起或控制进程断开的信号。用于指示终端会话已经断开,常用于在进程重启时重新加载配置文件。

  • SIGSEGV (11):段错误信号。通常表示进程访问了无效的内存地址。

  • SIGILL (4):非法指令信号。表示进程执行了非法、未定义或不协调的指令。

  • SIGFPE (8):浮点异常信号。表示进程执行了一个浮点运算异常,如除零错误或溢出。

  • SIGUSR1 (10) 和 SIGUSR2 (12):用户自定义信号。可以由应用程序自定义使用。

  • SIGPIPE (13):管道破裂信号。当进程写入已关闭的管道时会产生。

  • SIGALRM (14):定时器信号。通常由操作系统的定时器触发,可用于定时操作或超时处理。

  • 这些信号仅是一部分常用的信号,不同的操作系统和环境中可能还有其他系统特定的信号。可以使用命令 kill -l 查看系统支持的所有信号及其编号。另外,还可以通过自定义信号处理函数来处理这些信号,以满足特定应用程序的需求。

    4.6函数接口

    在 C 语言中,有一些常用的函数接口可用于处理信号。以下是一些常见的信号相关函数接口:

  • signal() 函数:
    void (*signal(int signum, void (*handler)(int)))(int)
    该函数用于注册信号处理函数。它接受两个参数:signum 表示要处理的信号编号,handler 为信号处理函数的指针。signal() 函数会返回先前的信号处理函数指针。通过传递 SIG_IGN(忽略信号)或 SIG_DFL(默认处理)作为 handler,可以忽略或使用默认行为处理信号。需要注意的是,signal() 函数在不同的操作系统中可能会有不同的行为和使用限制。

  • sigaction() 函数:
    int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact)
    该函数提供了更为高级和可靠的信号处理方式,相较于signal()函数,具有更多的选项和功能。它接受三个参数:signum 表示要处理的信号编号,act 为新的信号处理动作结构体指针,oldact 可用于保存旧的信号处理动作设置。通过设置 act 结构体的成员可以注册自定义的信号处理函数,以及指定一些信号处理的标志和选项。

  • kill() 函数:
    int kill(pid_t pid, int signum)
    该函数用于向指定进程发送信号。它接受两个参数:pid 为目标进程的 ID,signum 表示要发送的信号编号。通过向进程发送不同的信号可以触发相应的处理动作。如果 pid 为正值且具有合法进程 ID,则向该进程发送信号;如果 pid 为0,则信号将发送到与调用进程属于同一进程组的所有进程;如果 pid 为-1,则信号将发送给与调用进程具有相同用户 ID 的所有进程。

  • sigprocmask() 函数:
    int sigprocmask(int how, const sigset_t *set, sigset_t *oldset)
    该函数用于设置进程的信号屏蔽字,即阻塞或解除阻塞指定的信号集合。how 参数表示对信号屏蔽字的修改方式,可以是 SIG_BLOCK、SIG_UNBLOCK 或 SIG_SETMASK。通过指定 set 参数来设置要更改的信号集合。oldset 参数用于保存旧的信号屏蔽字。

  • sigpending() 函数:
    int sigpending(sigset_t *set)
    该函数用于获取进程当前挂起(未决)的信号。如果有挂起的信号,则将其保存到 set 指向的信号集中。可以通过该函数了解当前未被处理的信号。

  • 除了以上提到的函数,还有一些其他的信号相关函数接口,在不同的操作系统和平台上可能存在差异。因此,在编写处理信号的代码时,应该查阅相关的文档和手册,确保在目标平台上正确使用合适的函数接口。

    4.7信号屏蔽函数

    信号集屏蔽函数用于设置和操作信号集的屏蔽状态,以控制哪些信号会被阻塞。在 C 语言中,常用的信号集屏蔽函数有以下几个:

  • sigemptyset():
    int sigemptyset(sigset_t *set)
    该函数用于清空信号集,将所有信号都从集合中移除。传递一个信号集指针给 set 参数,函数将设置该指针所指向的信号集为空集。

  • sigfillset():
    int sigfillset(sigset_t *set)
    该函数用于将所有信号都添加到信号集中。传递一个信号集指针给 set 参数,函数将设置该指针所指向的信号集为包含所有信号的集合。

  • sigaddset():
    int sigaddset(sigset_t *set, int signum)
    该函数用于将特定信号添加到信号集中。传递一个信号集指针给 set 参数,signum 参数表示要添加的信号编号。函数将该信号添加到信号集中。

  • sigdelset():
    int sigdelset(sigset_t *set, int signum)
    该函数用于从信号集中删除指定的信号。传递一个信号集指针给 set 参数,signum 参数表示要删除的信号编号。函数将从信号集中删除指定的信号。

  • sigismember():
    int sigismember(const sigset_t *set, int signum)
    该函数用于检查给定信号是否在信号集中。传递一个信号集指针给 set 参数,signum 参数表示要检查的信号编号。如果指定的信号在信号集中,则返回非零值;否则返回 0。

  • sigprocmask():
    sigprocmask 是用于设置或修改进程的信号屏蔽字(signal mask)的函数。它可以阻塞或解除阻塞指定的信号集。

  • 函数原型如下:

    int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
    

    how 参数指定了对信号屏蔽字的修改方式,可以取以下三个值:

    • SIG_BLOCK: 将 set 中的信号添加到当前信号屏蔽字中。
    • SIG_UNBLOCK: 将 set 中的信号从当前信号屏蔽字中解除阻塞。
    • SIG_SETMASK: 将当前信号屏蔽字替换为 set

    set 参数是一个指向信号集的指针,表示要添加或解除阻塞的信号集。

    oldset 参数(可选)是一个用来保存之前信号屏蔽字的信号集指针。如果不为 NULL,则 oldset 指向的信号集会被填充为函数调用之前的信号屏蔽字。

    调用 sigprocmask 函数后,进程的信号屏蔽字会根据 how 参数对 set 中指定的信号进行修改。

    示例用法:

    #include 
    #include 
    
    int main() {
        sigset_t newmask, oldmask;
        
        // 设置屏蔽信号集包含 SIGINT 和 SIGQUIT
        sigemptyset(&newmask);
        sigaddset(&newmask, SIGINT);
        sigaddset(&newmask, SIGQUIT);
        
        // 阻塞 newmask 中的信号
        sigprocmask(SIG_BLOCK, &newmask, &oldmask);
        
        // 此处可以进行一些需要屏蔽信号的操作
        
        // 恢复原来的信号屏蔽字
        sigprocmask(SIG_SETMASK, &oldmask, NULL);
        
        return 0;
    }
    

    以上示例中,通过 sigprocmask 函数将 SIGINTSIGQUIT 信号添加到当前进程的信号屏蔽字中,实现对这两个信号的阻塞。在执行需要阻塞信号的操作后,通过再次调用 sigprocmask 恢复原来的信号屏蔽字,解除对这两个信号的阻塞。

    需要注意的是,sigprocmask 函数仅对当前进程的信号屏蔽字进行修改,并不影响其他进程的信号处理。

    这些函数接口都是针对 头文件中定义的 sigset_t 类型的信号集进行操作。它们一般用于配合其他信号处理函数或系统调用(如 sigprocmask())来设置或查询进程的信号屏蔽状态。需要注意的是,这些函数在不同的操作系统和平台上可能会有细微的差异,因此在使用时应查阅相关的文档和官方手册来了解具体的使用方式和兼容性。

    4.8未处理信号集

    实时信号和非实时信号在被屏蔽时的行为有所不同。

  • 非实时信号(标准信号):
    当一个非实时信号(编号为1~31)被屏蔽时,如果有多个该信号同时到达,只会为其保留一个未决(待处理)的信号。也就是说,非实时信号在被屏蔽时不会排队,只会保留一个待处理的信号。此时,如果该信号被解除屏蔽,只会处理一次。

  • 实时信号(实时时钟信号):
    实时信号(编号为34~64)在被屏蔽时会排队保留,它们不会被合并或丢弃。如果多个相同的实时信号同时到达,并且被屏蔽,这些信号将按照到达的顺序排队,依次保留在未决(待处理)的信号集中。当该信号被解除屏蔽时,在信号处理函数中会按照队列顺序处理这些信号,一个接一个地处理每个实时信号。

  • 需要注意的是,如果实时信号的队列溢出,而处理函数尚未处理完所有排队的实时信号时,新到达的实时信号将被丢弃。因此,在实时信号的处理函数中,应当尽早处理队列中的实时信号,以避免队列溢出。

    总结起来,如果非实时信号被屏蔽,只会保留一个未处理的实例。而实时信号在被屏蔽时会排队保留,并在解除屏蔽后按照到达的顺序进行处理。

    五.消息队列

    5.1消息队列特点

    Linux 中的消息队列具有以下特点:

  • 基于内核:Linux 的消息队列是由内核提供的。它由内核维护和管理,可以在用户空间中进行操作和访问。

  • 进程间通信:Linux 的消息队列是一种用于进程间通信的机制。它允许不同的进程之间通过发送和接收消息来实现数据的传递和共享。

  • 高效性:Linux 的消息队列在内核中实现,因此具有较高的性能和效率。它是一种基于内存的通信方式,消息的传递速度较快。

  • 异步通信:Linux 的消息队列支持异步通信。发送方可以将消息发送到消息队列中后立即返回,而不需要等待接收方的响应。

  • 支持多种消息类型:Linux 的消息队列可以支持不同类型的消息。每个消息由消息类型和消息数据组成,接收方可以根据消息类型来进行区分和处理。

  • 消息持久化:Linux 的消息队列可以通过设置相应的标志来实现消息的持久化。这意味着即使接收方在消息到达之前不可用,消息仍然会被保存在队列中,直到接收方准备好处理它。

  • 需要注意的是,Linux 的消息队列仅仅是 IPC(Inter-Process Communication,进程间通信)机制之一,还有其他的方式,如管道、共享内存等。选择合适的通信机制需要根据具体的需求和应用场景进行评估。

    补充

    独立于进程
    没有文件名和文件描述符
    IPC对象具有key和ID

    5.2编程步骤

    使用消息队列进行编程通常包含以下步骤:

  • 创建消息队列:首先,需要创建一个消息队列来存储和传递消息。具体的创建方式和语法取决于所用的编程语言和消息队列实现库。

  • 发送消息:在发送消息之前,需要确定消息的格式和内容。然后,将消息发送到消息队列中,等待接收方来获取并处理该消息。

  • 接收消息:接收方需要在消息队列中注册并等待接收消息。一旦有消息到达队列,接收方就可以从队列中获取消息并进行相应的处理。

  • 处理消息:接收方获取到消息后,进行相应的处理逻辑。根据消息的内容和类型,可以进行业务逻辑的处理、数据操作等操作。

  • 确认消息:在一些消息队列系统中,接收方需要确认已经成功接收和处理了消息。这样可以确保消息在发送和处理过程中的可靠性传递,避免丢失或重复处理。

  • 销毁消息队列:当不再需要使用消息队列时,需要进行手动销毁或关闭消息队列,释放相关的资源。

  • 需要注意的是,具体的编程步骤可能会因不同的编程语言、消息队列实现库和应用场景而有所差异。此外,还需了解所用消息队列的具体特性和相关的 API 文档,以便正确地使用和操作消息队列。

    5.3函数接口

    1.ftok()

    ftok 函数是 POSIX 标准中定义的一个函数,用于将给定的路径名和项目标识符生成一个唯一的键值,通常用于创建和关联 System V 消息队列、共享内存和信号量等 IPC 机制。

    ftok 函数原型如下:

    key_t ftok(const char *pathname, int proj_id);
    
    • pathname:一个存在的文件的路径名。通常使用一个特定的文件作为路径名,以表明某个特定 IPC 对象和项目相关联。
    • proj_id:一个非零的整数,用于区分不同的项目。需要保证在同一个 pathname 下,不同的 proj_id 生成的键值是唯一的。

    ftok 函数返回一个 key_t 类型的键值。该键值在后续的 msggetshmgetsemget 等函数中用于标识和关联IPC对象。

    需要注意的是,ftok 函数的键值生成是基于文件的 i-node 号和 proj_id 进行计算的。因此,当使用 ftok 生成键值时,需要确保 pathname 所指向的文件是存在的且不会轻易改变,否则可能导致生成的键值发生变化。

    以下是一个示例使用 ftok 函数的代码片段:

    #include 
    #include 
    
    int main() {
        // 生成一个键值
        key_t key = ftok("/tmp/myfile", 'A');
        if (key == -1) {
            perror("ftok");
            return 1;
        }
    
        // 使用生成的键值进行其他操作,比如创建消息队列、共享内存、信号量等
    
        return 0;
    }
    

    在这个示例中,我们将路径名设为 "/tmp/myfile",proj_id 设置为 'A',并使用 ftok 生成一个键值,然后可以将这个键值传递给其他 IPC 相关的函数使用。

    总之,ftok 函数用于将路径名和项目标识符转换为一个唯一的键值,用于标识和关联 System V 消息队列、共享内存和信号量等 IPC 机制。

    2.msgget()

    msgget 函数用于创建消息队列或获取已存在的消息队列的标识符。

    函数原型如下:

    #include 
    #include 
    #include 
    
    int msgget(key_t key, int msgflg);
    

    参数:

    • key:一个键值,通常使用 ftok 函数生成。该键值用于唯一标识一个消息队列。
    • msgflg:用于指定消息队列的权限和选项,可以通过按位或操作符 | 来设置多个选项。常用的选项包括:
      • IPC_CREAT:如果没有与给定 key 相关联的消息队列,则创建一个新的消息队列。
      • IPC_EXCL:与 IPC_CREAT 一起使用时,如果与给定 key 相关联的消息队列已经存在,则报错。

    函数返回值:

    • 成功时,返回消息队列的标识符(非负整数)。
    • 失败时,返回 -1,并设置全局变量 errno 来指示错误原因。

    以下是一个示例使用 msgget 的代码片段:

    #include 
    #include 
    #include 
    #include 
    #include 
    
    int main() {
        key_t key = ftok("/tmp/myfile", 'A');  // 生成键值
        if (key == -1) {
            perror("ftok");
            return 1;
        }
    
        int msgflg = IPC_CREAT | 0666;  // 创建消息队列并设置权限
    
        // 创建或获取消息队列
        int msqid = msgget(key, msgflg);
        if (msqid == -1) {
            perror("msgget");
            return 1;
        }
    
        printf("成功创建或获取消息队列,标识符为: %d\n", msqid);
    
        return 0;
    }
    

    在这个示例中,我们调用 msgget 函数创建或获取一个消息队列,首先使用 ftok 生成键值,然后设置创建选项为 IPC_CREAT | 0666 来创建一个新的消息队列并设置权限。如果消息队列创建成功,msgget 函数将返回一个非负整数作为消息队列的标识符 msqid。如果创建失败,调用 perror 函数打印错误信息,并返回非零值。

    需要注意的是,如果一个进程已经创建了一个与给定键值相关联的消息队列,其他进程可以通过相同的键值来获取该消息队列的标识符,从而实现进程间的通信。

    总之,msgget 函数用于创建或获取消息队列标识符,用于进程间的消息通信。

    3.msgsnd()

    msgsnd 函数用于向消息队列发送消息。

    函数原型如下:

    #include 
    #include 
    #include 
    
    int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);
    

    参数:

    • msqid:消息队列的标识符,由 msgget 函数返回。
    • msgp:指向要发送的消息的指针,通常是一个结构体指针,结构体定义应与消息队列中消息的结构匹配。
    • msgsz:要发送的消息的大小(以字节为单位)。
    • msgflg:发送消息的选项,可以通过按位或操作符 | 来设置多个选项,常用选项有:
      • IPC_NOWAIT:若消息队列已满,即时返回错误,而不是等待队列有空闲空间。
      • MSG_NOERROR:如果消息长度超过消息队列的最大字节数,则截断消息而不报错。

    函数返回值:

    • 成功时,返回 0。
    • 失败时,返回 -1,并设置全局变量 errno 来指示错误原因。

    以下是一个示例使用 msgsnd 的代码片段:

    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    
    struct message {
        long mtype;
        char mtext[100];
    };
    
    int main() {
        key_t key = ftok("/tmp/myfile", 'A');  // 生成键值
        if (key == -1) {
            perror("ftok");
            return 1;
        }
    
        int msgflg = IPC_CREAT | 0666;  // 创建消息队列并设置权限
    
        // 创建或获取消息队列
        int msqid = msgget(key, msgflg);
        if (msqid == -1) {
            perror("msgget");
            return 1;
        }
    
        // 准备要发送的消息
        struct message msg;
        msg.mtype = 1;  // 消息类型,可以根据需要自定义
        strncpy(msg.mtext, "Hello, message queue!", sizeof(msg.mtext));
    
        // 发送消息
        int result = msgsnd(msqid, &msg, sizeof(msg.mtext), 0);
        if (result == -1) {
            perror("msgsnd");
            return 1;
        }
    
        printf("成功发送消息到消息队列\n");
    
        return 0;
    }
    

    在这个示例中,我们调用 msgsnd 函数向消息队列发送消息。首先使用 ftok 生成键值,然后创建或获取了一个消息队列,并将准备好的消息(定义为一个结构体)发送到消息队列中。如果发送成功,msgsnd 函数将返回 0;如果发送失败,返回值为 -1,并通过调用 perror 函数打印错误信息。

    总之,msgsnd 函数用于向消息队列发送消息,需要指定消息队列的标识符、要发送的消息内容和选项。

    4.msgrcv()

    msgrcv 函数用于从消息队列接收消息。

    函数原型如下:

    #include 
    #include 
    #include 
    
    ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp, int msgflg);
    

    参数:

    • msqid:消息队列的标识符,由 msgget 函数返回。
    • msgp:指向接收消息的缓冲区的指针,通常是一个结构体指针,结构体定义应与消息队列中消息的结构匹配。
    • msgsz:接收缓冲区的大小(以字节为单位)。
    • msgtyp:指定要接收的消息类型。设置为 0 表示接收队列中的第一条消息,设置为正数表示接收指定类型的消息,设置为负数表示接收小于或等于指定类型的最小数值的消息。
    • msgflg:接收消息的选项,可以通过按位或操作符 | 来设置多个选项,常用选项有:
      • IPC_NOWAIT:若队列为空,即时返回错误,而不是等待队列有可用消息。

    函数返回值:

    • 成功时,返回实际接收到的消息的长度(以字节为单位)。
    • 失败时,返回 -1,并设置全局变量 errno 来指示错误原因。

    以下是一个示例使用 msgrcv 的代码片段:

    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    
    struct message {
        long mtype;
        char mtext[100];
    };
    
    int main() {
        key_t key = ftok("/tmp/myfile", 'A');  // 生成键值
        if (key == -1) {
            perror("ftok");
            return 1;
        }
    
        int msgflg = IPC_CREAT | 0666;  // 创建消息队列并设置权限
    
        // 创建或获取消息队列
        int msqid = msgget(key, msgflg);
        if (msqid == -1) {
            perror("msgget");
            return 1;
        }
    
        // 准备接收消息的缓冲区
        struct message received_msg;
    
        // 接收消息
        ssize_t result = msgrcv(msqid, &received_msg, sizeof(received_msg.mtext), 1, 0);
        if (result == -1) {
            perror("msgrcv");
            return 1;
        }
    
        printf("成功接收消息:%s\n", received_msg.mtext);
    
        return 0;
    }
    

    在这个示例中,我们调用 msgrcv 函数从消息队列接收消息。首先使用 ftok 生成键值,然后创建或获取一个消息队列,并准备一个缓冲区用于接收消息。通过调用 msgrcv 函数,指定要接收的消息类型为 1,然后将接收到的消息存储到接收缓冲区中。如果接收成功,msgrcv 函数将返回接收到的消息的长度(以字节为单位);如果接收失败,返回值为 -1,并通过调用 perror 函数打印错误信息。

    总之,msgrcv 函数用于从消息队列中接收消息,需要指定消息队列的标识符、接收缓冲区的大小、要接收的消息类型和选项。

    5.msgctl()
    msgctl 函数用于控制消息队列,包括对消息队列的删除、获取与修改等操作。

    函数原型如下:

    #include 
    #include 
    #include 
    
    int msgctl(int msqid, int cmd, struct msqid_ds *buf);
    

    参数:

    • msqid:消息队列的标识符,由 msgget 函数返回。
    • cmd:控制操作的命令,可以是以下值之一:
      • IPC_RMID:私有的标识符被删除,该消息队列将被销毁。
      • IPC_SET:设置消息队列的属性,需要提供 buf 参数指向的 msqid_ds 结构体,用于存储需要修改的属性。
      • IPC_STAT:获取消息队列的属性,buf 参数指向的 msqid_ds 结构体将被填充为消息队列的当前属性值。
    • buf:指向 msqid_ds 结构体的指针,用于存储或获取消息队列的属性。

    函数返回值:

    • 成功时,返回 0。
    • 失败时,返回 -1,并设置全局变量 errno 来指示错误原因。

    以下是一个示例使用 msgctl 的代码片段:

    #include 
    #include 
    #include 
    #include 
    #include 
    
    int main() {
        key_t key = ftok("/tmp/myfile", 'A');  // 生成键值
        if (key == -1) {
            perror("ftok");
            return 1;
        }
    
        int msgflg = IPC_CREAT | 0666;  // 创建消息队列并设置权限
    
        // 创建或获取消息队列
        int msqid = msgget(key, msgflg);
        if (msqid == -1) {
            perror("msgget");
            return 1;
        }
    
        // 删除消息队列
        int result = msgctl(msqid, IPC_RMID, NULL);
        if (result == -1) {
            perror("msgctl");
            return 1;
        }
    
        printf("成功删除消息队列\n");
    
        return 0;
    }
    

    在这个示例中,我们调用 msgctl 函数删除一个消息队列。首先使用 ftok 生成键值,然后创建或获取一个消息队列,并指定要删除操作的命令为 IPC_RMID,通过调用 msgctl 函数来删除消息队列。如果删除成功,msgctl 函数将返回 0;如果删除失败,返回值为 -1,并通过调用 perror 函数打印错误信息。

    总之,msgctl 函数用于对消息队列进行控制操作,可以删除消息队列、获取消息队列属性或修改消息队列属性。需要提供消息队列的标识符、控制操作的命令以及可选的参数 buf 来存储或获取属性。

    5.4编程实战

    msgsend.c

    #include
    #include
    #include
    #include
    #include
    #include
    #include
    #include

    struct msgsend
    {
    long type; //第一个必须是long类型
    char text[1024];
    };

    int main(int argc, char const *argv[])
    {
    //1.ftok产生唯一key值
    key_t key = ftok("/home/hq/demo/进程/a.txt", 'A');
    if (key < 0)
    {
    perror("key is err");
    return 0;
    }

    //2.创建或打开消息队列
    int msgid = msgget(key, IPC_CREAT | IPC_EXCL | 0666);
    //等于0报错证明已经创建过相同key的消息队列
    if (msgid

    相关文章

    服务器端口转发,带你了解服务器端口转发
    服务器开放端口,服务器开放端口的步骤
    产品推荐:7月受欢迎AI容器镜像来了,有Qwen系列大模型镜像
    如何使用 WinGet 下载 Microsoft Store 应用
    百度搜索:蓝易云 – 熟悉ubuntu apt-get命令详解
    百度搜索:蓝易云 – 域名解析成功但ping不通解决方案

    发布评论