Linux进程

本文详细介绍了操作系统中的进程管理,包括进程的查看、状态(运行、睡眠、磁盘休眠、停止、死亡、僵死、孤儿)、优先级、环境变量、地址空间和进程创建与终止。重点讨论了进程的PCB(ProcessControlBlock),进程的状态转换,以及fork函数在创建子进程时的写时复制机制。此外,还提到了简易shell的实现过程,涉及命令行获取、解析和进程替换。
摘要由CSDN通过智能技术生成

目录

查看进程

进程状态

运行状态

睡眠状态

磁盘休眠状态

停止状态

死亡状态

僵死状态

孤儿进程

进程优先级

环境变量

PATH

​编辑

进程地址空间

进程创建

进程终止​​​​​​​

进程等待

进程程序替换

简易shell实现

获取命令行

解析命令行

建立子进程、替换子进程、父进程等待退出


就像各种管理系统一样,进程需要像被管理的对象一样先描述成具体的数据结构再进行组织,因此,进程分为内核数据结构以及进程的代码和数据。而内核数据结构中保存的便是进程属性的集合,该内核数据结构称为PCB(process control block),被保存在内存中。在Linux操作系统中PCB的结构体名称是task struct。

而task struct 中包含

标示符: 描述本进程的唯一标示符,用来区别其他进程。

状态: 任务状态,退出代码,退出信号等。

优先级: 相对于其他进程的优先级。

程序计数器: 程序中即将被执行的下一条指令的地址。

内存指针: 包括程序代码和进程相关数据的指针,还有和其他进程共享的内存块的指针

上下文数据: 进程执行时处理器的寄存器中的数据[休学例子,要加图CPU,寄存器]。

I/O状态信息: 包括显示的I/O请求,分配给进程的I/O设备和被进程使用的文件列表。

记账信息: 可能包括处理器时间总和,使用的时钟数总和,时间限制,记账号等。

其他信息
 

查看进程

我们可以通过ls /proc、ps、top命令来查看进程,例如ps -axj

当然,我们也可以通过管道来进行查看某些进程

例如我们新增一个程序

由于我们需要在该程序执行的过程中查看它的属性,因此我们在这里写了一个死循环来保证该进程能够一直执行

在创建好进程之后,我们可以新建一个窗口来查询该进程

 我们也可以将头部的属性名称显示出来

其中PID表示该进程的ID,PPID表示其父进程的ID,STAT表示进程状态。

我们也可以通过程序代码来查看该进程和父进程的id

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

pid_t getpid(void);
pid_t getppid(void);


进程状态

/*
* 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 */
};

在Linux源码中,包括以上6种状态(T和t可以归为一类)

运行状态

R表示运行状态,当进程在内存中运行或是处于内存的运行队列的时候,便是运行状态。

例如这样一个死循环

该进程的状态便是R状态。

睡眠状态

而当我们等待非CPU资源就绪的时候,称为阻塞状态,这里的非CPU资源就包括输入设备,储存器,输出设备等,例如当我们进行scanf或是printf的时候,就需要去等待输入设备或输出设备。

 

而当内存不足的时候,操作系统会通过适当的置换进程的代码和数据到磁盘(swap 分区)来解决这个问题,而被置换的进程所处的状态就叫做挂起状态。

而这两种状态都是在等待事件的完成(阻塞状态等待非cpu资源,而挂起状态等待自己的代码和数据被置换回内存继续运行),因此都被称为睡眠状态(S)也称作可中断睡眠。

磁盘休眠状态

当服务器压力过大的时候,操作系统会通过一些手段来杀掉一些进程,来节省空间。

而当进程与磁盘进行大量数据的IO时,该进程会处于磁盘休眠状态(D状态),该进程不可被中断,不可被被动唤醒,也不会被上面所说的过程被杀掉,只能等待IO的结束或是关机重启。

dd命令可以模拟该状态。

停止状态

顾名思义,就是进程被暂停了,我们可以通过发送SIGSTOP信号(kill -19)来停止进程,也可以通过发送SIGCONT信号(kill -18)来让进程继续进行,同时,在调试的过程中进程也处于停止状态。

死亡状态

具有瞬时性,无法查看。

僵死状态

当一个进程退出时,不逊于操作系统释放,该等待被父进程 检测的状态称为僵死状态。

fork函数

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

pid_t fork(void);

创建子进程,共享父进程后续的代码,创建失败返回-1,成功则给子进程返回0,给父进程返回子进程的PID。

我们可以利用这个函数来实现一下僵尸进程

可以看到,子进程sleep  3秒后该进程就会退出,而父进程处于死循环,因此,在查看进程状态时,三秒后子进程的状态变成了僵死状态 。

若是一直处于僵尸进程,该进程的PCB会被操作系统一直维护,从而导致内存泄漏,而为了避免内存泄漏,就需要父进程进行进程等待(后面讲)。

孤儿进程

当父进程退出后,未退出的子进程就被称作孤儿进程,该进程会被一号进程init回收。


进程优先级

  通过ps -la命令,我们可以看到PRI和NI这两个属性,分别代表了该进程的优先级和nice值。

优先级的值越小,优先级别就越高。

PRI表示原本的优先级加上nice值后得到的值,nice值可以被我们修改,修改范围为-20到19,具体操作为top、r、PID、nice值。

 

 

其他概念

竞争性:进程的数目远大于CPU资源,因此进程之间具有竞争性,为了高效完成任务,需要优先级来合理安排。

独立性:多进程之间需要独享资源,互不干扰(包括父子进程之间)。

并行:多进程在多CPU下同时运行。

并发:多个进程在一个CPU下采用进程切换的方式使得各个进程得以推进。

在进程执行的某些时刻,内核可以决定抢占当前进程(当前进程出让给另一个进程),并重新开始一个先前被抢占了的进程,这种决策叫做调度,是由内核中称为调度器的代码处理的。当内核选择一个新的进程运行时,我们说内核调度了这个进程。在内核调度了一个新的进程运行后,它就抢占当前进程,并使用一种称为上下文切换的机制来控制转移到新的进程。

寄存器中的所存储的临时数据称为进程的上下文,寄存器只有一份,而上下文有多份,在进程运行期间上下文不能被丢弃。

上下文切换主要分为三步,(1). 保存当前进程的上下文        (2). 恢复某个先前被抢占的进程被保存的上下文(保证该进程被切换回来后,可以按照之前的逻辑继续运行)        (3). 将控制传递给这个新恢复的进程。


环境变量

环境变量指在操作系统中用来指定操作系统运行环境的一些参数。

常见环境变量:

PATH:指定命令的搜索路径

HOME:指定用户的主工作目录

SHELL:当前Shell,通常为/bin/bash

我们可以通过echo $NAME查看环境变量

PATH

当我们执行系统的命令时,我们不需要使用路径,而当我们执行自己的程序时,却必须带路径,这是由于系统的命令所处的路径全都在PATH环境变量中。

我们可以通过export命令来自己设置环境变量的值,因此我们可以通过export PATH=$PATH:添加程序的地址来新增一条地址 。

除此之外,env指令为显示所有环境变量,unset指令为清除环境变量,set命令为显示本地定义的shell变量和环境变量

除了使用命令之外,我们还可以通过代码来获取环境变量

第一种方法,我们可以使用main的参数来获取(第三个参数 char *env[])

而第二种方法,我们可以通过第三方变量environ来获取

 而这两个环境表(env、environ)都是字符指针数组,每个指针指向每个环境变量字符串。而结尾为NULL。

我们还可以通过getenv来获取单个环境变量

 子进程继承父进程所有的环境变量


进程地址空间

在之前,我们了解过内存的空间布局

而当我们使用fork创建父子进程时

 可以看到,在fork创建了子进程后,子进程对变量a进行了修改,这导致了父进程和子进程的变量a的地址相同,但是a的值却是不相同的。

这是因为,在Linux下,进程的地址都是虚拟地址,当fork在新进程中返回时,新进程现在的虚拟内存刚好和调用fork时存在的虚拟内存相同。因此,父子进程即使地址相同,但所映射的物理地址并不相同,所存储的数据也就不相同。而这些虚拟地址就被称为虚拟地址空间。

每一个进程都有自己的地址空间,操作系统将所有的空间先描述再组织,因此地址空间也是一种内核数据结构。

首先,任务结构(PCB / task_struct)中的一个条目指向mm_struct,它描述了虚拟内存的当前状态。而其中重要的是pgb和mmap两个字段,mmap指向一个vm_area_structs(区域结构)的链表,其中每个结点都描述了当前虚拟地址空间的一个区域(代码区、数据区等)。

Linux内存映射机制_灵魂构造师的博客-CSDN博客_linux内存映射机制

 结点中各个字段的含义:

vm_start:指向该区域起始处     vm_end:指向该区域结束处      vm_prot:描述该区域的读写权限

vm_flags:描述该区域内的页面是与其他进程共享还是私有        vm_next:指向下一个结点

而pgb指向第一级页表的基址。

当我们想要读写进程地址空间中的数据等,就需要通过页表来将虚拟地址映射成物理地址。而每一个进程的页表,都会将其虚拟地址映射到物理内存的不同区域,这也就使得进程之间在内存方面不会互相干扰,具有独立性。

有了进程地址空间,首先,当我们进行非法的访问或映射时,操作系统都能够识别到,并终止该进程。因此也能够保护物理地址中其他进程的数据等。

其次,通过页表的映射,数据可以被加载到任意位置,也就能实现内存管理模块和进程管理模块的解耦合。同时,由于页表的存在,我们也可以将内存有序化。

同时,通过进程地址空间,我们可以在申请内存的时候只申请虚拟地址,暂时不分配内存地址,当该块空间被访问时再去申请内存地址,来通过延迟分配提高效率。


进程创建

在前面进程状态的时候,我们提到过fork函数的使用。

当fork函数被当前进程调用时,内核为新进程创建各种数据结构,并分配给它一个唯一的PID,为了给其创建虚拟内存,创建了当前进程的mm_struct、区域结构和页表的原样副本。它将两个进程中的每个页面都标记为只读,并将两个进程中的每个区域结构都标记为私有的写时复制。

当这两个进程中的任一个后来进行写操作时,写时复制机制就会创建新页面,更新页表条目指向这个新的副本,然后恢复这个页面的可写权限。

而子进程继承的是父进程的mm_struct、区域结构和页表的原样副本。因此代码也是全部继承的。但也同时继承了父进程的上下文,因此,子进程也随同父进程执行fork之后的代码,而不会去执行fork之前的代码。

而系统采用写时拷贝的原因也很简单,首先,写时拷贝能够在数据被写入的时候再进行申请,而没有被写入的数据父子进程公用空间,因此能够高效的使用内存。其次,系统无法预知哪些数据之后会被写入,因此我们也就无法提前将它们在创建子进程时拷贝。

同时,我们之前提到过,fork返回值为-1时表示进程创建失败。主要原因是系统中的进程太多或是实际用户的进程数超过了限制。


进程终止

当进程终止后,操作系统要释放进程申请的相关内核数据结构和对应的代码和数据。

而进程终止的情况分为三种

代码运行完毕,结果正确

代码运行完毕,结果不正确

代码异常终止

而进程退出的方法分为

正常终止:main函数返回(return)、exit函数、和_exit函数

#include<stdilb.h>

void exit(int status);
#include<unistd.h>

void _exit(int status);

main函数返回实质上也是调用的exit函数,而exit与_exit函数的区别在于,exit会调用_exit,但在调用之前,exit会执行用户定义的清理函数以及冲刷缓冲区、关闭流等。

而异常终止是出现在程序崩溃时,此时退出码没有意义

我们可以使用echo $?来获取最近进程的退出码

 

 

可以看到,_exit并不会冲刷出缓存区里的"Hello Linux"语句 

可以通过strerror函数来将错误码转换为错误信息

#include <string.h>
#include <errno.h>

char * strerror ( int errnum );

#include<stdio.h>
#include<string.h>
#include<errno.h>

int main(){
    int index=1;
    for(;index<140;index++){
        printf("errnum[%d]:%s\n", index, strerror(index));
    }
    return 0;
}


进程等待

在前面的进程状态中,我们提到过僵死状态,就是子进程退出而父进程还没将其回收的状态。而我们可以使用wait或是waitpid函数来将子进程回收。同时,这两个函数还可以获取子进程的退出信息来判断子进程退出时的状况。

#include<sys/types.h>
#include<sys/wait.h>

pid_t wait(int *status);

pid_t waitpid(pid_t pid, int *status, int options);

而 wait(&status) 等价于 waitpid(-1, &status, 0)

waitpid的三个参数中,pid表示需要等待的子进程的pid,若pid为-1,表示等待任意子进程。

status表示等待到的子进程的退出状态,我们在这里只看其低16位,其中高8位表示退出码,低7位表示终止信号,若终止信号不为0,表示子进程异常终止,此时退出码无效。而中间1位表示core dump标志。

我们可以通过位操作来获取退出码和终止信号,同时,也可以利用现有的函数来获取

WIFEXITED(status)//若为正常终止子进程返回的状态,则为真(终止信号为0)
WEXITSTATUS(status)//若正常退出,则提取子进程退出码
#include <stdio.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include <stdlib.h>

int main(){
  int id=fork();
  if(id==0){
    sleep(3);                                                                                                                                   
    exit(1);
  }
  else{
    int status=0, ret=0;
    ret=waitpid(-1,&status,0);
    if(ret>0&&WIFEXITED(status))
      printf("child exit code:%d\n", WEXITSTATUS(status));
    else if(ret>0)
      printf("sig code:%d\n", status&0x7f);
  }
  return 0;
}

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

int main(){
  int id=fork();
  if(id==0){
    printf("child pid:%d\n", getpid());
    while(1);
    exit(1);
  }
  else{
    int status=0, ret=0;
    ret=waitpid(-1,&status,0);
    if(ret>0&&WIFEXITED(status))
      printf("child exit code:%d\n", WEXITSTATUS(status));
    else if(ret>0)
      printf("sig code:%d\n", status&0x7f);
  }
  return 0;
}

 

 options:WNOHANG表示非阻塞等待(define WHOHANG 1)若子进程没有结束,则返回0。

                0表示阻塞等待,一直等待,直到子进程结束

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

int main(){
  int id=fork();
  if(id==0){
    sleep(3);
    exit(1);
  }
  else{
    int status=0, ret=0;
    while(1){
      ret=waitpid(-1,&status,WNOHANG);
      if(ret==0){
        printf("waiting\n");
        sleep(1);
      }
      else{
        if(WIFEXITED(status))
          printf("child exit code:%d\n", WEXITSTATUS(status));
        else 
          printf("sig code:%d\n", status&0x7f);
        break;
      }
    }
  }
  return 0;
}


进程程序替换

我们可以用exec函数来将进程的程序替换成另一个程序。进程程序替换只会替换该进程的用户空间代码和数据,而不会改变内核数据结构中进程的属性等。

exec函数包含六个函数

#include<unisted.h>

int execl(const char *path, const char *arg, ...);
int execlp(const char *file, const char *arg, ...);
int execle(const char *path, const char *arg, ..., char *const envp[]);
int execv(const char *path, char* const argv[]);
int execvp(const char *file, char* const argv[]);
int execve(const char *file, char* const argv[]);

 其中,path代表替换程序的路径+文件名,file代表文件名(需要在环境变量PATH中),arg表示命令行的参数,以单个参数的形式存在可变参数列表中,以NULL结尾, argv表示命令行的参数,以数组形式作为参数,envp表示环境变量数组。而前五个函数最终都会调用第六个函数execve

这些函数在出错时返回值为-1,而成功时没有返回值。

通常,我们通过fork创建子进程来进行程序替换,这样的话,不会影响父进程,使父进程能够聚焦在读取数据、解析数据、指派进程执行代码等行为上。

在进行进程程序替换时,子进程进行了大量的写入,因此在这时发生了写时拷贝,将父子进程的数据和代码分离。

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

int main(){
  int id=0, ret=0, status=0;
  id=fork();
  if(id==0){
    execl("/bin/ls", "ls", "-a", "-l", NULL);
    exit(0);
  }
  else{
    ret=wait(&status);
  }
  return 0;
}


简易shell实现

 shell简单来说,主要分为五个过程

获取命令行

解析命令行

建立一个子进程

替换子进程

父进程等待子进程退出

首先,在获取命令行之前,前面还有

#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<string.h>
#include<sys/wait.h>
#include<sys/types.h>
int main(){
  int id=0, ret=0, status=0;
  while(1){
    printf("[szt@localhost myshell]$ ");
    fflush(stdout);//没有回车,需要冲刷缓存区
  }
}

获取命令行

#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<string.h>
#include<sys/wait.h>
#include<sys/types.h>

//命令行字符串
char cmd_line[1024];

int main(){
  int id=0, ret=0, status=0;
  while(1){
    printf("[szt@localhost myshell]$ ");
    fflush(stdout);
    memset(cmd_line, '\0', sizeof cmd_line);
    if(fgets(cmd_line, sizeof cmd_line, stdin)==NULL)
      continue;

  }
  return 0;
}

解析命令行

在我们学习进程替换时,我们知道命令行是以字符串数组的形式作为参数,因此我们需要将命令行字符串以空格做分隔形成字符串数组。而想要实现这一功能我们又现成的函数strtok

#include <string.h>

char * strtok ( char * str, const char * delimiters );
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<string.h>
#include<sys/wait.h>
#include<sys/types.h>

//命令行字符串
char cmd_line[1024];
//命令行参数字符串数组
char *g_argv[32];

int main(){
  int id=0, ret=0, status=0;
  while(1){
    printf("[szt@localhost myshell]$ ");
    fflush(stdout);

    //获取命令行
    memset(cmd_line, '\0', sizeof cmd_line);
    if(fgets(cmd_line, sizeof cmd_line, stdin)==NULL)
      continue;

    //解析命令行
    //去除命令行字符串尾部的回车
    cmd_line[strlen(cmd_line)-1]='\0';
    g_argv[0]=strtok(cmd_line, " ");
    int index=0;
    while(g_argv[++index]=strtok(NULL, " ")) ;


  }
  return 0;
}

建立子进程、替换子进程、父进程等待退出

#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<string.h>
#include<sys/wait.h>
#include<sys/types.h>

//命令行字符串
char cmd_line[1024];
//命令行参数字符串数组
char *g_argv[32];

int main(){
  int id=0, ret=0, status=0;
  while(1){
    printf("[szt@localhost myshell]$ ");
    fflush(stdout);

    //获取命令行
    memset(cmd_line, '\0', sizeof cmd_line);
    if(fgets(cmd_line, sizeof cmd_line, stdin)==NULL)
      continue;

    //解析命令行
    //去除命令行字符串尾部的回车
    cmd_line[strlen(cmd_line)-1]='\0';
    g_argv[0]=strtok(cmd_line, " ");
    int index=0;
    while(g_argv[++index]=strtok(NULL, " ")) ;
    
    id=fork();
    if(id==0){
      execvp(g_argv[0], g_argv);
      printf("-bash: %s: command not found\n", g_argv[0]);
      exit(1);//当替换失败时,才会执行子进程原本后来的代码
    }
    else{
      ret=wait(&status);
    }
  }
  return 0;
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

finish_speech

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

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

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

打赏作者

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

抵扣说明:

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

余额充值