跳到主要内容

4. 进程间通信

4.1 信号

  • 在 Linux 系统中,信号是一种异步通知机制,用于在进程之间或者操作系统和进程之间传递事件和信息。进程可以通过信号来响应外部事件,例如用户输入、硬件异常、其他进程的操作等。

  • 用户进程对信号的响应有三种方式:

    • 忽略信号:对信号不做任何处理。但是有两个信号是不能忽略的,即 SIGKILL 和 SIGSTOP。SIGKILL 用于立即终止进程的执行,SIGSTOP 用于暂停进程的执行。
    • 捕捉信号:定义信号处理函数,当信号发送时,执行相应的自定义处理函数。应用程序可以使用 signal() 或者 sigaction() 等系统调用来注册信号处理函数,以响应特定的信号。
    • 执行默认操作:Linux 对每种信号都规定了默认操作。例如,当进程收到 SIGTERM 信号时,Linux 默认会终止该进程的执行。应用程序可以选择不定义信号处理函数,而让系统执行默认操作来响应特定的信号。
  • 信号从产生到处理的过程:

    • 信号的产生:信号可以由多种事件触发,例如硬件中断、软件异常、用户按下某个键等。当事件发生时,Linux 内核会自动产生相应的信号,并将其发送给目标进程。
    • 信号的传递:产生信号的进程将信号发送给一个特定的目标进程,通常使用系统调用 kill() 或者 sigqueue() 来发送信号。发送信号的进程需要知道目标进程的进程 ID(PID),并且需要指定信号类型和其他参数。
    • 信号的接收:目标进程接收到信号,操作系统会检查该进程对该信号的处理方式。如果该信号已经被阻塞或者忽略,那么操作系统会将信号保存在进程的信号队列中,等待目标进程解除对该信号的阻塞或忽略后再进行处理。
    • 信号的处理:如果目标进程没有对该信号进行特殊处理,或者该信号没有被阻塞或忽略,那么操作系统就会调用目标进程的信号处理函数来处理该信号。每个进程可以设置自己的信号处理函数,当进程接收到信号时,操作系统会自动调用相应的信号处理函数。
    • 信号的处理方式:每个进程可以设置自己的信号处理方式,包括信号的处理函数、信号的阻塞方式、信号的忽略方式等。有些信号不能被阻塞或忽略,例如 SIGKILL 和 SIGSTOP 信号。
    • 信号的优先级:Linux 中的信号具有优先级,数字越小的信号优先级越高,例如 SIGKILL 的优先级为 9,而 SIGINT 的优先级为 2。当进程同时收到多个信号时,操作系统会根据信号的优先级来决定先处理哪个信号。
    • 信号的默认处理方式:对于每种信号类型,Linux 内核都定义了一种默认的处理方式。例如,对于 SIGINT 信号(通常由用户在终端上按下 Ctrl+C 产生),默认处理方式是终止目标进程。但是,进程可以通过调用 sigaction 等函数来设置自己的信号处理方式,包括设置信号的处理函数、阻塞或忽略信号等。
  • 如果想要查看Linux中已经定义好的信号,在终端输入:

    linaro@linaro-alip:~$ kill -l
    1) SIGHUP 2) SIGINT 3) SIGQUIT 4) SIGILL 5) SIGTRAP
    6) SIGABRT 7) SIGBUS 8) SIGFPE 9) SIGKILL 10) SIGUSR1
    11) SIGSEGV 12) SIGUSR2 13) SIGPIPE 14) SIGALRM 15) SIGTERM
    16) SIGSTKFLT 17) SIGCHLD 18) SIGCONT 19) SIGSTOP 20) SIGTSTP
    21) SIGTTIN 22) SIGTTOU 23) SIGURG 24) SIGXCPU 25) SIGXFSZ
    26) SIGVTALRM 27) SIGPROF 28) SIGWINCH 29) SIGIO 30) SIGPWR
    31) SIGSYS 34) SIGRTMIN 35) SIGRTMIN+1 36) SIGRTMIN+2 37) SIGRTMIN+3
    38) SIGRTMIN+4 39) SIGRTMIN+5 40) SIGRTMIN+6 41) SIGRTMIN+7 42) SIGRTMIN+8
    43) SIGRTMIN+9 44) SIGRTMIN+10 45) SIGRTMIN+11 46) SIGRTMIN+12 47) SIGRTMIN+13
    48) SIGRTMIN+14 49) SIGRTMIN+15 50) SIGRTMAX-14 51) SIGRTMAX-13 52) SIGRTMAX-12
    53) SIGRTMAX-11 54) SIGRTMAX-10 55) SIGRTMAX-9 56) SIGRTMAX-8 57) SIGRTMAX-7
    58) SIGRTMAX-6 59) SIGRTMAX-5 60) SIGRTMAX-4 61) SIGRTMAX-3 62) SIGRTMAX-2
    63) SIGRTMAX-1 64) SIGRTMAX
  • 系统中常用的信号:

    信号信号值程序默认行为说明
    SIGHUP1终止运行进程的控制终端关闭
    SIGINT2终止运行用户产生中断符(Ctrl-C)
    SIGQUIT3终止且进行内存转储用户产生退出符(Ctrl-)
    SIGILL4终止且进行内存转储进程试图执行非法指令
    SIGTRAP5终止且进行内存转储进入断点
    SIGABRT6终止且进行内存转储来自abort()函数的终止信号
    SIGBUS7终止且进行内存转储硬件或对齐错误
    SIGFPE8终止且进行内存转储算术异常
    SIGKILL9终止运行强制杀死进程,程序无法对该信号进行定制处理
    SIGUSR110终止运行进程自定义的信号1
    SIGSEGV11终止且进行内存转储无效内存访问
    SIGUSR212终止运行进程自定义的信号2
    SIGPIPE13终止向无读取进程的管道写入
    SIGALRM14终止进行由alarm()发送
    SIGTERM15终止可以捕获的进程终止信号
    SIGSTKFLT16终止(b)协处理器栈错误
    SIGCHLD17忽略子进程终止
    SIGCONT18忽略进程停止后继续执行
    SIGSTOP19停止挂起进程
    SIGSTP20停止用户生成挂起操作符(Ctrl-Z)
    SIGTTIN21停止后台进程从控制终端读
    SIGTTOU22停止后台进程从控制终端写
    SIGURG23忽略紧急I/O未处理
    SIGXCPU24终止且进行内存转储超过CPU时间资源限制
    SIGXFSZ25终止且进行内存转储超过文件大小资源限制
    SIGVTALRM26终止计算该进程占用CPU的时间
    SIGPROF27终止向无读取进程的管道写入
    SIGWINCH28忽略控制终端窗口大小改变
    SIGIO29终止(a)异步IO事件(Ctrl-C)
    SIGPWR30终止断电
    SIGSYS31终止且进行内存转储进程试图执行无效系统调用

4.1.1 发送信号

  • 发送信号的函数主要有kill(),raise(),alarm(),pause()等函数。

    1. kill()函数声明:

      #include <sys/types.h>
      #include <signal.h>
      int kill(pid_t pid, int sig);
      • pid 参数指定要接收信号的进程 ID;
      • sig 参数指定要发送的信号类型。
      • kill()函数实例:
      #include <signal.h>
      #include <stdlib.h>

      int main(int argc,char *argv[])
      {
      pid_t pid;
      int sig;

      if(argc < 3){
      printf("Usage:%s <pid_t> <signal>\n",argv[0]);
      return -1;
      }
      //字符串转整型
      sig = atoi(argv[2]);
      pid = atoi(argv[1]);

      kill(pid,sig);

      return 0;
      }
      • 编译命令并运行:
      gcc kill.c -o kill
      ./kill
    2. raise()函数可以让进程向自己发送指定的信号,函数声明:

      #include <signal.h>
      int raise(int sig);
      • sig 参数指定要发送的信号类型。
      #include <stdio.h>
      #include <unistd.h>
      #include <sys/types.h>
      #include <signal.h>
      #include <stdlib.h>

      int main(void)
      {
      printf("raise before\n");

      raise(9);

      printf("raise after\n");


      return 0;
      }
      • 运行结果:
      pi@raspberrypi:-/Linux/signal $./raise
      raise before
      Killed
      • 程序打印出开始,使用raise()函数发送停止信号,进程结束。
    1. alarm() 函数可以在指定的时间之后向当前进程发送一个SIGALRM信号,用于定时操作。函数声明:

      #include <unistd.h>
      unsigned int alarm(unsigned int seconds);
      • seconds 参数指定定时的秒数。alarm() 函数会在指定的秒数之后向当前进程发送一个 SIGALRM 信号。
      • alarm()函数实例:

        #include <stdio.h>
        #include <unistd.h>
        #include <signal.h>

        void handler(int sig) {
        printf("Received signal %d\n", sig);
        }

        int main() {
        signal(SIGALRM, handler); // 注册信号处理函数
        alarm(5); // 设置定时器,5秒后触发 SIGALRM 信号
        printf("Waiting for alarm to go off...\n");
        pause(); // 阻塞进程,等待信号触发
        printf("Exiting...\n");
        return 0;
        }
      • 编译命令并运行:

        gcc alarm.c -o alarm
        ./alarm
      • 运行结果:

        pi@raspberrypi:~/Linux/signal s./alarm
        waiting for alarm to go off...
        Received signal 14
        Exiting...
      • 在上面的示例程序中,我们首先注册了 SIGALRM 信号的处理函数 handler(),然后调用 alarm(5) 函数来设置一个 5 秒的定时器。在等待定时器触发的过程中,我们使用 pause() 函数来阻塞进程,等待信号触发。当定时器触发后,会自动向进程发送 SIGALRM 信号,从而触发信号处理函数,程序就会打印出收到的信号号码,并退出。

4.1.2 信号的接收

  • 接收信号:如果要让我们接收信号的进程可以接收到信号,那么这个进程就不能停止,一般使用while、sleep和pause。
  • Linux系统调用signal()来为信号设置一个新的信号处理程序,可以将这个信号处理程序设置为一个用户指定的函数,函数声明如下:

    #include <signal.h>
    typedef void (*sighandler_t)(int);
    sighandler_t signal(int signum, sighandler_t handler);
    • signum:我们要进行处理的信号,系统的信号我们可以再终端键入 kill -l查看。
    • handler:处理的方式(是系统默认还是忽略还是捕获)。
    *signal(SIGINT ,SIG_IGN); //SIG_IGN, 代表忽略,也就是忽略SIGINT信号,SIGINT信号由InterruptKey产生,通常是用户按了Ctrl+C键或者Delete键产生。
    *signal(SIGINT ,SIG_DFL); //SIG_DFL代表执行系统默认操作,大多数信号的系统默认动作时终止该进程。
    *signal(SIGINT ,handler); //捕捉SIGINT这个信号,然后执行handler函数里面的代码。handler由我们自己定义。
  • 自定义信号SIGINT的处理:

    #include <stdio.h>
    #include <signal.h>

    void signal_handler_fun(int signum)
    {
    printf("catch signal %d\n", signum);
    }

    int main(int argc, char *argv[])
    {
    signal(SIGINT, signal_handler_fun);
    while (1);
    return 0;
    }
    • 可以看到每次按下Ctrl+C键的时候,都会执行signal_handler_fun函数,而不是退出程序。

4.2 管道

  • 在 Linux 系统中,管道用于连接读进程和写进程,以实现它们之间通信的共享文件,故又称管道文件。

  • 在 Linux 中,管道可以分为两种类型:匿名管道和命名管道。

    • 匿名管道(Anonymous Pipes):匿名管道是一种简单的半双工管道,只能用于在具有亲缘关系的进程之间进行通信。匿名管道创建后,它就成为两个进程之间的共享文件描述符,其中一个进程通过管道写入数据,另一个进程通过管道读取数据。匿名管道的生命周期与进程相关联,当创建它的进程终止后,匿名管道也被销毁。

    • 命名管道(Named Pipes):命名管道也称为FIFO,是一种特殊类型的文件,它可以允许任何进程在任何时间通过文件名来访问它。它提供了一种无关进程亲缘关系的通信机制,可以允许多个进程同时访问它,并且可以用于在网络中进行进程通信。命名管道会一直存在于文件系统中,直到它被删除或系统关闭。任何有权限访问命名管道的进程都可以向其中写入数据或读取数据。

    • 综上所述,匿名管道适用于亲缘关系进程之间的通信,而命名管道则适用于非亲缘关系进程之间的通信,且命名管道具有持久性。

4.2.1 匿名管道

  • 匿名管道的特点:

    • 管道采用半双工通信方式,因此数据只能单向传输。
    • 管道其实是一个固定大小的缓冲区,如果一个进程向已满的管道写入数据,系统会阻塞该进程,直到管道能有空间接收数据。
    • 管道通信机制必须能够提供读写进程之间的同步机制。
  • 创建匿名管道使用pipe()函数,函数声明:

    #include <unistd.h>
    int pipe(int pipefd[2]);
    • 函数参数pipefd是一个包含两个int类型元素的数组,用于返回两个文件描述符;pipefd[0]为读端,pipefd[1]为写端。在调用pipe()函数后,两个进程可以使用这两个文件描述符进行通信,实现数据的传输。
  • 父子进程通过管道通信的实例:

    • 第一步:父进程调用pipe()函数创建管道,得到两个文件描述符fd[0]、fd[1],分别指向管道的读端和写端。

    • 第二步:父进程使用fork()函数创建子进程,那么子进程也有两个文件描述符指向同一管道。

    • 第三步:父进程关闭管道读端,子进程关闭管道写端。父进程可以向管道中写入数据,子进程将管道数据读出。

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

      void sys_err(const char *str)
      {
      perror(str);
      exit(1);
      }

      int main(void)
      {
      pid_t pid;
      char buf[1024];
      int fd[2];
      char p[] = "test for pipe\n";

      if (pipe(fd) == -1)
      sys_err("pipe");

      pid = fork();
      if (pid < 0) {
      sys_err("fork err");
      }
      else if (pid == 0) {
      close(fd[1]);
      printf("child process wait to read:\n");
      int len = read(fd[0], buf, sizeof(buf));
      write(STDOUT_FILENO, buf, len);
      close(fd[0]);
      }
      else {
      close(fd[0]);
      write(fd[1], p, strlen(p));
      wait(NULL);
      close(fd[1]);
      }

      return 0;
      }

4.2.2 命名管道

  • 创建命名管道使用mkfifo()函数,函数声明:

    #include <sys/types.h>
    #include <sys/stat.h>

    int mkfifo(const char *pathname, mode_t mode);
    • 函数参数pathname是有名管道的路径名,mode是权限掩码。调用mkfifo()函数会在指定路径名创建一个有名管道,返回值为0表示创建成功,-1表示创建失败。
    • 读写有名管道时可以使用普通的read()和write()函数,也可以使用open()函数打开管道文件,然后使用read()和write()函数进行读写。当然,在使用完有名管道后,需要使用unlink()函数删除管道文件。
  • 在Linux系统中,可以使用C语言提供的access()函数来检查一个进程是否有访问某个文件或目录的权限。函数声明:

    int access(const char *pathname, int mode);
    • pathname参数指定要检查的文件或目录的路径,mode参数指定要检查的访问权限,常用的访问权限包括:
      • F_OK:检查文件是否存在。
      • R_OK:检查文件是否可读。
      • W_OK:检查文件是否可写。
      • X_OK:检查文件是否可执行。
    • access()函数返回0表示检查成功,-1表示检查失败。如果access()函数返回-1,可以使用errno来获取错误码,通常的错误码包括:
  • 命名管道通信的实例:

    • 管道读取端:
    #include <stdio.h>
    #include <unistd.h>
    #include <stdlib.h>
    #include <sys/wait.h>
    #include <sys/types.h>
    #include <sys/stat.h>
    #include <fcntl.h>
    #include <string.h>
    int main(int argc, char *argv[])
    {
    char buf[32] = {0};
    int fd;
    if (argc < 2)
    {
    printf("Usage:%s <fifo name> \n", argv[0]);
    return -1;
    }
    fd = open(argv[1], O_RDONLY);
    while (1)
    {
    sleep(1);
    read(fd, buf, 32);
    printf("buf is %s\n", buf);
    memset(buf, 0, sizeof(buf));
    }
    close(fd);
    return 0;
    }
    • 管道写入端:
    #include <stdio.h>
    #include <unistd.h>
    #include <stdlib.h>
    #include <sys/wait.h>
    #include <sys/types.h>
    #include <sys/stat.h>
    #include <fcntl.h>

    int main(int argc, char *argv[])
    {
    int ret;
    char buf[32] = {0};
    int fd;
    if (argc < 2)
    {
    printf("Usage:%s <fifo name> \n", argv[0]);
    return -1;
    }

    if (access(argv[1], F_OK) == -1)
    {
    ret = mkfifo(argv[1], 0666);
    if (ret == -1)
    {
    printf("mkfifo is error \n");
    return -2;
    }
    printf("mkfifo is ok \n");
    }
    fd = open(argv[1], O_WRONLY);
    while (1)
    {
    sleep(1);
    write(fd, "hello", 5);
    }
    close(fd);
    return 0;
    }
  • 编译命令并运行:

    gcc fifo_write.c -o fifow
    gcc fifo_read.c -o fifor
    打开两个终端分别运行
    ./fifor fifo
    ./fifow fifo

4.3 消息队列

  • 在 Linux 系统中,消息队列是一种进程间通信(IPC)机制,用于实现不同进程之间的数据传输。它是一种先进先出(FIFO)的数据结构,允许一个或多个进程通过在消息队列中发送和接收消息来通信。 消息队列由一个消息队列标识符(mqid)来标识,它类似于文件描述符,用于标识消息队列的唯一性。在创建消息队列时,需要指定一个唯一的键(key),这个键用于在系统范围内标识消息队列,确保多个进程可以通过相同的键访问同一个消息队列。消息队列支持在不同进程之间共享数据,这使得它成为一种非常强大的进程间通信机制。

  • 使用消息队列进行进程间通信可以有以下优点:

    • 可以实现多对多通信模式,多个进程可以同时读写同一个消息队列。
    • 消息队列中的消息可以保持在系统中,即使发送消息的进程已经退出,接收进程仍然可以读取这些消息。
    • 消息队列具有一定的容错能力,当接收进程暂时无法处理消息时,消息可以保留在队列中,等待接收进程重新准备好之后再进行处理。
    • 消息队列可以通过指定优先级来处理消息,可以确保高优先级消息先被处理。
    • 总之,消息队列是一种非常实用的进程间通信机制,在各种应用程序中广泛应用,例如网络通信、多线程编程、分布式系统等。
  • 在Linux系统中,消息队列是一种进程间通信的机制,用于在不同的进程之间传递数据。在Linux系统中,消息队列的操作主要依赖于以下几个函数:

  1. 创建或获取一个消息队列函数msgget(),函数声明:

    #include <sys/types.h>
    #include <sys/ipc.h>
    #include <sys/msg.h>

    int msgget(key_t key, int msgflg);
    • key参数是消息队列的键值,用于唯一地标识一个消息队列;

    • msgflg参数是创建消息队列的标志位,用于指定消息队列的属性。

    • 在 Linux 系统中,可以使用一些宏来生成用于 msgget() 函数的 key 参数的值。常用的宏有以下几种:

      • IPC_PRIVATE 宏表示创建一个新的、私有的消息队列,仅当前进程可用。该标志一般用于在进程间共享数据时,仅仅在父子进程之间共享数据,不需要在不同进程之间共享。

      • ftok() 函数可以根据给定的文件路径名和整数生成一个唯一的键值。在使用 ftok() 函数时,需要传递一个可访问的文件路径名和一个用户自定义的整数作为参数,例如:

        key_t key = ftok("/tmp", 'a');
      • 该代码将使用路径名 /tmp 和字符 'a' 生成一个用于 msgget() 函数的键值。

      • IPC_CREAT 宏用于创建一个新的消息队列。如果指定的消息队列不存在,则创建一个新的消息队列,否则返回已有的消息队列的标识符。

      • IPC_EXCL 宏用于指定如果同时指定了 IPC_CREAT 和 IPC_EXCL 标志,则只在消息队列不存在时创建一个新的消息队列,否则返回错误。

    • msgflg参数用于指定消息队列的属性,可以使用多个标志位进行设置,其常用取值如下:

      • IPC_CREAT:如果指定的消息队列不存在,则创建一个新的消息队列,否则返回已有的消息队列的标识符。
      • IPC_EXCL:如果同时指定了 IPC_CREAT 和 IPC_EXCL 标志,则只在消息队列不存在时创建一个新的消息队列,否则返回错误。
      • IPC_PRIVATE:表示创建一个新的、私有的消息队列,仅当前进程可用。该标志一般用于在进程间共享数据时,仅仅在父子进程之间共享数据,不需要在不同进程之间共享。
      • 0666:表示创建的消息队列的权限,其取值与文件权限的表示方式相同。其中,6 表示读写权限,4 表示读权限,2 表示写权限,0 表示无权限。
  2. 向消息队列中发送消息函数msgsnd(),函数声明:

    #include <sys/types.h>
    #include <sys/ipc.h>
    #include <sys/msg.h>

    int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);
    • msqid 参数是消息队列的标识符(由msgget函数得到);
    • msgp 参数是指向消息缓冲区的指针,该缓冲区用来暂时存储要发送的消息,通常可用一个通用结构体来表示消息:
    struct msgbuf {
    long mtype; /* 消息类型 */
    char mtext[1024]; /* 消息内容 */
    };
    • msgsz 参数是发送消息的长度(字节数),使用公式sizeof(struct mymsg) - sizeof(long) 计算出消息的实际长度(不包括消息类型字段):
      • 结构体 mymsg 的总大小为 sizeof(long) + 1024 = 1032 字节。
      • msgsz = sizeof(struct mymsg) - sizeof(long) = 1032 - 8 = 1024字节。
    • msgflg 参数用于指定发送消息的行为,可以取以下值:
      • 0:表示阻塞方式,线程将被阻塞直到消息可以被写入。
      • IPC_NOWAIT:表示非阻塞方式,如果消息队列已满或其他情况无法送入消息,函数立即返回。
    • 如果函数执行成功就返回0,失败返回-1。
  3. 从消息队列中接收消息函数msgrcv(),函数声明:

    #include <sys/types.h>
    #include <sys/ipc.h>
    #include <sys/msg.h>

    ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp, int msgflg);
    • msqid 参数是消息队列的标识符;
    • msgp 参数是指向接收消息的指针,通常可用一个通用结构体来表示消息:
    struct msgbuf {
    long mtype; /* 消息类型 */
    char mtext[1024]; /* 消息内容 */
    };
    • msgsz 参数是接收消息的长度;
    • msgtyp 参数是指定接收消息的类型;
    • msgflg 参数用于指定接收消息的行为,可以取以下值:
      • 0:表示阻塞方式,当消息队列为空时,一直等待;
      • IPC_NOWAIT:表示非阻塞方式,消息队列为空时,不等待,马上返回-1.如果函数执行成功,msgrcv返回到mtext数组的实际字节数。
  4. 控制消息队列的状态函数msgctl(),函数声明:

    #include <sys/types.h>
    #include <sys/ipc.h>
    #include <sys/msg.h>

    int msgctl(int msqid, int cmd, struct msqid_ds *buf);
    • msqid 参数是消息队列的标识符;
    • cmd 参数是指定执行的操作,包括删除消息队列、查询消息队列的状态等,可以取以下值:
      • IPC_STAT:读取消息队列的属性,然后把它保存在buf指向的缓冲区;
      • IPC_SET:设置消息队列的属性,这个值取自buf参数;
      • IPC_EMID:将队列从系统内核中删除。
    • buf 参数是指向 msqid_ds 结构体的指针,用于保存查询到的消息队列的状态信息。
  5. 下面是一个简单的消息队列示例程序,包括发送进程和接收进程:

    #include <stdio.h>
    #include <stdlib.h>
    #include <string.h>
    #include <unistd.h>
    #include <sys/ipc.h>
    #include <sys/msg.h>

    #define MSG_KEY 1234

    /* 定义消息结构体 */
    struct mymsg {
    long mtype; /* 消息类型 */
    char mtext[1024]; /* 消息内容 */
    };

    /* 发送进程 */
    void sender()
    {
    int msgid, ret;
    struct mymsg msg;

    /* 创建或打开消息队列 */
    msgid = msgget(MSG_KEY, IPC_CREAT | 0666);
    if (msgid < 0) {
    perror("msgget");
    exit(1);
    }

    /* 构造消息 */
    msg.mtype = 1;
    strncpy(msg.mtext, "Hello, waveshare!", 1024);

    /* 发送消息 */
    ret = msgsnd(msgid, &msg, sizeof(struct mymsg) - sizeof(long), 0);
    if (ret < 0) {
    perror("msgsnd");
    exit(1);
    }

    printf("Sent message: %s\n", msg.mtext);
    }

    /* 接收进程 */
    void receiver()
    {
    int msgid, ret;
    struct mymsg msg;

    /* 打开消息队列 */
    msgid = msgget(MSG_KEY, IPC_CREAT | 0666);
    if (msgid < 0) {
    perror("msgget");
    exit(1);
    }

    /* 接收消息 */
    ret = msgrcv(msgid, &msg, sizeof(struct mymsg) - sizeof(long), 1, 0);
    if (ret < 0) {
    perror("msgrcv");
    exit(1);
    }

    printf("Received message: %s\n", msg.mtext);
    }

    int main()
    {
    pid_t pid;

    /* 创建子进程 */
    pid = fork();
    if (pid < 0) {
    perror("fork");
    exit(1);
    } else if (pid == 0) {
    /* 子进程作为发送进程 */
    sender();
    } else {
    /* 父进程作为接收进程 */
    receiver();
    }

    return 0;
    }

4.4 System-V IPC 信号量

  • System V 信号量是一种进程间同步和互斥的机制,它使用了一种基于计数器的方法来控制对共享资源的访问,允许多个进程同时对资源进行访问,但同时限制了并发访问的数量。 这种机制能够确保每个进程都能够顺利地获取到资源的控制权,从而实现了多进程间的同步和互斥。

4.4.1 创建或获取一个信号量

  • 在System V IPC (Inter-Process Communication) 中使用semget()函数来创建或获取一个信号量集,函数声明:

    #include <sys/types.h>
    #include <sys/ipc.h>
    #include <sys/sem.h>

    int semget(key_t key, int nsems, int semflg);
    • 参数说明:

      • key:用于标识信号量集的键值。可以使用 ftok 函数生成一个唯一的键值。
      • nsems:指定要创建或获取的信号量集中信号量的数量。
      • semflg:用于指定操作标志,可以是一个或多个标志位的按位或操作。
    • 函数返回值:函数执行成功返回一个非负整数,表示信号量集的标识符(也称为信号量集的描述符),执行失败返回 -1。

4.4.2 操作信号量

  • 在System V IPC 中使用semop()函数对信号量进行 PV 操作(加操作和减操作),函数声明:

    #include <sys/types.h>
    #include <sys/ipc.h>
    #include <sys/sem.h>

    int semop(int semid, struct sembuf *sops, size_t nsops);
  • 参数说明:

    • semid:信号量集的标识符(由 semget 函数返回)。
    • sops:指向结构体数组的指针,每个结构体描述一个 PV 操作。
    • nsops:指定 sops 数组中的结构体数量。
  • 函数返回值:函数执行成功返回0,执行失败返回 -1。

  • 结构体 sembuf 定义如下:

    struct sembuf {
    unsigned short sem_num; // 要操作的信号量的编号
    short sem_op; // 操作标志,P/V操作,1为V操作,释放资源。-1为P操作,分配资源。0为等待,直到信号量的值变成0
    short sem_flg; // 0表示阻塞,IPC_NOWAIT表示非阻塞
    };
  • 对信号量进行操作的两种基本方式:

    • P 操作也称为等待(Wait)操作或者减小(Decrement)操作,它用于获取信号量的控制权并将信号量的值减一。具体来说,当进程需要访问某个共享资源时,它会调用 P 操作等待信号量,如果信号量的值大于等于 1,那么进程会减少信号量的值并继续执行,否则,进程将被阻塞等待,直到信号量的值变为大于等于 1。
    • V 操作也称为释放(Signal)操作或者增加(Increment)操作,它用于释放信号量的控制权并将信号量的值加一。具体来说,当进程完成对某个共享资源的访问后,它会调用 V 操作释放信号量,这会使得被阻塞等待该信号量的进程重新获得控制权并继续执行。
  • PV 操作是 System V 信号量中最基本、最常用的操作,可以用于实现各种同步和互斥机制,如互斥锁、读写锁等。由于 PV 操作的原子性,它能够确保对共享资源的访问不会出现竞争条件(Race Condition)等问题,从而保证了多进程间的同步和互斥。

4.4.3 控制信号量集属性

  • 在System V IPC 中使用semctl()函数制信号量集属性,函数声明:

    #include <sys/types.h>
    #include <sys/ipc.h>
    #include <sys/sem.h>

    int semctl(int semid, int semnum, int cmd, ...);
    • 参数说明:

      • semid:信号量集的标识符(由 semget 函数返回)。

      • semnum:信号量在信号量集中的索引。对于大多数命令,该参数被忽略,可以设置为 0。

      • cmd:指定要执行的操作命令,可以是以下命令之一:

        • GETVAL:获取信号量的当前值。
        • SETVAL:设置信号量的值。
        • SETALL:设置所有信号量的值。
        • IPC_RMID:删除信号量集。
    • 函数返回值:函数执行成功返回一个非负整数,执行失败返回 -1。

    • 在使用 semctl 函数时,根据命令的不同,可能需要提供额外的参数。例如,对于 SETVAL 命令,需要提供一个 union semun 结构体作为第四个参数,其中包含要设置的信号量的值。union semun 定义如下:

      union semun {
      int val; // SETVAL 命令使用的值
      struct semid_ds *buf; // IPC_STAT 和 IPC_SET 命令使用的缓冲区
      unsigned short *array; // GETALL 和 SETALL 命令使用的数组
      };

4.4.4 信号量实例

  • 使用信号量来控制两个进程(父子进程)之间的执行顺序:

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

    union semun
    {
    int val;
    };

    int main(void)
    {
    int semid;
    int key;
    pid_t pid;
    struct sembuf sem;
    union semun semun_union;
    key = ftok("./a.c", 0666);
    semid = semget(key, 1, 0666 | IPC_CREAT);
    semun_union.val = 0;
    semctl(semid, 0, SETVAL, semun_union);
    pid = fork();
    if (pid > 0)
    {
    sem.sem_num = 0;
    sem.sem_op = -1; //P操作
    sem.sem_flg = 0;
    semop(semid, &sem, 1);
    printf("This is parents\n");
    sem.sem_num = 0;
    sem.sem_op = 1;
    sem.sem_flg = 0;
    semop(semid, &sem, 1);
    }

    if (pid == 0)
    {
    sleep(2);
    sem.sem_num = 0;
    sem.sem_op = 1; //V操作
    sem.sem_flg = 0;
    semop(semid, &sem, 1);
    printf("This is son\n");
    }
    return 0;
    }

  • 运行程序:

    linaro@linaro-alip:~/Linux/process$ ./semget
    This is parents
    This is son
    linaro@linaro-alip:~/Linux/process$

4.5 共享内存

  • 共享内存是指多个进程之间共享同一块物理内存的一种机制。这种机制允许多个进程可以访问同一块内存区域,从而实现进程间的数据共享。
  • 在共享内存的机制下,多个进程可以通过映射同一块内存区域来实现数据的共享。这样,多个进程就可以通过读写同一块内存来实现彼此之间的通信和同步,而无需进行任何的数据拷贝和进程间通信的操作。因此,共享内存是一种高效的进程间通信机制。

4.5.1 新建共享内存

  • 在Linux系统中使用shmget()函数来创建共享内存,函数声明:

    #include <sys/ipc.h>
    #include <sys/shm.h>

    int shmget(key_t key, size_t size, int shmflg);
    • 其中,参数key用来指定共享内存的键值,参数size指定共享内存的大小,参数shmflg用来指定创建共享内存的标志,其取值可以是以下几个常量的按位或组合:

      • IPC_CREAT:如果共享内存不存在,则创建共享内存;否则,返回共享内存的标识符。
      • IPC_EXCL:与IPC_CREAT标志一起使用,如果共享内存已经存在,则返回错误。
      • IPC_PRIVATE:使用一个随机值作为键值来创建共享内存。
    • 返回值:shmget函数的返回值是一个整数,表示共享内存的标识符。如果创建共享内存失败,shmget函数会返回-1,并设置errno变量指示错误类型。

  • 下面是一个示例代码,演示如何使用shmget函数创建共享内存:

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

    int main()
    {
    int shmid;
    shmid = shmget(IPC_PRIVATE,1024,0777);
    if(shmid < 0)
    {
    printf("shmget is error n");
    return -1;
    }

    printf("Shared memory segment ID = %d\n", shmid);

    exit(0);
    }

  • 运行程序:

    linaro@linaro-alip:~/Linux/process$ gcc -o shmget shmget.c
    linaro@linaro-alip:~/Linux/process$ ./shmget
    Shared memory segment ID = 983045
    linaro@linaro-alip:~/Linux/process$ ipcs -m

    ------ Shared Memory Segments --------
    key shmid owner perms bytes nattch status
    0x00000000 65536 linaro 600 33554432 2 dest
    0x00000000 163841 linaro 600 2097152 2 dest
    0x00000000 196610 linaro 600 393216 2 dest
    0x00000000 950275 linaro 600 524288 2 dest
    0x00000000 491524 linaro 600 524288 2 dest
    0x00000000 983045 linaro 777 1024 0

4.5.2 shmat()映射函数

  • shmat函数是Linux系统中用于将共享内存区域映射到进程地址空间的函数,函数声明:

    #include <sys/types.h>
    #include <sys/shm.h>

    void *shmat(int shmid, const void *shmaddr, int shmflg);
    • 其中,参数shmid是共享内存的标识符,由shmget函数返回。
    • 参数shmaddr用于指定映射的起始地址,如果shmaddr的值为0,则由操作系统选择一个合适的地址进行映射,一般写成NULL。
    • 参数shmflg用于指定映射的标志,一般使用0表示默认选项,操作共享内存的方式:
      • SHM_RDONLY: 以只读模式映射共享内存区域。
      • SHM_REMAP: 将共享内存重新映射到进程的地址空间。
      • SHM_EXEC: 允许共享内存区域的内容执行。一般情况下,共享内存的内容不允许执行。
      • SHM_DEST: 当进程调用shmdt()函数解除共享内存映射时,将销毁共享内存区域。
    • 返回值:shmat函数返回一个指针,指向共享内存映射的起始地址。如果映射失败,shmat函数返回-1,并设置errno变量指示错误类型。

4.5.3 shmdt()解除映射函数

  • shmdt函数是Linux系统中用于解除共享内存映射的函数,函数声明:

    #include <sys/types.h>
    #include <sys/shm.h>

    int shmdt(const void *shmaddr);
    • 参数shmaddr是共享内存映射的起始地址,该地址通常是由shmat函数返回的指针。
    • 返回值:返回0表示成功,返回-1表示失败。
    • 注意:shmdt函数用于将共享内存从进程的地址空间中分离,解除映射关系。它并不会删除共享内存区域,只是使得当前进程无法再访问该共享内存。

4.5.4 shmctl()获取或设置属性函数

  • shmctl函数是Linux系统中用于控制共享内存的函数,它可以用于获取或修改共享内存的属性,函数声明:

    #include <sys/types.h>
    #include <sys/shm.h>

    int shmdt(const void *shmaddr);
    • 参数shmid是共享内存的标识符,由shmget函数返回。参数cmd是要执行的操作命令,可以是以下几个常用的命令:

      • IPC_STAT:获取共享内存的状态信息,将结果保存在buf指向的结构体shmid_ds中。
      • IPC_SET:设置共享内存的状态信息,使用buf中的数据进行设置。
      • IPC_RMID:删除共享内存,释放其资源。
    • 参数buf是一个指向struct shmid_ds结构体的指针,用于存储或传递共享内存的状态信息。

    • 返回值:成功时返回0,失败时返回-1。

4.5.5 共享内存的应用实例

  • 建立父子进程间的共享内存:

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

    int main(void)
    {

    int shmid;
    key_t key;
    pid_t pid;
    char *s_addr, *p_addr;
    key = ftok("./a.c", 'a');
    shmid = shmget(key, 1024, 0777 | IPC_CREAT);
    if (shmid < 0)
    {
    printf("shmget is error\n");
    return -1;
    }
    printf("shmget is ok and shmid is %d\n", shmid);
    pid = fork();
    if (pid > 0)
    {
    p_addr = shmat(shmid, NULL, 0);
    strncpy(p_addr, "hello", 5);
    wait(NULL);
    exit(0);
    }
    if (pid == 0)
    {

    sleep(2);
    s_addr = shmat(shmid, NULL, 0);
    printf("s_addr is %s\n", s_addr);
    exit(0);
    }
    return 0;
    }