当前位置: 首页 > news >正文

Linux进程详细介绍

文章目录

  • Linux进程
    • 1、计算机体系结构和操作系统管理
      • 1.1、计算机体系结构 -- 硬件
      • 1.2、操作系统(Operator System) -- 软件
    • 2、进程
      • 2.1、进程基本概念
      • 2.2、进程标识符
        • 2.2.1、获取当前进程标识符和当前进程的父进程标识符
        • 2.2.2、通过系统调用创建进程 -- fork初识
        • 2.2.3、更改当前工作目录
      • 2.3、进程状态
        • 2.3.1、进程任务状态
        • 2.3.2、进程等待
        • 2.3.3、进程终止
          • 2.3.3.1、进程退出代码
          • 2.3.3.2.、进程退出信号
      • 2.4、进程优先级
      • 2.5、进程程序替换
      • 2.6、环境变量
      • 2.6、进程地址空间
      • 2.7、Linux2.6内核进程调度队列
    • 3、自制Shell

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 和cdif (!target) target = Home();//第二个参数为NULL//改变当前工作目录chdir(target);//处理target为..的情况//重新获取当前路径char temp[1024];getcwd(temp, 1024);//更新当前环境变量PWDsnprintf(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主页

相关文章:

  • C# 属性设置为“get; private set;”好处
  • 亚信安慧AntDB:为数字化铺平道路
  • 详细了解网络通信流程、协议组成、编码方式、数据传输方式和途径、Http 协议的编码、cookie的使用和提取路径
  • 算法学习03:前缀和与差分(互逆)
  • MySQL高可用性攻略:快速搭建MySQL主从复制集群 !
  • MYSQL C++链接接口编程
  • Redis—5种基本数据类型
  • LZO索引文件失效说明
  • 【C++】每周一题——2024.3.3
  • MongoDB Helloworld For Window
  • b站小土堆pytorch学习记录——P14 torchvision中的数据集使用
  • 【Java EE 】认识文件与Java文件操作
  • JVM堆内存中新生代晋升到老年代的条件
  • 【机器学习】CIFAR-10数据集简介、下载方法(自动)
  • 为什么有了HTTP协议,还要有WebSocket协议?
  • 【许晓笛】 EOS 智能合约案例解析(3)
  • CentOS从零开始部署Nodejs项目
  • Git同步原始仓库到Fork仓库中
  • Intervention/image 图片处理扩展包的安装和使用
  • Java知识点总结(JDBC-连接步骤及CRUD)
  • oldjun 检测网站的经验
  • PHP CLI应用的调试原理
  • python_bomb----数据类型总结
  • Python学习之路13-记分
  • React Transition Group -- Transition 组件
  • ReactNativeweexDeviceOne对比
  • Webpack入门之遇到的那些坑,系列示例Demo
  • 搭建gitbook 和 访问权限认证
  • 近期前端发展计划
  • 看图轻松理解数据结构与算法系列(基于数组的栈)
  • 前端设计模式
  • 使用 Node.js 的 nodemailer 模块发送邮件(支持 QQ、163 等、支持附件)
  • 用 Swift 编写面向协议的视图
  • ​520就是要宠粉,你的心头书我买单
  • #pragma data_seg 共享数据区(转)
  • #pragma 指令
  • (33)STM32——485实验笔记
  • (一)UDP基本编程步骤
  • (转)linux 命令大全
  • (转)总结使用Unity 3D优化游戏运行性能的经验
  • (轉)JSON.stringify 语法实例讲解
  • :=
  • @DataRedisTest测试redis从未如此丝滑
  • @JsonFormat与@DateTimeFormat注解的使用
  • [4.9福建四校联考]
  • [Android开源]EasySharedPreferences:优雅的进行SharedPreferences数据存储操作
  • [AX]AX2012开发新特性-禁止表或者表字段
  • [C++]C++基础知识概述
  • [C++进阶篇]STL中vector的使用
  • [CentOs7]搭建ftp服务器(2)——添加用户
  • [Delphi]一个功能完备的国密SM4类(TSM4)[20230329更新]
  • [leetcode 双指针]
  • [LeetCode] 19. 删除链表的倒数第 N 个结点
  • [LeetCode] Binary Tree Preorder Traversal 二叉树的先序遍历
  • [LeetCode]Max Points on a Line