Linux进程详细介绍


img

Linux进程

1、计算机体系结构和操作系统管理

1.1、计算机体系结构 – 硬件

  • 目前我们常用的计算机,都是采用冯·诺依曼体系结构

  • 冯·诺依曼提出了电子计算机系统制造的三个基本原则,即采用二进制逻辑、程序存储执行以及电子计算机系统由五个部分组成(运算器、控制器、存储器、输入设备、输出设备,其中运算器与控制器又共同组成为中央处理器CPU),这套理论被称为冯·诺依曼体系结构。

    • 其中:

      • 输入单元:包括键盘, 鼠标,扫描仪, 写板等

      • 中央处理器(CPU):含有运算器和控制器等

      • 输出单元:显示器,打印机等

      • 存储器:这里一般都指内存

    • 不考虑缓存情况,这里的CPU能且只能对内存进行读写,不能访问外设(输入或输出设备)。

    • 外设(输入或输出设备)要输入或者输出数据,也只能写入内存或者从内存中读取。

    • 总结:所有设备都只能直接和内存打交道。

  • 那么为什么CPU不能直接和外设直接打交道?

    原因是CPU速度很快,外设速度很慢,如果CPU与外设直接进行数据传输会导致CPU的大部分时间在等待外设的输入,非常浪费CPU的速度。那么如果在CPU和外设之间加上一个内存,CPU与内存的数据传输会快很多,外设的一些数据可以在CPU还没有运行到这部分的时候先导入到内存中等待,CPU运行时候,直接从内存中拿就行,进而提高传输效率。

    • 我们来看各存储单元的运行速率,是遵循一个金字塔的模式。

      我们可以看到,存储单元离CPU越近,速度更快,但是造价更贵,单体容量越小;存储单元离CPU越远,速度更慢,但是造价更便宜,单体容量越大


1.2、操作系统(Operator System) – 软件

  • 操作系统是对计算机软硬件资源管理的软件

  • 任何计算机系统都包含一个基本的程序集合,称为操作系统(OS)。笼统的理解,操作系统包括:

    • 内核(进程管理,内存管理,文件管理,驱动管理)

    • 其他程序(例如函数库,shell程序等等)

  • 操作系统管理:先描述被管理对象,再组织被管理对象。

    • 底层硬件:其实就是冯·诺依曼体系结构。

    • 驱动程序:驱动程序是计算机系统中的软件,作为硬件与操作系统之间的桥梁,实现操作系统对硬件设备的控制和数据交互,确保系统正常运行。

    • 操作系统:对计算机软硬件资源管理的软件

    • 系统调用接口:操作系统提供给用户的接口,比如用户需要执行一些特权操作,则需要进行系统调用才能访问到内核数据(trapgetpid()等)。(比如会接触到硬件的函数调用)

    • 用户操作接口:设计者提供的。比如lib即库,针对不同的操作系统(如Linux和windows),在C语言编译器中都能用printf函数,说明库函数里面不止实现了某一个操作系统的printf函数。

    • 用户:经过上述管理,用户只需要进行可视化操作。

  • 操作系统对各个模块(内存、进程、文件、驱动等)的管理都是采用链表进行管理(其实就是对PCB(进程控制块 – 后面涉及)、FCB(文件控制块)等进行管理)。

    比如对设备进行管理,那么先定义一个设备的结构体

    struct device{
      char name[];
      char status[];
      ...
      struct device* next;
    }
    

    接下来如果有多个设备需要运行,则使用链表进行排队。

    这就是所谓的先描述,再组织的管理思想。

  • 为什么要有对操作系统的管理

    • 对下管理好软硬件资源 – 手段
    • 对上提供一个良好(稳定、高效、安全)的运行环境 – 目的
      • 如果没有操作系统管理软硬件资源,即如果用户直接管理软硬件资源,用户可能会有不安全的操作(用户1可以拿用户2的数据),导致严重的后果。

2、进程

我们在前面有提到操作系统对外设的管理是先描述,再组织。那么操作系统对进程的管理是怎么样的?同样,先描述,再组织。

2.1、进程基本概念

  • 进程是计算机系统中的执行实体,是程序在执行过程中分配和管理资源的基本单位。每个进程都有独立的内存空间、代码、数据和系统资源,进程之间通过通信机制进行交互。进程的创建、调度和终止由操作系统管理,使得多个任务能够并发执行,提高系统的效率和资源利用率。

  • 进程 = PCB(进程控制块) + 程序。

  • 进程信息被放在一个叫做进程控制块的数据结构中,可以理解为进程属性的集合。课本上称之为PCB(process control block)。

    //举例
    struct PCB{
      //状态
      //优先级
      //内存指针
      //标识符
      //...包含进程几乎所有的属性字段
      struct PCB* next;
    }
    
  • Linux操作系统下的PCB是: task_struct

  • task_ struct内容分类:

    • 标示符: 描述本进程的唯一标示符,用来区别其他进程。
    • 状态: 任务状态,退出代码,退出信号等。
    • 优先级: 相对于其他进程的优先级。
    • 程序计数器: 程序中即将被执行的下一条指令的地址。
    • 内存指针: 包括程序代码和进程相关数据的指针,还有和其他进程共享的内存块的指针
    • 上下文数据: 进程执行时处理器的寄存器中的数据[休学例子,要加图CPU,寄存器]。
    • I/O状态信息: 包括显示的I/O请求,分配给进程的I/O设备和被进程使用的文件列表。
    • 记账信息: 可能包括处理器时间总和,使用的时钟数总和,时间限制,记账号等。
    • 其他信息
  • 查看进程ls \proc查看进程,ls \proc\1可以查看进程id1的进程信息,说明进程在计算机的存储方式也是一个文件夹

    • 大多数进程信息同样可以使用topps这些用户级工具来获取。

      top

      ps axj | head -1 && ps axj | grep process | grep -v grep

      #include <stdio.h>
      #include <sys/types.h>
      #include <unistd.h>
      
      int main()
      {
           while(1){
           		sleep(1);
       		 }
       	return 0;
      }
      

2.2、进程标识符

  • 进程id(PID)

  • 父进程id(PPID)

2.2.1、获取当前进程标识符和当前进程的父进程标识符
  • 使用getpid()函数,这个函数是使用了系统调用接口。使用指令man getpid查看使用的库及返回值。

    #include <sys/types.h>    
    #include <unistd.h>    
    #include <stdio.h>    
        
    int main(){                                                                        
        printf("当前进程id:%d,当前进程的父进程id:%d\n",getpid(),getppid());    
        return 0;    
    } 
    


2.2.2、通过系统调用创建进程 – fork初识
  • 通过man fork查看fork用法

  • fork()函数有两个返回值,子进程返回0,父进程返回子进程id,创建不成功返回-1

  • 父子进程代码fork之后共享,数据各自开辟空间,私有一份(采用写时拷贝)

  • 写时拷贝:在Linux中,写时拷贝(Copy-On-Write,简称COW)是一种用于提高系统性能和节省资源的策略。它通常应用于进程创建和文件复制等场景。以下是一些相关的简要介绍:

    1. 进程创建

      • 当一个进程通过fork()系统调用创建子进程时,子进程并不立即复制父进程的整个地址空间。
      • 相反,子进程会共享父进程的地址空间,只有在其中一个进程试图修改共享的内存页时,才会进行实际的复制(写时拷贝)
    2. 文件复制

      • 当使用类似于cp命令进行文件复制时,操作系统也可以利用写时拷贝的策略。
      • 初始阶段,新文件与原文件共享相同的数据块。只有在其中一个文件尝试修改数据时,才会复制数据块,确保每个文件有自己的副本。

    写时拷贝的优势在于节省内存和提高效率。由于父子进程或复制的文件共享相同的资源,不需要立即进行复制,从而减少了内存的使用和提高了系统的性能。

  • 测试fork是怎么有两个返回值的。

    #include <stdio.h>
    #include <sys/types.h>
    #include <unistd.h>
    
    int main()
    {
         int ret = fork();
         printf("hello proc : %d!, ret: %d\n", getpid(), ret);
         sleep(1);
         return 0;
    }
    

    • 这里我们看到,printf打印了两次,根据我们查询的fork的用法,知道pid1653的为父进程,pid1564的是子进程!这里还有一个问题,父子进程用的是同一个ret,为什么返回值是不同的?(后面涉及到虚拟地址空间再详述)
  • 通常,fork之后要用if分流。

      #include <stdio.h>
      #include <sys/wait.h>
      #include <sys/types.h>
      #include <unistd.h>
      int main()
      {
           int ret = fork();
           if(0 == ret){
               int cnt = 5;
               while(cnt){
                  printf("I am child process : id: %d!, ret: %d\n", getpid(), ret);   
                  sleep(1);
                  cnt--;
               }
    	         exit(0);
           }
           //只有父进程才能运行到这里
           pid_t id = wait(NULL);
           int cnt = 10;
           while(cnt){
              printf("I am father process : id: %d!, ret: %d\n", getpid(),ret);
              sleep(1);
              cnt--;
           }
           printf("等待子进程成功,子进程ID: %d\n",id);
           return 0;
      }
    


2.2.3、更改当前工作目录
  • 使用chdir

    #include <stdio.h>    
    #include <unistd.h>    
        
    int main(){    
        printf("I am a process,pid: %d ,ppif: %d\n",getpid(),getppid());    
        printf("更改工作目录前\n");    
        sleep(15);                                                                  
        chdir("/home/xp2/test_change_workdir");    
        printf("更改工作目录后\n");    
        FILE* pf = fopen("./110.txt","w");    
        sleep(5);    
        printf("创建文件成功\n");    
        fclose(pf);    
        pf = NULL;    
        return 0;    
    }
    


2.3、进程状态

2.3.1、进程任务状态

进程状态查看ps axj | head -1 && ps axj | grep 进程名称 | grep -v grep,如果想要每隔1秒查看一次:while :;do ps axj | head -1 && ps axj | grep process | grep -v grep; sleep 1; done

看看内核代码怎么定义进程状态的:

/*
* The task state array is a strange "bitmap" of
* reasons to sleep. Thus "running" is zero, and
* you can test for combinations of others with
* simple bit tests.
*/
static const char * const task_state_array[] = {
"R (running)", /* 0 */
"S (sleeping)", /* 1 */
"D (disk sleep)", /* 2 */
"T (stopped)", /* 4 */
"t (tracing stop)", /* 8 */
"X (dead)", /* 16 */
"Z (zombie)", /* 32 */
};
  • R-运行状态(running): 并不意味着进程一定在运行中,它表明进程要么是在运行中要么在运行(就绪)队列里。进程不是一直在运行的,一般都是采用时间片策略(每隔一段时间,这个时间非常短,CPU轮换的运行不同的进程),没有在运行的进程,如果只差CPU一种资源的话,就是就绪状态(这里我们统称为运行状态R)。

      #include <stdio.h>    
          
      int main(){    
          printf("准备开始while循环\n");    
    	    sleep(3);    
          while(1){    
           //   printf("查看进程现在状态\n");                                       
          }    
          return 0;    
      }
    

  • S-睡眠状态(sleeping): 意味着进程在等待事件完成(阻塞)(这里的睡眠有时候也叫做可中断睡眠(interruptible sleep))。

      #include <stdio.h>    
                                
      int main(){    
          int input = 0;                                                            
          scanf("%d",&input);    
          printf("input = %d\n",input);    
    	    sleep(3);    
          return 0;    
      } 
    

  • D-磁盘休眠状态(Disk sleep)有时候也叫不可中断睡眠状态(uninterruptible sleep),在这个状态的进程通常会等待IO的结束。

  • T-停止状态(stopped): 可以通过发送 SIGSTOP 信号给进程来停止(T)进程。这个被暂停的进程可以通过发送 SIGCONT 信号让进程继续运行。

  • X-死亡状态(dead):这个状态只是一个返回状态,你不会在任务列表里看到这个状态。

  • Z(zombie)-僵尸进程:

    • 僵死状态(Zombies)是一个比较特殊的状态。当进程退出并且父进程没有读取到子进程退出的返回代码时(读取使用wait()系统调用,后面讲)就会产生僵死(尸)进程。
    • 僵死进程会以终止状态保持在进程表中,并且会一直在等待父进程读取退出状态代码。
    • 所以,只要子进程退出,父进程还在运行,但父进程没有读取子进程状态,子进程进入Z状态。并且如果父进程一直不读取子进程状态,子进程会一直保持Z状态,直到父进程退出(如果父进程比子进程先退出,则子进程的状态为S(子进程变为孤儿进程(后面会说到),父进程id变为1),直到进程退出)。
    #include <stdio.h>                                                              
    #include <unistd.h>
    #include <sys/types.h>
                          
    int main(){
        printf("马上准备创建子进程\n");
        pid_t id =  fork();
        if(id < 0) perror("fork error\n");
        else if(id == 0) {
            //子进程
            int cnt = 5;
            while(cnt){
                printf("I am child ,pid: %d ,ppid: %d\n",getpid(),getppid());
                sleep(1);
                cnt--;
            }
        }
        else{
            //父进程
            int cnt = 10;
            while(cnt){
                printf("I am father ,pid: %d ,ppid: %d\n",getpid(),getppid());
                sleep(1);
                cnt--;
            }
        }
        return 0;
    }
    


僵尸进程的危害

  • 进程的退出状态必须被维持下去,因为他要告诉关心它的进程(父进程),你交给我的任务,我办的怎么样了。可父进程如果一直不读取,那子进程就一直处于Z状态?是的!

  • 维护退出状态本身就是要用数据维护,也属于进程基本信息,所以保存在task_struct(PCB)中,换句话说,Z状态一直不退出,PCB一直都要维护?是的!

  • 那一个父进程创建了很多子进程,就是不回收,是不是就会造成内存资源的浪费?是的!因为数据结构对象本身就要占用内存,想想C中定义一个结构体变量(对象),是要在内存的某个位置进行开辟空间!

  • 内存泄漏?是的!

  • 如何避免?wait()或者waitid()(后面讲)。

  • wait()函数

  #include <stdio.h>
  #include <sys/wait.h>
  #include <sys/types.h>
  #include <unistd.h>
  int main()
  {
       int ret = fork();
       if(0 == ret){
           int cnt = 5;
           while(cnt){
              printf("I am child process : id: %d!, ret: %d\n", getpid(), ret);   
              sleep(1);
              cnt--;
           }
	         exit(0);
       }
       //只有父进程才能运行到这里
       pid_t id = wait(NULL);
       int cnt = 10;
       while(cnt){
          printf("I am father process : id: %d!, ret: %d\n", getpid(),ret);
          sleep(1);
          cnt--;
       }
       printf("等待子进程成功,子进程ID: %d\n",id);
       return 0;
  }

孤儿进程

  • 父进程如果提前退出,那么子进程后退出,进入Z之后,那该如何处理呢?

  • 父进程先退出,子进程就称之为“孤儿进程”,子进程的ppid变为1。

  • 孤儿进程被1号init进程领养,当然要有init进程回收喽。

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

int main(){
    printf("马上准备创建子进程\n");
    pid_t id =  fork();
    if(id < 0) perror("fork error\n");
    else if(id == 0) {
        //子进程
        int cnt = 10;
        while(cnt){
            printf("I am child ,pid: %d ,ppid: %d\n",getpid(),getppid());
            sleep(1);
            cnt--;
        }
    }
    else{
        //父进程
        int cnt = 5;
        while(cnt){
            printf("I am father ,pid: %d ,ppid: %d\n",getpid(),getppid());
            sleep(1);
            cnt--;
        }
    }
    return 0;
}

2.3.2、进程等待
  • 进程等待必要性

  • 之前讲过,子进程退出,父进程如果不管不顾,就可能造成‘僵尸进程’的问题,进而造成内存泄漏。

  • 另外,进程一旦变成僵尸状态,那就刀枪不入,“杀人不眨眼”的kill -9 也无能为力,因为谁也没有办法杀死一个已经死去的进程。

  • 最后,父进程派给子进程的任务完成的如何,我们需要知道。如,子进程运行完成,结果对还是不对,或者是否正常退出。

  • 父进程通过进程等待的方式,回收子进程资源,获取子进程退出信息。

  • 进程等待的方法

  • wait方法:

    #include<sys/types.h>
    #include<sys/wait.h>
    
    pid_t wait(int *status);
    
    返回值:
     		成功返回被等待进程pid,失败返回-1。
    参数:
     		输出型参数,获取子进程退出状态,不关心则可以设置成为NULL
    
  • waitpid方法:

    pid_t waitpid(pid_t pid, int *status, int options);
    
    返回值:
         当正常返回的时候waitpid返回收集到的子进程的进程ID;
         如果设置了选项WNOHANG,而调用中waitpid发现没有已退出的子进程可收集,则返回0;
         如果调用中出错,则返回-1,这时errno会被设置成相应的值以指示错误所在;
    参数:
         pid:
             Pid=-1,等待任一个子进程。与wait等效。
             Pid>0.等待其进程ID与pid相等的子进程。
     status:
           WIFEXITED(status): 若为正常终止子进程返回的状态,则为真。(查看进程是否是正常退出)
           WEXITSTATUS(status): 若WIFEXITED非零,提取子进程退出码。(查看进程的退出码)
     options:
         	 WNOHANG(加入这个参数,那么进程就是非阻塞等待。不加入这个参数,及参数为0,则进程是阻塞等待): 若pid指定的子进程没有结束,则waitpid()函数返回0,不予以等待。若正常结束,则返回该子进程的ID。
    
    • 如果子进程已经退出,调用wait/waitpid时,wait/waitpid会立即返回,并且释放资源,获得子进程退出信息。
    • 如果在任意时刻调用wait/waitpid,子进程存在且正常运行,则进程可能阻塞。
    • 如果不存在该子进程,则立即出错返回。
  • 获取进程的status

    • wait和waitpid,都有一个status参数,该参数是一个输出型参数,由操作系统填充。

    • 如果传递NULL,表示不关心子进程的退出状态信息。

    • 否则,操作系统会根据该参数,将子进程的退出信息反馈给父进程。

    • status不能简单的当作整形来看待,可以当作位图来看待,具体细节如下图(只研究status低16比特位):

  • 举例:

    • process_wait.c文件

      #include <stdio.h>                                                                                                                                                                                                                                                                                          
      #include <sys/types.h>    
      #include <unistd.h>    
      #include <sys/wait.h>    
      #include <stdlib.h>    
          
      #define NUM 5    
      typedef void(*fun_t)();    
      fun_t tasks[NUM];    
      //任务/    
          
      void printLog(){    
          printf("this is a printLog\n");    
      }    
          
      void flushNPC(){    
          printf("this is a flushNPC\n");    
      }    
          
      void printNet(){    
          printf("this is a printNet\n");    
      }    
          
      void initTasks(){    
          tasks[0] = printLog;    
          tasks[1] = flushNPC;    
          tasks[2] = printNet;    
          tasks[3] = 0;    
      }    
          
      void executeTasks(){    
          for(int i=0; tasks[i]; ++i){    
              tasks[i]();    
          }    
      }    
      int main(){    
          initTasks();    
          
          printf("I am a process! pid=%d ,ppid=%d\n",getpid(),getppid());    
          pid_t id = fork();    
          if(id < 0) perror("fork\n");    
          else if (id == 0){    
              //子进程    
              int cnt = 10;    
              while(cnt){    
                  printf("I am child process! pid=%d ,ppid=%d\n",getpid(),getppid());    
                  sleep(1);    
                  --cnt;    
              }    
              exit(111);    
          }    
          //父进程    
          int status = 0;    
           
          while(1){    
              pid_t rid = waitpid(id,&status,WNOHANG);//非阻塞等待    
              if( 0 == rid ){    
                  //子进程没有结束,我们父进程可以做一些其他事    
                  printf("during wait child ,do other things ....\n");    
                  printf("############ tasks begin ###########\n");    
                  executeTasks();    
                  printf("############ tasks end ###########\n");    
          
              }    
              else if (rid > 0){    
                  //进程正常终止    
                  printf("wait child success,status:%d,exitcode:%d\n",status,WEXITSTATUS(status));    
                  break;    
              }    
              else{    
                  perror("waitpid error\n");    
                  break;    
              }    
              sleep(1);    
          }    
              //pid_t rid = waitpid(id,&status,0);//阻塞等待    
              //    if(WIFEXITED(status)){    
              //        //进程正常终止    
              //   printf("wait child success,status:%d,exitcode:%d\n",status,WEXITSTATUS(status));    
              //    }else{    
              //        printf("waitpid error,exitsignal:%d\n",status&0x7F);    
              //        exit(-1);    
              //    }    
              //}    
          
          return 0;    
      } 
      
    • 运行结果:


2.3.3、进程终止
  • 进程退出场景:
    • 代码运行完毕,结果正确
    • 代码运行完毕,结果不正确
    • 代码异常终止
  • 进程退出方法:可以通过 echo $? 查看进程退出码。
    • 正常终止:
      • 从main返回
      • 调用exit
      • 调用_exit
    • 异常终止:
      • ctrl + c,信号终止

2.3.3.1、进程退出代码
  • 在C语言中,printf函数通常需要遇到换行符 \n 或者程序正常结束(return 0)时,才会将缓冲区中的内容刷新到标准输出。这是因为标准输出是行缓冲的,意味着当遇到换行符或者程序正常结束时,才会刷新缓冲区。

  • 如果你在使用 printf 输出的内容没有包含 \n 并且程序没有正常结束,可能会导致输出没有立即刷新到终端,而是留在了缓冲区中。这就可能导致你看不到输出,或者输出的顺序不符合你的期望。

  • 为了确保 printf 输出的即时刷新,你可以使用 fflush(stdout) 函数,它可以强制将缓冲区的内容刷新到标准输出。

  • _exit函数

    #include <unistd.h>
    void _exit(int status);
    参数:status 定义了进程的终止状态,父进程通过wait来获取该值
    
    • 说明:虽然statusint,但是仅有低8位(前面的位还有用来记录信号值)可以被父进程所用。所以_exit(-1)时,在终端执行echo $?发现返回值是255
  • exit函数

    #include stdlib.h>
    #include <unistd.h>
    void exit(int status);
    
    • 说明:exit最后也会调用_exit, 但在调用_exit之前,还做了其他工作:

      1. 执行用户通过 atexit或on_exit定义的清理函数。

      2. 关闭所有打开的流,所有的缓存数据均被写入(相当于带有刷新缓冲区的功能)。

      3. 调用_exit。

  • 举例:

    #include <stdio.h>    
    #include <stdlib.h>
    #include <unistd.h>                                                             
        
    int main(){    
        printf("进程退出咯");    
        //exit(0);    
        _exit(0);    
    } 
    

    • return退出:return是一种更常见的退出进程方法。执行return n等同于执行exit(n),因为调用main的运行时函数会将main的返回值当做 exit的参数。


2.3.3.2.、进程退出信号
  • 进程退出信号是在status的低7位。我们获取进程退出代码可以使用对位与的方法,及&0x7f

2.4、进程优先级

  • 基本概念

    • cpu资源分配的先后顺序,就是指进程的优先权(priority)。

    • 优先权高的进程有优先执行权利。配置进程优先权对多任务环境的linux很有用,可以改善系统性能。

    • 还可以把进程运行到指定的CPU上,这样一来,把不重要的进程安排到某个CPU,可以大大改善系统整体性能。

  • 查看系统进程

    • 在linux或者unix系统中,用ps –l命令则会类似输出以下几个内容:

    • 注意到其中的几个重要信息:

      • UID : 代表执行者的身份

      • PID : 代表这个进程的代号

      • PPID :代表这个进程是由哪个进程发展衍生而来的,亦即父进程的代号

      • PRI :代表这个进程可被执行的优先级,其值越小越早被执行

      • NI :代表这个进程的nice值

    • 其中PRI 和 NI 是我们在这一节更需要注意的。

      • PRI(即priority)也还是比较好理解的,即进程的优先级,或者通俗点说就是程序被CPU执行的先后顺序,此值越小进程的优先级别越高。

      • NI 就是我们所要说的nice值了,其表示进程可被执行的优先级的修正数值

      • PRI值越小越快被执行,那么加入nice值后,将会使得PRI变为:PRI(new)=PRI(old)+nice注意,这里的PRI(old)在进程创建时就已经确定了,是不可改变的,即每次对NI的修改,都是在这个PRI(old)基础上进行修改的

      • 这样,当nice值为负值的时候,那么该程序将会优先级值将变小,即其优先级会变高,则其越快被执行

      • 所以,调整进程优先级,在Linux下,就是调整进程nice值

      • nice其取值范围是-20至19,一共40个级别

      • 需要强调一点的是,进程的nice值不是进程的优先级,他们不是一个概念,但是进程nice值会影响到进程的优先级变化。

      • 可以理解nice值是进程优先级的修正修正数据

  • 用top命令更改已存在进程的nice

    1. 在命令行输入top
    2. 进入top后按“r”–>输入进程PID–>输入nice值。(如果权限不够就切换root身份(su -))
    #include <stdio.h>
    #include <unistd.h>
    #include <sys/types.h>
    
    int main(){
      	while(1){
          	printf("I am a process! pid: %d, ppid: %d\n",getpid(),getppid());
        }
      	return 0;
    }
    

  • 一些概念:

    • 竞争性: 系统进程数目众多,而CPU资源只有少量,甚至1个,所以进程之间是具有竞争属性的。为了高效完成任务,更合理竞争相关资源,便具有了优先级
    • 独立性: 多进程运行,需要独享各种资源,多进程运行期间互不干扰
    • 并行: 多个进程在多个CPU下分别,同时运行,这称之为并行
    • 并发: 多个进程在一个CPU下采用进程切换的方式,在一段时间之内,让多个进程都得以推进,称之为并发

2.5、进程程序替换

  • 基本概念
  • 进程程序替换是指一个正在运行的进程被另一个程序替代的过程。这通常通过执行 exec 系列函数来实现。在进程程序替换发生时,原进程的地址空间、代码、数据和堆栈等内容会被新程序所取代,新程序接管原进程的执行(也就是不会创建新进程)

    以下是进程程序替换的一般步骤和主要特点:

    1. 加载新程序: 新程序的可执行文件被加载到原进程的地址空间中。这包括新程序的代码段、数据段以及堆栈等。

    2. 更新进程上下文: 进程上下文包括寄存器的状态、程序计数器和堆栈指针等。这些上下文信息会被更新,以便开始执行新程序的代码。

    3. 关闭文件描述符: 进程通常会继承一些文件描述符,这些描述符可能是原进程打开的文件、套接字等。在进程替换时,可以选择关闭一些文件描述符,打开新的文件描述符,或者继续使用原有的描述符。

    4. 设置环境变量和命令行参数: 新程序可能需要特定的环境变量或命令行参数来正确执行。这些信息需要在进程替换前进行设置。

    5. 权限检查: 系统可能对新程序的执行权限进行检查,确保进程有权执行新程序。

    6. 执行新程序: 进程开始执行新程序的代码,原程序的执行被替代。

    7. 资源清理: 在进程替换完成后,可能需要进行资源清理工作,释放原进程占用的资源,如内存、文件描述符等。

    进程程序替换的主要优点是在无需创建新进程的情况下,实现了程序的动态更新。这在一些场景下很有用,例如在服务器端动态加载新的应用程序或更新服务时。常见的 exec 函数有 execveexecvpexecl 等,它们提供了不同的参数传递方式和替换策略。

  • 常见的程序替换函数使用
  • execl:参数path需要指令的绝对路径,arg需要具体的指令,需要分开写,最后以NULL结尾。如execl("/usr/bin/ls", "ls" , "-a" , "-l", NULL);

  • execlp:参数file只需要指令名称(绝对路径也可以),arg需要具体的指令,需要分开写,最后以NULL结尾。如execlp("ls", "ls" , "-a" , "-l", NULL);

  • execle:给子进程设置全新的环境变量。参数path需要指令的绝对路径,arg需要具体的指令,需要分开写,最后以NULL结尾,envp需要自己设置环境变量数组。

    envp环境变量数组举例:

    char *const env[] = {    
                (char*)"Y=yyyyyy",    
                (char*)"Z=zzzzzz",    
                (char*)"L=llllll"    
            };
    

    execle("./mytest", "mytest" , NULL , env);

  • execv:参数path需要指令的绝对路径,argv需要具体的指令数组。

    argv指令数组举例:

    char *const argv[] = {    
                (char*)"ls",    
                (char*)"-l",    
                (char*)"-a"    
            };
    

    execv("/usr/bin/ls", argv);

  • execvp:参数file只需要指令名称(绝对路径也可以),argv需要具体的指令数组。如execvp("ls", argv);

  • process_replace.c文件
 	#include <stdio.h>                                                                    
  #include <unistd.h>
  #include <sys/types.h>
  #include <sys/wait.h>
  
  int main(){
      printf("I am a process!,pid:%d\n",getpid());
      char *const env[] = {    
          (char*)"Y=yyyyyy",    
          (char*)"Z=zzzzzz",    
          (char*)"L=llllll"    
      };
      pid_t id = fork();
      if(id == 0){
      		char *const argv[] = {
              (char*)"ls",
              (char*)"-l",
              (char*)"-a"
          };
          printf("进程程序替换开始\n");
          sleep(2);
          //带l的有可变参数,带p的路径可以只需要指令名称,带e的有环境变量
          execle("./mytest","mytest","-a","-b","-c","-d", NULL,env);
          //execvp("ls", argv);
          //execv("/usr/bin/ls", argv);
          //execlp("ls", "ls", "-a", "-l", NULL);
          //execl("/usr/bin/ls", "ls", "-a", "-l", NULL);    
          printf("进程程序替换结束\n");    
      }    
      
      pid_t rid = waitpid(id,NULL,0);    
      if(rid > 0){    
          printf("wait success\n");    
      }    
  }    
  • mytest.cc文件
	#include <iostream>    
  #include <stdio.h>    
	int main(int argc, char* argv[], char* env[]){                                        
      for(int i=0; argv[i]; ++i){    
	        printf("argv[%d]=%s\n",i,argv[i]);    
      }    
      
      for(int i=0; env[i]; ++i){    
	        printf("env[%d]=%s\n",i,env[i]);    
      }    
      return 0;    
  }   
  • Makefile文件
.PHONY:all    
all:process_replace mytest    
    
mytest:mytest.cc    
    g++ -o $@ $^ -std=c++11    
    
process_replace:process_replace.c    
    gcc -o $@ $^    
    
.PHONY:clean    
    
clean:    
    rm -f process_replace mytest 
  • 运行结果

  • 助记
  • l(list) : 表示参数采用列表

  • v(vector) : 参数用数组

  • p(path) : 有p自动搜索环境变量PATH

  • e(env) : 表示自己维护环境变量


2.6、环境变量

  • 基本概念

    • 环境变量(environment variables)一般是指在操作系统中用来指定操作系统运行环境的一些参数

    • 如:我们在编写C/C++代码的时候,在链接的时候,从来不知道我们的所链接的动态静态库在哪里,但是照样可以链接成功,生成可执行程序,原因就是有相关环境变量帮助编译器进行查找。

    • 环境变量通常具有某些特殊用途,还有在系统当中通常具有全局特性

  • 常见环境变量

    • PATH : 指定命令的搜索路径

    • HOME : 指定用户的主工作目录(即用户登陆到Linux系统中时,默认的目录~)

    • SHELL : 当前Shell,它的值通常是/bin/bash

  • 查看环境变量方法

    • echo $NAME //NAME:你的环境变量名称

    • 这里所显示的路径环境变量是系统中的命令的路径,比如ls等。

  • 测试文件:

    1. 创建command_path.c文件
    #include <stdio.h>    
        
    int main(){    
        printf("测试指令路径\n");    
        return 0;                                                                   
    }
    
    1. 对比./command_path执行和之间command_path执行区别

    2. 为什么有些指令可以直接执行,不需要带路径,而我们的二进制程序需要带路径(比如需要绝对路径/home/xp2/Linux_Primary/day_21/command_path或者./command_path)才能执行?

    3. 将我们的程序所在路径加入环境变量PATH当中, export PATH=$PATH:command_path程序所在路径。这样command_path程序所在路径就是系统路径里了,下面执行command_path程序就不用绝对路径了,直接在命令行输入command_path即可。

    4. 命令行输入对比/home/xp2/Linux_Primary/day_21/command_path或者./command_pathcommand_path

    5. 还有什么方法可以不用带路径,直接就可以运行呢?

    6. 使用符号链接:如果知道程序的相对路径或部分路径,可以将其链接到某个目录(例如/usr/local/bin),这样就可以从任何地方运行它。

    7. 使用别名:使用alias命令可以为命令创建别名,这样可以使用更短或更易于记忆的名称来代替长命令。

  • 和环境变量相关的命令

    1. echo: 显示某个环境变量值

    2. env: 显示所有环境变量

    3. export: 设置一个新的环境变量

    4. unset: 清除环境变量

    5. set: 显示本地定义的shell变量和环境变量

  • 环境变量组织方式

    • 每个程序都会收到一张环境表,环境表是一个字符指针数组,每个指针指向一个以’\0’结尾的环境字符串。
  • 通过代码获取环境变量

    • 通过代码获取命令行参数:用到main函数第一个参数(命令行输入的字符串个数)和第二个参数(命令行输入的字符串,存到了argv数组中,以NULL结尾,和environ的组织方式类似)

        #include <stdio.h>    
        #include <stdlib.h>    
        #include <string.h>    
        #include <unistd.h>    
                              
      	int main(int argc, char *argv[], char *env[]) {    
            if (argc != 4) {    
                printf("usage:\n\t%s -[add|sub|mul|div] x y\n\n", argv[0]);    
                return 1;    
            }    
                              
            int x = atoi((const char*)argv[2]);    
            int y = atoi((const char*)argv[3]);    
            if (strcmp(argv[1], "-add") == 0) {    
                printf("%d+%d = %d\n", x, y, x + y);    
            }    
            
            
            if (strcmp(argv[1], "-sub") == 0) {    
                printf("%d-%d = %d\n", x, y, x - y);    
            }    
            
            
            if (strcmp(argv[1], "-mul") == 0) {    
                printf("%d*%d = %d\n", x, y, x * y);    
            }    
            
            
            if (strcmp(argv[1], "-div") == 0) {    
                printf("%d/%d = %d\n", x, y, x / y);    
            }    
                                                
            return 0;    
        }
      

    • main函数第三个参数

        #include <stdio.h>    
                                                                                      
      	int main(int argc, char *argv[], char *env[]){    
             int i = 0;           
             for(; env[i]; i++){         
                printf("%s\n", env[i]);    
             }          
             return 0;    
        }
      

    • 通过第三方变量environ获取

        #include <stdio.h>    
                                                                                                        
      	int main(int argc, char *argv[]){    
            extern char **environ;    
            int i = 0;    
            for(; environ[i]; i++){    
                printf("%s\n", environ[i]);    
            }    
            return 0;    
        }
      

      libc中定义的全局变量environ指向环境变量表,environ没有包含在任何头文件中,所以在使用时要用extern声明。

  • 通过系统调用获取或设置环境变量

    • putenv :顾名思义,设置环境变量,但是请注意,使用 putenv 设置的环境变量实际上是直接修改了当前进程的环境变量,因此这种更改在程序结束后不会保留。如果你想要在**调用其他程序时(这里不要搞混淆,子进程还是会继承父进程环境变量)**传递环境变量,可以考虑使用 setenv 函数,它允许更灵活地设置和修改环境变量。以下是 putenv 函数的基本用法:
    #include <stdlib.h>
    
    int putenv(char *string);
    
    • 函数接受一个形如 "name=value" 的字符串,将其添加到当前进程的环境变量中。如果环境变量已经存在,则它的值将被替换。如果字符串中不包含等号(=),则环境变量会被删除。
    • setenvsetenv函数用于设置环境变量,提供了更为直观和安全的方式,相对于 putenv 更加现代和推荐使用。下面是 setenv 函数的基本用法:
    #include <stdlib.h>
    
    int setenv(const char *name, const char *value, int overwrite);
    
    • name: 要设置的环境变量的名称。
    • value: 要设置的环境变量的值。
    • overwrite: 一个整数,表示是否覆盖已存在的环境变量。如果 overwrite 为非零值,将覆盖已存在的环境变量;如果为零值,不会覆盖已存在的环境变量。
    • getenv:顾名思义,获取环境变量。以下是 getenv 函数的基本用法:
    #include <stdlib.h>
    
    char *getenv(const char *name);
    
    • name: 要获取值的环境变量的名称。
    • getenv 函数返回一个指向以 null 结尾的字符串的指针,表示指定环境变量的值。如果环境变量不存在,则返回 NULL
    #include <stdio.h>    
    #include <stdlib.h>    
    #include <unistd.h>    
       
    int main(){    
       extern char **environ;    
       char str[] = "HELLO=aaaaaaaaaaaaa";    
       putenv(str);    
       setenv("BYEBYE","bbbbbbbbbbbbb",1);    
       printf("%s\n",getenv("PATH"));    
       printf("%s\n",getenv("HELLO"));    
       printf("%s\n",getenv("BYEBYE"));    
       int i = 0;    
       for(; environ[i]; ++i){    
           printf("%s\n",environ[i]);    
       }    
       pid_t id = fork();    
       if(0 == id){    
           printf("我是子进程\n");    
           printf("%s\n",getenv("HELLO"));    
           printf("%s\n",getenv("BYEBYE"));    
           int i = 0;                                                                                                                      
           for(; environ[i]; ++i){    
               printf("%s\n",environ[i]);    
           }    
       }    
       return 0;    
    }
    

  • 环境变量通常是具有全局属性的

    • 环境变量通常具有全局属性,可以被子进程继承下去。

    • 命令行中输入export 环境变量(例如:MYENV=mmmmmmmm) 和 单纯输入环境变量(例如:MYENV=mmmmmmmm) 区别:

      • export 环境变量(例如:MYENV=mmmmmmmm) :这里是导出环境变量MYENV放到**普通变量(环境变量)**里,可以命令行输入env查看到。影响子进程:当你使用 export 命令设置环境变量时,该环境变量将会影响当前 Shell 进程及其所有子进程。这意味着这个环境变量会传递给由当前 Shell 启动的其他命令或脚本。
      • 环境变量(例如:MYENV=mmmmmmmm):这里是设置的本地变量(局部变量),只能在当前 Shell 函数或脚本中可见(echo $MYENV查看)。在函数或脚本的其他部分,以及在启动的子进程中,这些局部变量是不可见的
      #include <stdio.h>    
      #include <stdlib.h>    
                          
      int main(){    
          char* ret = getenv("MYENV");    
          if(ret){    
              printf("%s\n",ret);    
          }    
          return 0;                                                                   
      }
      


2.6、进程地址空间

  • 研究背景

    • kernel 2.6.32
    • 32位平台
  • 对比C语言内存空间布局

    • 程序地址空间和这个布局类似。
  • 查看以下代码运行结果

    #include <stdio.h>    
    #include <stdlib.h>                                                             
    #include <unistd.h>    
        
    int g_val = 1;    
    int main(){    
        pid_t id = fork();    
        if(id < 0){    
            perror("fork");    
            return 0;    
        }else if(id == 0){    
            printf("child[%d]: %d : %p\n", getpid(), g_val, &g_val);    
        }else{    
            printf("parent[%d]: %d : %p\n", getpid(), g_val, &g_val);    
        }    
        return 0;    
    }
    

    • 我们发现父进程和子进程里变量g_val的值一样,地址也一样

    • 再看下面修改g_val值的代码:

    #include <stdio.h>    
    #include <stdlib.h>    
    #include <unistd.h>    
        
    int g_val = 1;    
    int main(){    
        pid_t id = fork();    
        if(id < 0){    
            perror("fork");    
            return 0;    
        }else if(id == 0){    
            g_val = 100;    
            printf("child[%d]: %d : %p\n", getpid(), g_val, &g_val);    
        }else{                                                                      
            printf("parent[%d]: %d : %p\n", getpid(), g_val, &g_val);    
        }    
        return 0;    
    }
    

    • 我们发现父进程和子进程里变量g_val的值不同,但是地址相同,说明这里输出的变量不是同一个变量,只是变量名字一样而已

    • 这里我们就能想到,这个共同的地址肯定不是真实地址(物理地址),也就是说这个地址是虚拟地址。(其实这里也涉及到写时拷贝

      操作系统负责将虚拟地址转换为物理地址

  • 进程地址空间分布图

    • 通过这个分布图,我们可以清晰的看到,父子进程g_val的虚地址是一样的,但是父子进程维护不同的页表(简单来说就是虚拟地址映射为物理地址的映射表),也就是虚拟地址映射为物理地址的映射规则不同,因此映射到物理内存的父子进程的g_val的地址不一样,值也不同(如果父子进程都没对g_val进行修改,那么父子进程g_val物理内存也指向同一块空间,即在对变量进行修改的时候,需要进行写实拷贝,这时候就会对应不同的物理地址)

2.7、Linux2.6内核进程调度队列

  • 一般来说,一个CPU只有一个runqueue。如果有多个CPU,则需要考虑到进程个数的负载均衡问题。

  • 活跃队列

    • 时间片还没有结束的所有进程都按照优先级放在该队列

    • nr_active: 总共有多少个运行状态的进程

    • queue[140]: 一个元素就是一个进程队列,相同优先级的进程按照FIFO规则进行排队调度,所以,数组下标就是优先级(因此优先级数值范围是0~139,其中0~99是实时优先级,100~139是普通优先级)!

    • 从该结构中,选择一个最合适的进程,过程是怎么的呢?

      1. 从0下表开始遍历queue[140]

      2. 找到第一个非空队列,该队列必定为优先级最高的队列

      3. 拿到选中队列的第一个进程,开始运行,调度完成!

      4. 遍历queue[140]时间复杂度是常数!但还是太低效了!

    • bitmap[5]:一共140个优先级,一共140个进程队列,为了提高查找非空队列的效率,就可以用5*32个比特位表示队列是否为空,这样,便可以大大提高查找效率!

  • 过期队列

    • 过期队列和活动队列结构一模一样
    • 过期队列上放置的进程,都是时间片耗尽的进程
    • 当活跃队列上的进程都被处理完毕之后,对过期队列的进程进行时间片重新计算(重新放到活跃队列运行)
  • active指针和expired指针

    • active指针永远指向活跃队列
    • expired指针永远指向过期队列
    • 可是活跃队列上的进程会越来越少,过期队列上的进程会越来越多,因为进程时间片到期时一直都存在的。
    • 没关系,在合适的时候,只要能够交换active指针和expired指针的内容,就相当于有具有了一批新的活动进程!
  • 总结

    • 在系统当中查找一个最合适调度的进程的时间复杂度是一个常数,不随着进程增多而导致时间成本增加,我们称之为进程调度O(1)算法。

3、自制Shell

  • 效仿shell交互界面:

  • 所以要写一个shell,需要循环以下过程

    1. 获取命令行

    2. 解析命令行

    3. 建立一个子进程(fork)

    4. 替换子进程(execvp)

    5. 父进程等待子进程退出(wait)

  • 实现代码

    #include <stdio.h>
    #include <stdlib.h>
    #include <string.h>
    #include <unistd.h>
    #include <sys/types.h>
    #include <sys/wait.h>
        
    #define SIZE 1024
    #define MAX_LEN 128
    #define SEP " "
        
    char *argv[MAX_LEN];//命令行字符串数组
    char pwd[SIZE];
    char envtemp[SIZE];
    int lastcode = 0;//退出码
        
    const char *HostName() {
        char *hostname = getenv("HOSTNAME");
        if (hostname) return hostname;
        else return "None";
    }
        
    const char *UserName() {
        char *hostname = getenv("USER");
        if (hostname) return hostname;
        else return "None";
    }
        
    const char *CurrentWorkDir() {
        char *hostname = getenv("PWD");
        if (hostname) return hostname;
        else return "None";
    }
        
    char *Home() {
        char *hostname = getenv("HOME");
        if (hostname) return hostname;
        else return "None";
    }
        
    int Interactive(char *commandline, int size) {
        printf("[%s@%s %s]$ ", UserName(), HostName(), CurrentWorkDir());
        fgets(commandline, SIZE, stdin);
        commandline[strlen(commandline) - 1] = '\0';
        
        return strlen(commandline);//空串返回0
    }
        
    void Split(char *commandline) {
        int i = 0;
        argv[i++] = strtok(commandline, SEP);
        while (argv[i++] = strtok(NULL, SEP));
        //解决ls无彩色问题
        if (strcmp(argv[0], "ls") == 0) {
            argv[i - 1] = (char *) "--color";
            argv[i] = NULL;
        }
    }
        
    int BuildingCmd() {
        int ret = 0;
        if (strcmp(argv[0], "cd") == 0) {
            ret = 1;
            char *target = argv[1];
            //cd XXX 和cd
            if (!target) target = Home();//第二个参数为NULL
            //改变当前工作目录
            chdir(target);
            //处理target为..的情况
            //重新获取当前路径
            char temp[1024];
            getcwd(temp, 1024);
            //更新当前环境变量PWD
            snprintf(pwd, SIZE, "PWD=%s", temp);
            //导出环境变量
            putenv(pwd);
        } else if (strcmp(argv[0], "export") == 0) {
            ret = 1;
            if (argv[1]) {
                strcpy(envtemp, argv[1]);
                putenv(envtemp);
            }
        } else if (strcmp(argv[0], "echo") == 0) {
            ret = 1;
            if (argv[1] == NULL) {
                printf("\n");
            } else {
                if (argv[1][0] == '$') {
                    if (argv[1][1] == '?') {
                        printf("%d\n", lastcode);
                        lastcode = 0;
                    } else {
                        char *e = getenv(argv[1] + 1);
                        if (e) printf("%s\n", e);
                    }
                } else {
                    printf("%s\n", argv[1]);
                }
            }
        }
        return ret;
    }
        
    void Execute() {
        //只能交给子进程,如果用父进程执行命令行,执行一次就结束了
        pid_t id = fork();
        if (id < 0) perror("fork\n");
        else if (id == 0) {
            execvp(argv[0], argv);
            exit(1);//执行完退出
        }
        //父进程等待子进程
        int status = 0;
        pid_t rid = waitpid(id, &status, 0);
        if (rid == id) lastcode = WEXITSTATUS(status);//等待成功
    }
        
    int main() {
        
        while (1) {
            char commandline[SIZE];
            //1. 打印命令行提示符,获取用户的命令字符串
            int n = Interactive(commandline, SIZE);
            if (!n) continue;//返回值为0就是空串,下面代码不再执行
            //2. 对命令行字符串进行切割
            Split(commandline);
            //3. 处理内建命令
            n = BuildingCmd();
            if (n) continue;
            //4. 执行这个命令
            Execute();
        }
        //int i;
        //for(i=0;argv[i];++i){
        //    printf("argv[%d]:%s\n",i,argv[i]);
        //}
        return 0;
    }
    

那么好,Linux进程就到这里,如果你对Linux和C++也感兴趣的话,可以看看我的主页哦。下面是我的github主页,里面记录了我的学习代码和leetcode的一些题的题解,有兴趣的可以看看。

Xpccccc的github主页

评论 48
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Xpccccc

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值