【Linux网络编程】多进程编程

总结《Linux高性能服务器编程》第13章

第13章 多进程编程

fork系统调用

  • Linux下创建新进程的系统调用是fork:

    #include<sys/types.h>
    #include<unistd.h>
    pid_t fork(void);
    
    • 父进程中返回的是子进程的PID,在子进程中则返回0;
    • fork函数复制当前进程,在内核进程表中创建一个新的进程表项;
      • 新的进程表项有很多属性和原进程相同(如堆指针、栈指针和标志寄存器的值);
      • 也有许多属性被赋予了新的值(如该进程的PPID被设置成原进程的PID,信号位图被清除);
    • 子进程的代码与父进程完全相同;
    • 子进程会复制父进程的数据(堆数据、栈数据和静态数据),采用写时复制

exec系列系统调用

  • 使用exec系列系统调用==在子进程中执行其他程序==,即替换当前进程映像:

    #include<unistd.h>
    extern char**environ;
    int execl(const char*path,const char*arg,...);
    int execlp(const char*file,const char*arg,...);
    int execle(const char*path,const char*arg,...,char*const envp[]);
    int execv(const char*path,char*const argv[]);
    int execvp(const char*file,char*const argv[]);
    int execve(const char*path,char*const argv[],char*const envp[]);
    
    • path参数指定可执行文件的完整路径,file参数可以接受文件名,arg接受可变参数;
    • 一般情况下,exec函数是不返回的,除非出错返回-1;
    • 如果没出错,则原程序中exec调用之后的代码都不会执行,因为此时原程序已经被exec的参数指定的程序完全替换。

处理僵尸进程

  • 父进程一般需要跟踪子进程的退出状态,当子进程结束运行时,内核不会立即释放该进程的进程表表项,以满足父进程后续对该子进程退出信息的查询;

  • 僵尸进程:父进程没有正确处理子进程的返回信息,子进程将停留在僵尸态,并占据内核资源

    • 子进程结束运行之后,父进程读取其退出状态之前,称该子进程处于僵尸态;
    • 父进程结束或者异常终止,而子进程继续运行,称该子进程处于僵尸态;
  • 处理僵尸进程的函数,在父进程中调用;

    #include<sys/types.h>
    #include<sys/wait.h>
    pid_t wait(int*stat_loc);
    pid_t waitpid(pid_t pid,int*stat_loc,int options);
    
    • wait函数将阻塞进程,直到该进程的某个子进程结束运行为止;
    • waitpid函数只等待由pid参数指定的子进程退出,并回收子进程;
      • options参数可以控制waitpid函数的行为,取值WNOHANG时waitpid调用是非阻塞的
      • 最好在某个子进程退出之后,向父进程发送SIGCHID信号,在父进程中捕获SIGCHLD信号,并在信号处理函数中调用waitpid函数以彻底结束一个子进程。

进程间通信IPC

  • 主要方法:管道、信号量、共享内存、消息队列

管道

  • 管道是进程间通信的常用手段,但只能用于有关联的两个进程间的通信

  • 管道能在父、子进程间传递数据,父进程和子进程必须有一个关闭fd[0],另一个关闭fd[1];

    • 如果要实现父、子进程之间的双向数据传输,就必须使用两个管道;
    • socket编程接口提供了一个创建全双工管道的系统调用:socketpair

信号量

  • 信号量原语

    • 当多个进程同时访问系统上的某个资源时,需要考虑进程的同步问题,以确保任一时刻只有一个进程可以拥有对资源(关键代码段或临界区)的独占式访问

    • 信号量是一种特殊的变量,它只能取自然数值并且只支持两种操作:==P(占有,进入临界区)==和 V(释放,退出临界区)

      P(SV),如果SV的值大于0,就将它减1;如果SV的值为0,则挂起进程的执行;

      V(SV),如果有其他进程因为等待SV而挂起,则唤醒之;如果没有,则将SV加1;

    • Linux信号量的API都定义在sys/sem.h头文件中,主要包含3个系统调用:semgetsemopsemctl

  • semget系统调用

    • 创建一个新的信号量集,或者获取一个已经存在的信号量集

      #include<sys/sem.h>
      int semget(key_t key, int num_sems, int sem_flags);
      
      • key参数是一个键值,用来标识一个全局唯一的信号量集;
      • num_sems参数指定要创建/获取的信号量集中信号量的数目;
      • sem_flags参数指定权限等;
      • 如果semget用于创建信号量集,则与之关联的内核数据结构体semid_ds将被创建并初始化;
  • semop系统调用

    • 与每个信号量关联的一些重要的内核变量

      unsigned short semval;
      /*信号量的值*/
      unsigned short semzcnt;
      /*等待信号量值变为0的进程数量*/
      unsigned short semncnt;
      /*等待信号量值增加的进程数量*/
      pid_t sempid;
      /*最后一次执行semop操作的进程ID*/
      
    • semop系统调用改变信号量的值,即执行P、V操作,实际上就是对以上内核变量的操作;

      int semop(int sem_id,struct sembuf* sem_ops, size_t num_sem_ops);
      
      • sem_id参数是由semget调用返回的信号量集标识符;

      • sem_ops参数指向一个sembuf结构体类型的数组

        struct sembuf{
        //sem_num成员是信号量的编号,sem_op成员指定操作类型,其可选值为正整数、0和负整数
        unsigned short int sem_num; short int sem_op; 
        short int sem_flg; //可选值是IPC_NOWAIT和SEM_UNDO
        }
        
      • semop对数组sem_ops中的每个成员按照数组顺序依次执行原子操作,以避免别的进程在同一时刻按照不同的顺序对该信号集中的信号量执行semop操作导致的竞态条件;

  • semctl系统调用

    • semctl系统调用允许调用者对信号量进行直接控制

      #include<sys/sem.h>
      int semctl(int sem_id, int sem_num, int command,...);
      
      • semctl支持的所有命令

  • 特殊键值IPC_PRIVATE

    • semget的调用者可以给其key参数传递一个特殊的键值IPC_PRIVATE,这样无论该信号量是否已经存在,semget都将创建一个新的信号量

共享内存

  • 共享内存原语

    • 共享内存是最高效的IPC机制,因为它不涉及进程之间的任何数据传输,但必须同步进程对共享内存的访问;
    • Linux共享内存的API都定义在sys/shm.h头文件中,包括4个系统调用:shmgetshmatshmdtshmctl
  • shmget系统调用

    • shmget系统调用创建一段新的共享内存,或者获取一段已经存在的共享内存

      #include<sys/shm.h>
      int shmget(key_t key,size_t size,int shmflg);
      
      • shmget成功时返回一个正整数值,它是共享内存的标识符;
      • key参数是一个键值,用来标识一段全局唯一的共享内存;
      • size参数指定共享内存的大小;
      • shmflg参数的使用和含义与semget系统调用的sem_flags参数相同,支持两个额外的标志:SHM_HUGETLB(使用“大页面”来为共享内存分配空间)和SHM_NORESERVE(不为共享内存保留交换分区);
      • 若shmget用于创建共享内存,则这段共享内存的所有字节都被初始化为0,与之关联的内核数据结构shmid_ds将被创建并初始化;
  • shmat和shmdt系统调用

    • 共享内存被创建/获取之后,需要先将它关联到进程的地址空间中;

    • 使用完共享内存之后需要将它从进程地址空间中分离

      #include<sys/shm.h>
      void*shmat(int shm_id, const void* shm_addr, int shmflg);
      int shmdt(const void* shm_addr);
      
      • shmflg参数:SHM_RND(圆整)、SHM_RDONLY(只读)、SHM_REMAP(重新关联)、SHM_EXEC(执行权限);
  • shmctl系统调用

    • shmctl系统调用控制共享内存的某些属性;

      #include<sys/shm.h>
      int shmctl(int shm_id,int command,struct shmid_ds*buf);
      

  • 共享内存的POSIX方法

    POSIX:可移植操作系统接口(Portable Operating System Interface of UNIX)

    • mmap函数可以实现父、子进程或无关进程之间的内存共享

    • 使用mmap需要先使用如下函数来创建或打开一个POSIX共享内存对象

      #include<sys/mman.h>
      #include<sys/stat.h>
      #include<fcntl.h>
      int shm_open(const char*name,int oflag,mode_t mode);
      
      • shm_open调用成功时返回一个文件描述符。该文件描述符可用于后续的mmap调用,从而将共享内存关联到调用进程,失败时返回-1并设置errno;
    • 由shm_open创建的共享内存对象使用完之后也需要被删除;

      #include<sys/mman.h>
      #include<sys/stat.h>
      #include<fcntl.h>
      int shm_unlink(const char*name);
      

代码清单13-4 使用共享内存的聊天室服务器程序

消息队列

  • 消息队列原语

    • 消息队列是在两个进程之间传递二进制块数据的一种简单有效的方式;
    • 每个数据块都有一个特定的类型,接收方可以根据类型来有选择地接收数据,而不一定像管道和命名管道那样必须以先进先出的方式接收数据;
    • Linux消息队列的API都定义在sys/msg.h头文件中,包括4个系统调用:msggetmsgsndmsgrcvmsgctl
  • msgget系统调用

    • msgget系统调用创建一个消息队列,或者获取一个已有的消息队列

      #include<sys/msg.h>
      int msgget(key_t key,int msgflg);
      
      • msgget成功时返回一个正整数值,它是消息队列的标识符;
      • key参数是一个键值,用来标识一个全局唯一的消息队列;
      • msgflg参数的使用和含义与semget系统调用的sem_flags参数相同;
    • 如果msgget用于创建消息队列,则与之关联的内核数据结构msqid_ds将被创建并初始化

  • msgsnd系统调用

    • msgsnd系统调用把一条消息添加到消息队列中

      #include<sys/msg.h>
      int msgsnd(int msqid,const void*msg_ptr,size_t msg_sz,int msgflg);
      
      • msqid参数是由msgget调用返回的消息队列标识符;

      • msg_ptr参数指向一个准备发送的消息,消息必须被定义为如下类型:

        struct msgbuf
        {
        long mtype;/*消息类型*/
        char mtext[512];/*消息数据*/
        };
        
      • 默认情况下,发送消息时如果消息队列满了,则msgsnd将阻塞;

      • msgflg参数控制msgsnd的行为,它通常仅支持IPC_NOWAIT标志,即以非阻塞的方式发送消息;

    • msgrcv系统调用从消息队列中获取消息

      #include<sys/msg·h>
      int msgrcv(int msqid, void* msg_ptr, size_t msg_sz, long int msgtype, int msgflg);
      
      • msg_ptr参数用于存储接收的消息,msg_sz参数指的是消息数据部分的长度;

      • msgtype参数指定接收何种类型的消息

        • msgtype等于0,读取消息队列中的第一个消息;
        • msgtype小于0,读取消息队列中第一个类型值比msgtype的绝对值小的消息;
        • msgtype大于0,读取消息队列中第一个类型为msgtype的消息;
      • 参数msgflg控制msgrcv函数的行为:IPC_NOWAIT、MSG_EXCEPT、MSG_NOERROR;

    • msgrcv成功时返回0,失败则返回-1并设置errno,msgrcv成功时将修改内核数据结构msqid_ds的部分字段;

  • msgctl系统调用

    • msgctl系统调用控制消息队列的某些属性

      #include<sys/msg.h>
      int msgctl(int msqid,int command,struct msqid_ds*buf);
      

      [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-LburDwG2-1668428072940)(img/Linux高性能服务器编程/image-20221029155521449.png)]

IPC命令

  • 上述3种System V IPC进程间通信方式都使用一个全局唯一的键值(key)来描述一个共享资源;

  • Linux提供了ipcs命令,以观察当前系统上拥有哪些共享资源实例;

  • 可以使用ipcrm命令删除遗留在系统中的共享资源;

在进程间传递文件描述符

  • fork调用之后,父进程中打开的文件描述符在子进程中仍然保持打开,所以文件描述符可以方便地从父进程传递到子进程;
  • 传递一个文件描述符并不是传递一个文件描述符的值,而是要在接收进程中创建一个新的文件描述符,并且这两个文件描述符指向内核中相同的文件表项
  • 可以利用UNIX域socket在进程间传递特殊的辅助数据,以实现文件描述符的传递。

e.g. 代码清单13-5 在进程间传递文件描述符


版权声明:本文为weixin_42659457原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。