Linux进程

一.进程关键概念

  1. 什么是程序,什么是进程,有什么区别?

    • 程序是静态的概念,gcc xxx.c -o pro

      磁盘中生成pro文件,叫做程序。

    • 进程是程序的一次运行活动,通俗点意思是程序跑起来了,系统中就多了一个进程。

  2. 如何查看系统中有哪些进程?

    • 适用ps指令查看

      实际工作中,配合grep来查找程序中是否存在某一个进程。

    • 使用top指令查看,类似windows任务管理器

  3. 什么是进程标识符?

    • 每个进程都有一个非负整数表示的唯一ID。叫做PID,类似身份证。

    • PID=0:称为交换进程(swapper);作用:进程调度

      PID=1:init进程;作用:系统初始化

    • 编程调用getpid函数获取自身的进程标识符,getppid获取父进程的进程标识符。

  4. 什么叫父进程,什么叫子进程?

    进程A创建了进程B,那么A叫做父进程,B叫做子进程,父子进程是相对的概念,理解为人类中的父子关系。

  5. C程序的存储空间是如何分配?

    在这里插入图片描述

    在这里插入图片描述

二.进程创建实战

  1. 使用fork函数创建一个进程:pid_t fork(void);

    • fork是复制进程的函数,程序一开始就会产生一个进程,当这个进程(代码)执行到fork()时,forK就会复制一份原来的进程即就是创建一个新进程,我们称子进程,而原来的进程我们称为父进程,此时父子进程是共存的,他们一起向下执行代码。
    • 就是调用fork函数之后,一定是两个进程同时执行fork函数之后的代码,而之前的代码以及由父进程执行完毕。

    • fork的返回值问题:

      在父进程中,fork返回新创建子进程的进程ID;

      在子进程中,fork返回0;

      如果出现错误,fork返回一个负值;

      getppid():得到一个进程的父进程的PID;

      getpid():得到当前进程的PID;

      **注意:**在fork函数执行完毕后,如果创建新进程成功,则出现两个进程,一个是子进程,一个是父进程。在子进程中,fork函数返回0,在父进程中,fork返回新创建子进程的进程ID。我们可以通过fork返回的值来判断当前进程是子进程还是父进程。

  2. 实例

    #include <stdio.h>
    #include <stdlib.h>
    #include <unistd.h>
    /**
     *最基础的fork例子
     **/
    int main(int argc, char const *argv[])
    {
        pid_t pid;
        //判断1
        if ((pid=fork()) < 0)
        {
            perror("fork error");
        }
        //判断2
        else if (pid == 0)//子进程
        {
             printf("child getpid()=%d\n", getpid());
        }
        //判断3
        else if(pid > 0)//父进程
        {
            printf("parent getpid()=%d\n", getpid());
        }
     
        return 0;
    }
    

    解析:

    两个判断的代码都执行了,这是非常不可思议的,但fork函数确实实现了这样的功能。也就是在fork函数后面的代码都会执行2遍。

    这就是为什么两个判断都会被执行的原因。在来梳理-下成功fork的执行流程

    第一步: pid=fork(),如果成功那么pid就有一个非0正值。否则返回-1。

    第二步: 因为pid>0,所以进入判断3。这是在父进程。

    第三步: 父进程的代码执行完了,程序又会把fork后面的函数再执行一遍,此时pid的值变为0,所以进入判断2。

  3. 写时拷贝

    在这里插入图片描述

    参考链接:http://t.csdnimg.cn/XmPsq

  4. fork创建一个子进程的一般目的

    一个父进程希望复制自己,使父、子进程同时执行不同的代码段。这在网络服务进程中是常见的——父进程等待客户端的服务请求。当这种请求到达时,父进程调用fork,使子进程处理此请求。父进程则继续等待下一个服务请求到达。

    #include <stdio.h>
    #include <stdlib.h>
    #include <unistd.h>
    int main(int argc, char const *argv[])
    {
             int data;
             pid_t pid;
             while(1){//用while(1)来一直请求
                    printf("please input data:\n");
                    scanf("%d",&data);
                    if(data == 1){//用户请求满足
                            pid = fork();
                            if(pid > 0){//parent process父进程
    
                            }
                            if(pid == 0){//subprocess子进程
                                    while(1){
                                            printf("connect success!pid = %d\n",getpid());
                                            sleep(3);
                                    }
                            }
                    }else{
                            printf("wait,do nothing!\n");
                    }
             }
             return 0;
    }
    
  5. vfork函数

    • vfork和fork的区别

      1. fork() 子进程拷贝父进程的数据段,代码段;vfork() 子进程与父进程共享数据段。
      2. fork() 父子进程的执行次序不确定;vfork():保证子进程先运行,当子进程调用exit退出后,父进程才 执行。
    • 实例:

      #include <stdio.h>
      #include <stdlib.h>
      #include <unistd.h>
      int main(int argc, char const *argv[])
      {
              pid_t pid;
              pid = vfork();
              int cnt;
              if(pid > 0){//parent process
                      while(1){
                              printf("cnt = %d\n",cnt);
                              printf("this is parent process,pid = %d\n",getpid());
                              sleep(1);
      
                      }
              }
              if(pid == 0){//subprocess
                      while(1){
                              printf("this is subprocess,pid = %d\n",getpid());
                              sleep(1);
                              cnt++;
                              if(cnt == 3){
                                      exit(0);
                              }
                      }
              }
               return 0;
      }
      

      在这里插入图片描述

      子进程执行3次退出后,父进程才开始执行,且因为共享数据,cnt的值被改变。

三.退出

  1. 退出方式

    • 正常退出

      1. main()函数调用return。
      2. 进程调用exit(),标准C库。
      3. 进程调用_exit()或者_Exit(),属于系统调用。
      4. 进程最后一个线程返回,最后一线程调用pthread_exit。
    • 异常退出

      1. 调用abort。
      2. 当进程收到某些信号时,如Ctrl+C。
      3. 最后一个线程对取消(cancellation)请求做出响应。

    在这里插入图片描述

  2. 为什么要等待子进程退出

    • 防止僵尸进程,造成内存泄漏。
    • 父进程要管理子进程,所以父进程交代给子进程的任务完成的如何,都需要知道,如,子进程运行完成,运行结果对还是不对,或者是否正常退出。
    • 父进程通过进程等待的方式,回收子进程资源,获取子进程退出信息。
  3. 僵尸进程

    如果子进程的退出状态不被收集,子进程会变成僵死进程(僵尸进程)。

    #include <stdio.h>
    #include <stdlib.h>
    #include <unistd.h>
    int main(int argc, char const *argv[])
    {
            pid_t pid;
            pid = vfork();
            int cnt;
            if(pid > 0){//parent process
                    while(1){
                            printf("cnt = %d\n",cnt);
                            printf("this is parent process,pid = %d\n",getpid());
                            sleep(1);
                    }//父进程中没有收集子进程状态的函数
            }
            if(pid == 0){//subprocess
                    while(1){
                            printf("this is subprocess,pid = %d\n",getpid());
                            sleep(1);
                            cnt++;
                            if(cnt == 3){
                                    exit(0);
                            }
                    }
            }
             return 0;
    }
    

    在这里插入图片描述

    子进程pid为49017。

    在这里插入图片描述

    Z是单词(zombie)的缩写,意思是僵尸。

  4. 收集子进程退出信息的函数wait和waitpid

    • wait函数

      #include <sys/types.h>
      #include <sys/wait.h>
      pid_t wait(int *status);
      
    • status:是一个整型数指针。

      指针为空:不关心退出状态;

      指针非空:子进程退出状态放在它所指向的地址中。

    • 函数作用:父进程一旦调用了wait就立即阻塞自己,由wait自动分析是否当前进程的某个子进程已经退出,如果让它找到了这样一个已经变成僵尸的子进程,wait就会收集这个子进程的信息,并把它彻底销毁后返回;如果没有找到这样一个子进程,wait就会一直阻塞在这里,直到有一个出现为止。

    • wait()要与fork()配套出现,如果在使用fork()之前调用wait(),程序出错时wait()的返回值则为-1,正常情况下wait()的返回值为子进程的PID。

    • wait(status),status非空。

      #include <stdio.h>
      #include <stdlib.h>
      #include <unistd.h>
      int main(int argc, char const *argv[])
      {
              pid_t pid;
              pid = vfork();
              int cnt;
              int status = 0;
              if(pid > 0){//parent process
                      while(1){
                              wait(&status);
                              printf("child process exit data = %d\n",WEXITSTATUS(status));
                              //status的输出需要系统定义的宏WEXITSTATUS来解码
                              printf("cnt = %d\n",cnt);
                              printf("this is parent process,pid = %d\n",getpid());
                              sleep(1);
      
                      }
              }
              if(pid == 0){//subprocess
                      while(1){
                              printf("this is subprocess,pid = %d\n",getpid());
                              sleep(1);
                              cnt++;
                              if(cnt == 3){
                                      exit(0);
                              }
                      }
              }
      
               return 0;
      }
      

      在这里插入图片描述

      得到exit(0);的返回值0。

      在这里插入图片描述

      且不存在子进程的僵尸进程。

    • 关于退出返回值的一些宏

      在这里插入图片描述

  5. waitpid

    • 头文件和函数体

      #include <sys/types.h>
      #include <sys/wait.h>
      pid_t waitpid(pid_t pid, int *status, int options);
      int waitid(idtype_t idtype, id_t id, siginfo_t *infop, int options);
      
    • 从本质上讲,系统调用waitpid和wait的作用是完全相同的,但waitpid多出了两个可由用户控制的参数pid和options,wait使调用者阻塞,waitpid 可以使调用者不阻塞。

    • PID:

      pid > 0时,只等待进程ID等于pid的子进程,不管其它已经有多少子进程运行结束退出了,只要指定的子进程还没有结束,waitpid就会一直等下去。

      pid == -1时,等待任何一个子进程退出,没有任何限制,此时waitpid和wait的作用一模一样。

      pid == 0时,等待同一个进程组中的任何子进程,如果子进程已经加入了别的进程组,waitpid不会对它做任何理睬。

      pid < -1时,等待一个指定进程组中的任何子进程,这个进程组的ID等于pid的绝对值。

      **status:**是一个整型数指针,和wait一样。

      **options:**options提供了一些额外的选项来控制waitpid。

        waitpid(pid,&status,WNOHANG);
      
  6. 孤儿进程

    • 父进程如果不等待子进程退出,在子进程之前就结束了自己的“生命”,此时子进程叫做孤儿进程。
      Linux为了避免孤儿进程过多,init进程收留孤儿进程,变成孤儿进程的父进程(init进程为系统初始化进程,进程ID为1)。

    • 实例

      #include <stdio.h>
      #include <stdlib.h>
      #include <unistd.h>
      int main(int argc, char const *argv[])
      {
              pid_t pid;
              pid = fork();
              int cnt;
              int status = 0;
              if(pid > 0){//parent process
                      printf("this is parent process,pid = %d\n",getpid());
                      sleep(2);
              }
              if(pid == 0){//child process
                      while(1){
                              printf("this is child process,pid = %d",getpid());
                              printf("my father pid = %d\n",getppid());
                              sleep(1);
                              cnt++;
                              if(cnt == 3){
                                      exit(0);
                              }
                      }
              }
      
               return 0;
      }
      

      在这里插入图片描述

      开始子进程的父进程pid是50348,之后父进程结束,子进程继续运行,变成孤儿进程,被init收留,此时父进程的pid为1。

      在这里插入图片描述

      孤儿进程运行状态为S(后台运行),Ctrl+C不能退出,需要kill -9 PID来推出孤儿进程。

四.exec族函数

  1. exec族函数

    • **功能:**我们用fork函数创建新进程后,经常会在新进程中调用exec函数去执行另外一个程序。当进程调用exec函数时,该进程被完全替换为新程序。因为调用exec函数并不创建新进程,所以前后进程的ID并没有改变。

      在调用进程内部执行一个可执行文件。可执行文件既可以是二进制文件,也可以是任何Linux下可执行的脚本文件。

    • **函数族:**exec函数族分别是:execl, execlp, execle, execv, execvp, execvpe。

    • 函数原型:

      #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 execvpe(const char *file, char *const argv[],char *const envp[]);
      
    • 返回值:

      exec函数族的函数执行成功后不会返回,调用失败时,会设置errno并返回-1,然后从原程序的调用点接着往下执行。

    • 参数说明:

      path:可执行文件的路径名字

      arg:可执行程序所带的参数,第一个参数为可执行文件名字,没有带路径且arg必须以NULL结束

      file:如果参数file中包含/,则就将其视为路径名,否则就按 PATH环境变量,在它所指定的各目录中搜寻可执行文件。

      exec族函数参数极难记忆和分辨,函数名中的字符会给我们一些帮助:

      l : 使用参数列表

      p:使用文件名,并从PATH环境进行寻找可执行文件

      v:应先构造一个指向各参数的指针数组,然后将该数组的地址作为这些函数的参数。

      e:多了envp[]数组,使用新的环境变量代替调用进程的环境变量

  2. 带l的一类exac函数

    • 带l的一类exac函数(l表示list),包括execl、execlp、execle,要求将新程序的每个命令行参数都说明为 一个单独的参数。这种参数表以空指针结尾。

    • 实例:

      //文件echoarg.c
      #include <stdio.h>
      
      int main(int argc,char *argv[])
      {
          int i = 0;
          for(i = 0; i < argc; i++)
          {
              printf("argv[%d]: %s\n",i,argv[i]); 
          }
          return 0;
      }
      
      //文件execl.c
      #include <stdio.h>
      #include <stdlib.h>
      #include <unistd.h>
      //函数原型:int execl(const char *path, const char *arg, ...);
      
      int main(void)
      {
          printf("before execl\n");
          if(execl("./echoarg","echoarg","abc",NULL) == -1)
          {
              printf("execl failed!\n");   
              perror("why");   
          }
          printf("after execl\n");
          return 0;
      }
      

      执行结果:

      在这里插入图片描述

      其他示例:

      调用ls:
      execl("/bin/ls","ls",NULL);
      调用ls -l:
      execl("/bin/ls","ls","-l",NULL);
      获取系统时间:
      execl("/bin/date","date",NULL);
      
  3. 带p的一类exac函数

    • 带p的一类exac函数,包括execlp、execvp、execvpe,如果参数file中包含/,则就将其视为路径名,否则就按 PATH环境变量,在它所指定的各目录中搜寻可执行文件。举个例子,PATH=/bin:/usr/bin。

    • 示例:

      execl("/bin/date","date",NULL);
      等价于:
      execlp("date","date",NULL);
      
  4. 带v不带l的一类exac函数

    • 带v不带l的一类exac函数,包括execv、execvp、execve,应先构造一个指向各参数的指针数组,然后将该数组的地址作为这些函数的参数。 如char *arg[]这种形式,且arg最后一个元素必须是NULL,例如char *arg[] = {“ls”,”-l”,NULL}; 。

    • 示例:

      char *argv[] = {"ps","-l",NULL};
      execv("/bin/ps",argv);
      等价于:
      execvp("ps",argv);
      
  5. system函数

    • system()会调用fork()产生子进程,由子进程来调用/bin/sh-c string来执行参数string字符串所代表的命令,此命令执行完后随即返回原调用的进程。在调用system()期间SIGCHLD 信号会被暂时搁置,SIGINT和SIGQUIT 信号则会被忽略。

    • system源码

      #include <sys/types.h>
      #include <sys/wait.h>
      #include <errno.h>
      #include <unistd.h>
      
      int system(const char * cmdstring)
      {
          pid_t pid;
          int status;
      
          if(cmdstring == NULL){
                 
               return (1);
          }
      
          if((pid = fork())<0){
      
                  status = -1;
          }else if(pid = 0){
      
              execl("/bin/sh", "sh", "-c", cmdstring, (char *)0);
      
              -exit(127); //子进程正常执行则不会执行此语句
      
          }else{
                  while(waitpid(pid, &status, 0) < 0){
      
                      if(errno != EINTER){
      
                          status = -1;
      
                          break;
      
                      }
                  }
              }
              return status;
      }
      
    • 有system源码可知,system其实是简单粗暴的exec;system是exec的封装版。

    • system和exec的区别:

      1. system()和exec()都可以执行进程外的命令,system是在原进程上开辟了一个新的进程,但是exec是用新进程(命令)覆盖了原有的进程。
      2. system()和exec()都有能产生返回值,system的返回值并不影响原有进程,但是exec的返回值影响了原进程。
  6. popen函数

    • 函数原型

      #include <stdio.h>   
      FILE *popen(const char *command, const char *type);
      
    • **返回值:**如果调用成功,则返回一个读或者打开文件的指针,如果失败,返回NULL,具体错误要根据errno判断。

    • **函数作用:**popen() 函数用于创建一个管道:其内部实现为调用 fork 产生一个子进程,执行一个 shell 以运行命令来开启一个进程这个进程必须由 pclose() 函数关闭。

  • 43
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值