Linux 多进程与同步

Linux 多进程与同步

进程与进程间通讯相关八股可以查看面试八股收集这篇笔记。

Linux下的进程

  • main函数的运行是系统的引导程序将程序指针指向了main。

    程序运行需要通过操作系统的加载器来实现,加载器是操作系统中的程序,

    当执行程序时,加载器负责将此应用程序加载内存中去执行。

  • 程序退出的不同方式

    • 正常退出:return、exit、_Exit 、_exit
    • 异常退出:abort函数,收到信号如SIGKILL
  • 为程序的正常退出设置一个回调函数

    • 函数原型

      #include <stdlib.h>
      int atexit(void (*function)(void));
      
    • 代码示例

      static void aexit_callback(void) {
          printf("Exiting...\n");
      }
      int main(void) {
          printf("Hello, World!\n");
          //注册线程退出的回调函数
          if (atexit(aexit_callback)!=0) {
              perror("Failed to register atexit");
          }
          return 0;
      }
      
    • 测试

      /home/joe/Code/Embeed/ApplicationDevelop/MyImplement/MultiProcess/cmake-build-debug/MultiProcess
      Hello, World!
      Exiting...
      

获取进程信息

进程其实就是一个可执行程序的实例。可执行程序是一个静态文件,可以认为是CPP中的类,进程是这个类的实例。

  • 使用getpid来获取进程的id

    
    #include <stdio.h>
    #include <stdlib.h>
    #include <sys/types.h>
    #include <unistd.h>
    
    static void aexit_callback(void) {
        printf("Exiting...\n");
    }
    int main(void) {
        printf("Hello, World!\n");
        printf("PID %d \n",getpid());
        //注册线程退出的回调函数
        if (atexit(aexit_callback)!=0) {
            perror("Failed to register atexit");
        }
        return 0;
    }
    
  • 使用getppid来获取父进程的id

    每个进程都有自己的父进程,也就是启动当前线程的进程。通过getppid来获取其id

    static void aexit_callback(void) {
        printf("Exiting...\n");
    }
    int main(void) {
        printf("Hello, World!\n");
        printf("PID %d \n",getpid());
        printf("Parent PID %d \n",getppid());
        //注册线程退出的回调函数
        if (atexit(aexit_callback)!=0) {
            perror("Failed to register atexit");
        }
        return 0;
    }
    
  • 获取环境变量

    environ 就是一个 char* 数组的指针

    extern char **environ; //环境变量 ,二级指针
    int main(void) {
        printf("Hello, World!\n");
        printf("PID %d \n",getpid());
        printf("Parent PID %d \n",getppid());
        //注册线程退出的回调函数
        if (atexit(aexit_callback)!=0) {
            perror("Failed to register atexit");
        }
        //打印环境变量
        int  i =0;
        while (environ[i]!=NULL && i < 5) {
            i++;
            printf("environ[%d] %s\n",i,environ[i]);
        }
        return 0;
    }
    
    

    获取特定的环境变量

        //获取特定的环境变量
        const char* envContent=NULL;
        envContent=getenv("TERM");
        if (envContent!=NULL) {
            printf("TERM Env is  %s\n",envContent);
        }
    TERM Env is  xterm-256color
    
  • 增删改环境

    • 增加: 新建或修改环境变量

      环境变量的内容是这个字符串而不是字符串的值拷贝。因此,不能修改这个字符串。可以直接使用字符常量来传入

      #include <stdlib.h>
      int putenv(char *string);
      
          putenv("env_Ide=clion");
      
          //尝试获取这个环境变量
          const char *envRead;
          envRead = getenv("env_Ide");
          if (envRead!=NULL) {
              printf("environ %s\n",envRead);
          }
      
      environ clion
      
    • 修改

      setenv()函数可以替代 putenv()函数,用于向进程的环境变量列表中添加一个新的环境变量或修改现有环境变量对应的值。

      更推荐使用setenv,会自动为字符串分配一块内存缓冲区。

      #include <stdlib.h>
      int setenv(const char *name, const char *value, int overwrite); //overwrite用于控制当环境变量存在时是否覆盖
      
          putenv("env_Ide=clion");
      
          //尝试获取这个环境变量
          char *envRead;
          envRead = getenv("env_Ide");
          if (envRead!=NULL) {
              printf("environ %s\n",envRead);
          }
      
          setenv("env_Ide","VScode",1);
          //尝试获取这个环境变量
          envRead = getenv("env_Ide");
          if (envRead!=NULL) {
              printf("environ %s\n",envRead);
          }
      
    • 删除

      使用unsetenv来删除环境变量

          putenv("env_Ide=clion");
      
          //尝试获取这个环境变量
          char *envRead;
          envRead = getenv("env_Ide");
          if (envRead!=NULL) {
              printf("environ %s\n",envRead);
          }
      
          setenv("env_Ide","VScode",1);
          //尝试获取这个环境变量
          envRead = getenv("env_Ide");
          if (envRead!=NULL) {
              printf("environ %s\n",envRead);
          }
      
          unsetenv("env_Ide");
          //尝试获取这个环境变量
          envRead = getenv("env_Ide");
          if (envRead!=NULL) {
              printf("environ %s\n",envRead);
          }
          else {
              printf("Env not exist \n");
          }
      
      
      environ clion
      environ VScode
      Env not exist 
      
    • 清空

          //清空环境变量
          clearenv();
      

进程的内存布局

  • 正文段
  • 初始化数据段
  • 未初始化数据段

image

进程的虚拟地址空间

每个进程都有自己的虚拟地址空间

image

其通过硬件MMU来实现物理地址空间的转换

在Cortex-M系列芯片中,由于其没有MMU,所以无法运行Linux系统。

为什么需要内存映射?

  • 进程隔离:每个进程拥有独立的虚拟地址空间

  • 防止干扰:一个进程无法直接访问另一个进程的内存

  • 权限控制:可设置不同内存区域的读/写/执行权限

  • 按需分配:仅在需要时才分配物理内存页面

  • 高效的内存使用:支持虚拟内存和页面交换

  • 内存碎片化减少:在虚拟地址空间中保持连续性

  • 并发执行:支持多个程序同时运行而不冲突

  • 地址空间复用:相同的程序可加载到相同虚拟地址的不同物理位置

  • 动态链接库:多个程序可共享同一物理内存中的库代码

  • 位置无关代码:代码可加载到任意虚拟地址而无需修改

进程的创建

fork方法

创建一个当前进程的拷贝,父进程会得到子进程的pid,子进程会得到0返回值。

理解 fork()系统调用的关键在于,完成对其调用后将存在两个进程,一个是原进程(父进程)、另一个则是创建出来的子进程,并且每个进程都会从 fork()函数的返回处继续执行,会导致调用 fork()返回两次值,子进程返回一个值、父进程返回一个值。

  • 父子进程共享代码段,在内存中只存在一份代码段(也叫正文段)数据。其他的例如初始化数据段、未初始化数据段、堆和栈都是各自拥有的。子进程的初值来自父进程的拷贝。

  • 对于在 fork 之前 open 的 fd,其可以理解为是一个指针。因此,复制给子进程之后,子进程也掌握着这样一个指针。所以子进程和父进程共享这个 fd 打开的文件和相同的读写偏移量。

  • 但是对于 fork之后打开的文件,父子进程拿到的 fd 就不一样了。所以此时的读写偏移量就是互相独立的。

fork创建父子进程并进行识别
int main(void) {

    pid_t  pid = -1;
    printf("Test output before fork");
    fflush(stdout);//刷新文件缓冲
    pid = fork();
    if (pid != 0) {
        //父进程
        printf("This is Parent with pid: %d,Child is %d \n", getpid(),pid);
        exit(0);
    }
    if (pid == 0) {
        //子进程
        printf("child process with pid: %d ,fork return %d , \n",getpid(),pid);
        _exit(0);
    }
    return 0;
}

运行结果为

Test output before forkThis is Parent with pid: 473135,Child is 473136 
child process with pid: 473136 ,fork return 0 , 

vfork方法

fork 与 vfork 方法在功能上是相同,也是创建子进程,但是问题在于 vfork 不会复制父进程中的数据段和堆栈段中的内容。专门适用于创建子进程后立刻执行 exec 函数来调起其他程序的方法。

但是, 由于 COW(copy on write)技术的引进,可以直接使用 fork了。不推荐使用 vfork了

vfork 会带来未定义的未知的的问题。

进程竞争问题

现在存在一个问题,当创建子进程之后,是父进程还是子进程先行?

  • 当调用 vfork 时,会让子进程先行。

  • 当调用 fork 时,这是未知的不确定的。因此,可以使用信号来完成操作 。

    在 fork 之前先绑定信号的处理函数,然后进行 fork 操作。对于父进程,直接进行挂起操作。而对于子进程,先执行操作。而后向父进程发送信号,唤醒父进程。

  • 程序

    static void aexit_callback(void) {
        printf("Exiting...\n");
    }
    
    static void sig_handler(int sig) {
        printf("Received signal %d\n", sig);
    }
    int main(void) {
    
        pid_t  pid = -1;
        printf("Test output before fork ");
        fflush(stdout);//刷新文件缓冲
        //设置信号响应
        struct sigaction sig={0};
        sigset_t wait_mask;
        sigemptyset(&wait_mask);
    
        sig.sa_flags = 0;
        sig.sa_handler = sig_handler;
        sigaction(SIGUSR1, &sig, NULL);
        pid = fork();
        if (pid != 0) {
    
            //父进程挂起,等待信号。wait_mask是一个空集,代表任何信号都可以唤醒
            if (sigsuspend(&wait_mask) != -1) {
                perror("sigsuspend");
            }
            printf("\nThis is Parent with pid: %d,Child is %d \n", getpid(),pid);
            exit(0);
        }
        if (pid == 0) {
            //子进程
            printf("\n child process with pid: %d ,fork return %d , \n",getpid(),pid);
            kill(getppid(),SIGUSR1);//向父进程发送信号
            _exit(0);
        }
        return 0;
    }
    
  • 执行

    Test output before fork 
     child process with pid: 486506 ,fork return 0 , 
    Received signal 10
    
    This is Parent with pid: 486505,Child is 486506 
    
    进程已结束,退出代码为 0
    

进程的退出

这里主要讲 _exit和exit 的区别。exit 函数是_exit 的封装,其会多做这两件事

  1. 调用已注册的退出回调函数
  2. 刷新 stdio 缓冲区

然后就是照常执行_exit 来退出;

其他的关于正常和异常退出已经讲了,不再赘述。

监视子进程

当创建子进程后,需要监视子进程的状态的变化。

wait函数

wait 函数用于阻塞等待子线程。而且 wait 参数不指定线程 id。

此外,wait 函数可以为子进程收尸,进行资源处理。及时回收成为僵尸进程的子进程。

原型为

#include <sys/types.h>
#include <sys/wait.h>
pid_t wait(int *status);
  • 参数说明

    • 参数 status 用于存放子进程终止时的状态信息,参数 status 可以为 NULL,表示不接收子进程终止时的状态信息。
  • 返回值

    若成功则返回终止的子进程对应的进程号;失败则返回-1。当该进程无子进程,则返回-1 且将 errno 设置为 ECHILD。


通过宏来做转换这个 status。

  • WIFEXITED(status):如果子进程正常终止,则返回 true;

  • WEXITSTATUS(status):返回子进程退出状态,是一个数值,其实就是子进程调用_exit()或 exit()

    时指定的退出状态;

  • WIFSIGNALED(status):如果子进程被信号终止,则返回 true;

    • WTERMSIG(status):返回导致子进程终止的信号编号。如果子进程是被信号所终止,则可以通过此宏获取终止子进程的信号
  • WCOREDUMP(status):如果子进程终止时产生了核心转储文件,则返回 true;


waitpid函数

  • 在 wait 函数的基础上再进一步,用于指定等待的进程。适用于该进程存在多个子进程的情况。

  • 使用 wait()将无法等待某个特定的子进程的完成,只能按照顺序等待下一个子进程的终止,一个一个来、谁先终止就先处理谁。

  • 使用 wait()只能发现那些被终止的子进程,对于子进程因某个信号(譬如 SIGSTOP 信号)而停止

    (注意,这里停止指的暂停运行),或是已停止的子进程收到 SIGCONT 信号后恢复执行的情况就

    无能为力了


引入 waitpid 函数

  • 原型

    #include <sys/types.h>
    #include <sys/wait.h>
    pid_t waitpid(pid_t pid, int *status, int options);
    
  • 参数

    • pid > 0,等待指定的 pid

    • pid = 0 等待与调用进程在同一个进程组的所有子进程

    • pid < -1 等待进程组标识符与-pid 相等的子进程。(进程组

    • pid = -1 等待任意子进程,与 wait 等价。

    • status 与 wait 等价

    • options 一个位掩码,可以设置多种行为:

      • WNOHANG设置非阻塞。可以实现轮询 poll
      • WUNTRACED:除了返回终止的子进程的状态信息外,还返回因信号而停止(暂停运行)的子进程状态信息
      • WCONTINUED:返回那些因收到 SIGCONT 信号而恢复运行的子进程的状态信息
  • 返回值

    返回值与 wait()函数的返回值意义基本相同,在参数 options 包含了 WNOHANG 标志的情况

    下,返回值会出现 0

  • 示例

    先运行,然后fork创建子程序,让子程序等一会再退出。父进程继续干活,然后轮询。

    int main(void) {
    
        pid_t  pid = -1;
        fflush(stdout);//刷新文件缓冲
    
        pid = fork();
        if (pid != 0) {
            //父进程
    
            printf("Parent waiting \n ");
            //在这里 waitpid
            int ret = 0;
            while (1) {
                ret = waitpid(pid,NULL,WNOHANG);
                if (ret == 0) {
                    //对应进程正在运行,
                    printf("waiting dead Child  \n "); //这一句是会被运行的
                    sleep(1);
                }
                else if (ret < 0 ) {
                    perror("waitpid");
                    exit(1);
                }
                else {
                    //来活了
                    printf("Child %d  Exited\n", ret);
                    break;
                }
            }
            exit(0);
        }
        if (pid == 0) {
            //子进程
            sleep(5);
            _exit(0);
        }
        return 0;
    }
    
  • 测试

    Parent waiting 
     waiting dead Child  
     waiting dead Child  
     waiting dead Child  
     waiting dead Child  
     waiting dead Child  
     Child 503440  Exited
    
    

僵尸进程与孤儿进程

  • 当父进程先于子进程结束,则子进程则会变成孤儿进程,其进程归属会自动被切换给其他进程。在无图形界面下,是 init 进程(pid1)。

  • 僵尸进程

    僵尸进程指的是,子进程先于父进程退出,此时需要还有一个资源等待被回收。回收操作需要由父进程来完成。父进程通过调用 wait 操作来实现对子进程资源的回收释放。


系统资源是有限的,因此不能放任僵尸进程不管。要主动进行僵尸进程的管理,调用 wait 来轮询的话,会出现高频占用。对于这种场景,使用信号是最理想的。

使用SIGCHLD 信号来实现子进程的管理操作。

  • 发送的时机

    • 当父进程的某个子进程终止时,父进程会收到 SIGCHLD 信号
    • 当父进程的某个子进程因收到信号而停止(暂停运行)或恢复时,内核也可能向父进程发送该信号。
  • 默认情况下,对于SIGCHLD是忽略的,因此,我们需要进行捕获。

    • 存在问题:由于SIGCHLD是不可靠信号且存在掩码机制。当 SIGCHLD 信号处理函数正在为一个终止的子进程“收尸”时,如果相继有两个子进程终止,即使产生了两次 SIGCHLD 信号,父进程也只能捕获到一次 SIGCHLD 信号。
    • 解决方案: 在函数中循环以非阻塞方式调用 waitpid,确保无子进程需要处理。
  • 示例代码

    static void childHandler(int sig) {
        if (sig == SIGCHLD) {
            printf("Received SIGCHLD\n");
            exit(0);
        }
    }
    int main(void) {
    
        pid_t  pid = -1;
        fflush(stdout);//刷新文件缓冲
        struct  sigaction  sig={0}  ;
        sig.sa_handler = childHandler;
        sig.sa_flags = 0;
        sigaction(SIGCHLD,&sig,NULL);//注册
        pid = fork();
        if (pid != 0) {
            //父进程
            int timeCnt = 0;
            while (++timeCnt < 10) {
                sleep(1);
                printf("Time %d \n",timeCnt);
            }
        }
        if (pid == 0) {
            //子进程
            printf("Child process running...\n");
            sleep(5);
            _exit(0);
        }
        return 0;
    }
    
  • 测试

    Child process running...
    Time 1 
    Time 2 
    Time 3 
    Time 4 
    Received SIGCHLD
    
    进程已结束,退出代码为 0
    

执行新程序

在创建子线程后,用于启动另一个进程,而不是实现原本的程序。

execve函数

将外部可执行文件加载到进程的内存空间运行。使用新的程序来替换旧的程序。

  • 原型

    #include <unistd.h>
    int execve(const char *filename, char *const argv[], char *const envp[])
    
  • 参数

    • filename:

      参数 filename 指向需要载入当前进程空间的新程序的路径名,既可以是绝对路径、也可以是

      相对路径

    • argv:参数 argv 则指定了传递给新程序的命令行参数。是一个字符串数组,该数组对应于 main(int argc,char *argv[])函数的第二个参数 argv

    • envp:参数 envp 也是一个字符串指针数组,指定了新程序的环境变量列表

  • 返回值

    返回值:execve 调用成功将不会返回;失败将返回-1,并设置 errno

  • 示例

    • 父进程

      int main(void) {
      
          pid_t  pid = -1;
          fflush(stdout);//刷新文件缓冲
          struct  sigaction  sig={0}  ;
          sig.sa_handler = childHandler;
          sig.sa_flags = 0;
          sigaction(SIGCHLD,&sig,NULL);//注册
          pid = fork();
          if (pid != 0) {
              //父进程
              int timeCnt = 0;
              while (++timeCnt < 10) {
                  sleep(1);
                  printf("Time %d \n",timeCnt);
              }
          }
          if (pid == 0) {
              //子进程
              printf("Child process running...\n");
              char* args[3]={"1","2",NULL};
              //使用 execve函数来运行新的进程
              if (execve("./testApp",args,NULL)==-1) {
                  perror("execve failed");
              }
              _exit(0);
          }
          return 0;
      }
      
    • 子进程启动的应用

      int main(int argc, char** argv) {
          printf("Execve Child process running...\n");
          if (argc == 2) {
              printf("Received Param : %s and %s \n",argv[0],argv[1]);
          }
          else {
              printf("Param num not correct ,expected 2, got %d \n",argc);
          }
          exit(0);
      }
      
      
  • 测试

    Child process running...
    Execve Child process running...
    Received Param : 1 and 2 
    Received SIGCHLD
    

这里引入了一个新的知识点: 运行程序时。程序接收到的参数的个数的问题

  • 通过 exec 族函数实现运行

    char* args[3]={"1","2",NULL}; 
    if (execve("./testApp",args,NULL)==-1) {
                perror("execve failed");
            }
    

    此时传入的参数的数量就是args中有效值的个数,也就是 2 个

  • 通过终端进行运行的时候,会多出来一个参数.

    int main(int argc, char** argv) {
        printf("Execve Child process running...\n");
        if (argc == 2) {
            printf("Received Param : %s and %s \n",argv[0],argv[1]);
        }
        else {
            printf("Param num not correct ,expected 2, got %d \n",argc);
        }
        exit(0);
    }
    

    shell 会自动将 程序名作为第一个参数 (argv[0]) 添加

     ./testApp  1 
    

    实际传入的是 2 个参数,

    Received Param : ./testApp and 1 
    

exec 族函数

有许多类似于 execve 的库函数

#include <unistd.h>
extern char **environ;
int execl(const char *path, const char *arg, ... /* (char *) NULL */);
int execlp(const char *file, const char *arg, ... /* (char *) NULL */);
int execle(const char *path, const char *arg, ... /*, (char *) NULL, char * const envp[] */);
int execv(const char *path, char *const argv[]);
int execvp(const char *file, char *const argv[]);
int execvpe(const char *file, char *const argv[], char *const envp[]);
  • 区别

    • l = list(参数列表,可变参数)

    • v = vector(参数数组)

    • p = PATH(在环境变量PATH中搜索可执行文件)

    • e = environment(显式指定环境变量)

    • execve 和 execl 基本一致,其可接收参数依次排列

      char *arg_arr[5];
      arg_arr[0] = "./newApp";
      arg_arr[1] = "Hello";
      arg_arr[2] = "World";
      arg_arr[3] = NULL;
      execv("./newApp", arg_arr);
      
      execl("./newApp", "./newApp", "Hello", "World", NULL);
      
    • execlp和execvp,在原基础加了一个 p,代表 path。

      execl()和execv()要求提供新程序的路径名,而 execlp()和 execvp()则允许只提供新程序文件名,其会自动在 PATH 路径下进行搜索。可用于执行标准的Linux 命令

    • execle()和 execvpe()这两个函数在命名上加了一个 e,这个 e 其实表示的是 environment 环境变量。着这两个函数可以指定自定义的环境变量列表给新程序,参数envp与系统调用execve()的envp参数相同,也是字符串指针数组。

      用 null 作为截断的区分

      // execvpe 传参
      char *env_arr[5] = {"NAME=app", "AGE=25",
      "SEX=man", NULL};
      char *arg_arr[5];
      arg_arr[0] = "./newApp";
      arg_arr[1] = "Hello";
      arg_arr[2] = "World";
      arg_arr[3] = NULL;
      execvpe("./newApp", arg_arr, env_arr);
      // execle 传参
      execle("./newApp", "./newApp", "Hello", "World", NULL, env_arr);//用 null 作为截断的区分
      
  • 示例

    //===========================================
            // exec 族函数 - 实际使用时请只保留一个
            //===========================================
    
            //1. execve - 系统调用(参数数组 + 环境变量)
            if (execve("/bin/ls", args, NULL) == -1) {
                perror("execve failed");
            }
    
            //2. execl - 可变参数列表(参数列表 + 完整路径)
            if (execl("/bin/ls", "ls", "-l", "-a", NULL) == -1) {
                perror("execl failed");
            }
    
            //3. execv - 参数数组(完整路径)
            if (execv("/bin/ls", args) == -1) {
                perror("execv failed");
            }
    
            //4. execlp - 可变参数列表(PATH中搜索)
            if (execlp("ls", "ls", "-l", "-a", NULL) == -1) {
                perror("execlp failed");
            }
    
            //5. execvp - 参数数组(PATH中搜索)
            if (execvp("ls", args) == -1) {
                perror("execvp failed");
            }
    
            //6. execle - 可变参数列表(完整路径 + 环境变量)
            if (execle("/bin/ls", "ls", "-l", "-a", NULL, NULL) == -1) {
                perror("execle failed");
            }
    
            //7. execvpe - 参数数组(PATH中搜索 + 环境变量,GNU扩展)
            if (execvpe("ls", args, NULL) == -1) {
                perror("execvpe failed");
            }
    
            // 默认使用 execvp 示例
            if (execvp("ls", args) == -1) {
                perror("execvp failed");
            }
    
  • 输出

    总计 100
    drwxrwxr-x 5 joe joe  4096 1111 18:57 .
    drwxrwxr-x 5 joe joe  4096 1111 18:56 ..
    -rw-rw-r-- 1 joe joe 12118 1111 13:45 build.ninja
    drwxrwxr-x 3 joe joe  4096 1110 23:24 .cmake
    -rw-rw-r-- 1 joe joe 13414 1110 23:24 CMakeCache.txt
    drwxrwxr-x 5 joe joe  4096 1111 13:45 CMakeFiles
    -rw-rw-r-- 1 joe joe  2238 1111 13:45 cmake_install.cmake
    -rw-rw-r-- 1 joe joe   130 1110 23:24 .gitignore
    -rwxrwxr-x 1 joe joe 21608 1111 18:57 MultiProcess
    -rw-rw-r-- 1 joe joe 13228 1111 18:57 .ninja_deps
    -rw-rw-r-- 1 joe joe  2389 1111 18:57 .ninja_log
    drwxrwxr-x 3 joe joe  4096 1110 23:24 Testing
    

system函数

使用 system()函数可以很方便地在我们的程序当中执行任意 shell 命令。

  • 原型

    #include <stdlib.h>
    int system(const char *command);
    

其内部是通过调用 fork()、execl()以及 waitpid()这三个函数来实现它的功能,首先 system()会调用 fork()创建一个子进程来运行 shell(可以把这个子进程成为 shell 进程),并通过 shell 执行参数command。

因此, system 函数会启动至少两个进程,一个是 shell 进程,一个是 shell 中执行的进程。比 exec 函数会造成更大的开销

  • 使用

    system("ls -la")
    

进程状态与关系

进程关系

在 Linxu 下存在进程树,每个进程有自己的父进程,父进程也有其父进程。构成一个进程树,树的根节点是 init 进程,其 pid 为 1.

进程间关系有三种:

  1. 无关系
  2. 父子
  3. 同个进程组

  • 关于进程组:

    • 每个进程必定属于某一个进程组、且只能属于一个进程组
    • 每一个进程组有一个组长进程,组长进程的 ID 就等于进程组 ID;
    • 在组长进程的 ID 前面加上一个负号即是操作进程组,例如 waitpid函数中的 pid 参数
    • 组长进程不能再创建新的进程组;
    • 只要进程组中还存在一个进程,则该进程组就存在,这与其组长进程是否终止无关
    • 一个进程组可以包含一个或多个进程,进程组的生命周期从被创建开始,到其内所有进程终止或离开该进程组;
  • 获取进程组id

    #include <unistd.h>
    pid_t getpgid(pid_t pid); //返回指定 pid 所在的进程的进程组 id
    pid_t getpgrp(void); //返回当前所在组的组 id
    
  • 设置进程组 id

    一个进程只能为它自己或它的子进程设置进程组 ID,在它的子进程调用 exec 函数后,它就不能更改该子进程的进程组 ID

    #include <unistd.h>
    int setpgid(pid_t pid, pid_t pgid);//设置指定进程所在的进程组。若参数相等则设置其为一个新组的组长进程。
    int setpgrp(void);//setpgrp()函数等价于 setpgid(0, 0)。设置自己当前 id 和当前 id 为组长的组
    

可以存在多个进程组,如下所示

image


  • 会话

    一个会话可包含一个或多个进程组,但只能有一个前台进程组,其它的是后台进程组;每个会话都有一个会话首领(leader) ,即创建会话的进程。上图即是一个会话。

    会话的首领进程连接一个终端之后,该终端就成为会话的控制终端,与控制终端建立连接的会话首领进程被称为控制进程。

    产生在终端上的输入和信号将发送给会话的前台进程组中的所有进程

  • 会话 ID

    会话的首领进程的进程组 ID 将作为该会话的标识,也就是会话 ID(sid)。

    新创建的进程会继承父进程的会话 ID。通过系统调用 getsid()可以获取进程的会话 ID

  • 获取会话 id

    #include <unistd.h>
    pid_t getsid(pid_t pid);
    
  • 设置会话 id 来创建一个会话

    #include <unistd.h>
    pid_t setsid(void);
    

    如果调用者进程不是进程组的组长进程,调用 setsid()将创建一个新的会话,调用者进程是新会话的首领进程,同样也是一个新的进程组的组长进程,调用 setsid()创建的会话将没有控制终端。

实现守护进程

守护进程(Daemon)也称为精灵进程,是运行在后台的一种特殊进程,它独立于控制终端并且周期性地执行某种任务或等待处理某些事情的发生。

  • 创建守护进程

    1. 创建子进程,终止父进程

      父进程调用 fork()创建子进程,然后父进程使用 exit()退出。

    2. 子进程调用 setsid 创建会话

      由于子进程肯定不是进程组的组长进程,其调用后会创建新的会话,以及成为一个新的进程组的组长进程。

      这里调用 setsid 有三个作用:让子进程摆脱原会话的控制、让子进程摆脱原进程组的控制和让子进程摆脱原控制终端的控制。

    3. 将工作目录更改为根目录

      子进程是继承了父进程的当前工作目录,由于在进程运行中,当前目录所在的文件系统是不能卸载的,这对以后使用会造成很多的麻烦。因此通常的做法是让“/”作为守护进程的当前目录

    4. 重设文件权限掩码 umask

      文件权限掩码 umask 用于对新建文件的权限位进行屏蔽。默认的文件掩码继承自父进程,需要自己重设

    5. 关闭不再需要的文件描述符

    6. 将文件描述符号为 0、1、2 定位到/dev/null

    7. 其它:忽略 SIGCHLD 信号。

      将 SIGCHLD 信号的处理方式设置为SIG_IGN,也就是忽略该信号,可让内核将僵尸进程转交给 init 进程去处理

  • 代码

    #include <stdio.h>
    #include <stdlib.h>
    #include <unistd.h>
    #include <sys/types.h>
    #include <sys/stat.h>
    #include <fcntl.h>
    #include <signal.h>
    
    
    extern char **environ; //环境变量 ,二级指针
    
    static void aexit_callback(void) {
        printf("Exiting...\n");
    }
    
    static void sig_handler(int sig) {
        printf("Received signal %d\n", sig);
    }
    
    static void childHandler(int sig) {
        if (sig == SIGCHLD) {
            printf("Received SIGCHLD\n");
            exit(0);
        }
    }
    int main(void) {
    
        pid_t  pid = -1;
        fflush(stdout);//刷新文件缓冲
        struct  sigaction  sig={0}  ;
        sig.sa_handler = childHandler;
        sig.sa_flags = 0;
        // sigaction(SIGCHLD,&sig,NULL);//注册
        pid = fork();
        if (pid != 0) {
            //父进程
            exit(0);
        }
        if (pid == 0) {
            //子进程
            // 启动守护进程
            if (setsid()<0) {
                perror("setsid");
            }
    
            if (chdir("/")<0) {
                perror("chdir");
            }
    
            umask(0);
    
            //关闭文件描述符
            for (int i = 0;i<sysconf(_SC_OPEN_MAX);i++) {
                close(i);
            }
    
            open("dev/null",O_RDWR);
            dup(0);
            dup(0);
    
            //忽略 CHILD
            signal(SIGCHLD,SIG_IGN);
    
    
            //开始进入守护进程干活
    
            _exit(0);
        }
        return 0;
    }
    

当会话即将退出时,系统会发送 SIGHUP信号给所有子进程,子进程收到会就会退出,会话终止。

实现单例模式

通常情况下,一个程序可以被多次执行,即程序在还没有结束的情况下,又再次执行该程序,也就是系统中同时存在多个该程序的实例化对象(进程)。


方法一: 通过文件存在与否进行判断,将文件创建在 tmp 目录下

代码中以 O_RDONLY | O_CREAT | O_EXCL 的方式打开文件,如果文件不存在则创建文件,如果文件

存在则 open 会报错返回-1;使用 atexit 注册进程终止处理函数,当程序退出时,使用 remove()删除该文件。


方法二:使用文件锁的方式,最常用的方式

当程序启动之后,首先打开该文件,调用 open 时一般使用O_WRONLY | O_CREAT 标志。

而后尝试去获取文件锁,若是成功,则将程序的进程号(PID)写入到该文件中,写入后不要关闭文件或解锁(释放文件锁),保证进程一直持有该文件锁;若是程序获取锁失败,代表程序已经被运行、则退出本次启动

需要使用flock方法来进行上锁。

这部分在高级 IO 中会接触到。

进程状态

进程处在以下几种状态中

image

  • 可中断睡眠状态:可中断睡眠也称为浅度睡眠,表示睡的不够“死”,还可以被唤醒,一般来说可以通过信号来唤醒;深度睡眠无法被信号唤醒,只能等待相应的条件成立才能结束睡眠状态。合称为等待态或者阻塞态

  • 涉及到了 Linux 中的调度算法: 完全公平调度算法(Completely Fair Scheduler, CFS)Linux 的 CFS 调度算法是通过复杂的加权机制来实现调度,力求在响应性、吞吐量和相对公平之间取得平衡,而不是简单的平均分配。

进程间通讯方式

参考进程间通讯这个笔记的内容

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值