Linux 【进程】

目录

1.冯诺依曼体系结构(硬件结构)

冯诺依曼结构计算机的基本工作原理是什么?

举例说明数据的流动过程

2.操作系统简述

总结:先描述,再组织

3.进程

4.通过代码创建子进程:fork

进程终止

进程等待

waitpid:

进程具有独立性,为什么父进程能拿到子进程退出码数据?

5.Linux操作系统进程状态

环境变量

批量化注释+去注释

6.进程替换

exec系列函数

 自制shell


1.冯诺依曼体系结构(硬件结构

Linux 【进程】_第1张图片

存储器指的是:物理内存

冯诺依曼体系结构中数据输入设备的有:键盘、摄像头、话筒、磁盘(读取)、网卡等

冯诺依曼体系结构中数据输出设备的有:显示器、磁盘、音响、网卡等

中央处理器(CPU)的运算器进行运算的两种情况:算术运算,逻辑运算;

控制器:CPU可以响应外部事件(拷贝数据到内存)


硬件层面上的数据流向

CPU读取数据(数据+代码),都是要从内存中读取。站在数据的角度,我们认为CPU不和外设直接交互。外设(输入或输出设备)要输入或者输出数据,也只能写入内存或者从内存中读取。

冯诺依曼体系结构最典型的特征是所有的外设(输入单元和输出单元)产生的数据,必须将数据写入存储器。存储器本身没有计算能力,CPU通过某种方式来访问存储器,将数据读取进CPU内部,CPU进行运算器运算,控制器控制,最终将计算完的结果写回到内存当中。最后将最终的结果显示到显示器当中。

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


冯诺依曼结构计算机的基本工作原理是什么?

计算机就是为了完成指定的数据处理,而通过指令按指定流程完成指定功能,指令的合集就是一段程序。计算机就是按照指定的指令执行流程完成对指定数据的处理

简写为:存储程序、程序控制


举例说明数据的流动过程

由于程序已经在内存中运行,当我们发送消息时,通过键盘输入(输入设备),输入的数据放到内存,cpu经过计算回写到存储器,存储器将数据定期刷新出去,此时的输出设备叫做网卡。


接收消息的电脑接受数据,此时的输入设备是网卡,网卡收到的数据放到内存里,然后经过cpu运算,把处理完的结果再写回存储器里,存储器定期将数据刷新到输出设备,此时的输出设备是显示器。

总结:冯诺依曼结构规定了计算机硬件体系结构走向,如果数据流向不清楚,想象一下硬件体系结构是什么样子,程序的数据流走向也就清楚了(硬件决定了走向,例如胳膊只能向上弯)


2.操作系统简述

操作系统是“搞管理”的软件,与硬件交互,管理所有的软硬件资源,为用户程序(应用程序)提供一个良好的执行环境,操作系统包括:

1.内核,也就是操作系统(进程管理,内存管理,文件管理,驱动管理)

2.其他程序(例如函数库,shell程序,接口,库等等)

总结:先描述,再组织

如何管理软硬件?管理者(操作系统)拿到被管理者(硬件)的核心数据,来进行管理决策才最重要的,先对被管理对象进行描述,在根据描述类型,定义对象,可以把对象组织成数组或者其他数据结构。也就是对数据的管理工作,变成了对数组或者其他数据结构的增删改查

管理:是对被管理对象数据的管理

1. 描述起来,用struct结构体 2. 组织起来,用链表或其他高效的数据结构


系统调用和库函数

操作系统会展开部分接口,供开发使用,由操作系统提供的接口,叫做系统调用。

对系统调用进行适度封装,形成库,更加方便开发者或者用户使用


3.进程

管理大量进程:先描述,再组织(描述:Linux内核中操作系统会为每个进程申请一个结构体:PCB)

该结构体保存了对应进程所有的属性,方便操作系统管理进程。组织:对进程的管理,变成了对进程PCB结构体管理(增删查改),等于对数据结构管理(相当于学生管理系统,学生在学校不代表就是该校学生,配合管理系统信息才能确定)

总结:进程=对应的代码和数据+进程对应PCB结构体

PCB(进程控制块 process control block)

Linux系统中PCB名称:struct task_struct{};

查看进程状态:ps ajx | head -1 && ps axj | grep 'mytest'

写一个死循环,查看mytest状态

PID:进程ID,代表当前进程的ID值

grep --color =auto mytest:最后一个是grep进程,查找mytest,grep中包含该关键字,通常一起显示

ls /proc:把所有内存级进程数据以文件形式显示出来(目录文件是动态的)

Linux 【进程】_第2张图片 死循环PID程序1006运行运行

获取当前进程pid的系统调用函数:getpid

其中pid_t是操作系统提供的属性类型,实际是一个无符号整数

知道pid,还可以使用kill命令终止指令:kill -9 pid

获取当前进程的父进程的系统调用函数:getppid

查看当前进程父进程可以发现:ppid是bash,就是shell命令行,外壳程序

在命令行中运行命令或者程序时,父进程永远都是bash(执行命令时,所有的命令都是以子进程的形式去运行,shell以创建子进程的方式运行一段命令或程序)

Linux 【进程】_第3张图片


4.通过代码创建子进程:fork

Linux 【进程】_第4张图片

fork有两个返回值:

1.失败的时候返回-1

2.成功时  a.给父进程返回子进程pid b.给子进程返回0

为什么fork有两个返回值?

1.因为fork是函数,return语句被其内部实现的创建子进程代码两个执行流各自执行,返回值就有两个

可以看到,printf只是一条语句,却被打印了两次

Linux 【进程】_第5张图片

Linux 【进程】_第6张图片

fork之后,变为两个进程,一个是父进程(自己),一个是子进程

Linux 【进程】_第7张图片

 Linux 【进程】_第8张图片

父进程返回子进程pid;子进程返回0

Linux 【进程】_第9张图片

fork用法

由于fork之后代码是父子共享,通常要用 if 进行分流(id值不同,父子进程执行不同的代码)

Linux 【进程】_第10张图片

进程终止

进程终止时,OS做了什么?

释放进程申请的相关内核数据结构和对应的代码+数据

main函数返回值有什么意义?return 0的意义是什么

main函数的返回值,并不是总是0,可以是其他值,main函数的0指的是进程的退出码,用来表示进程是否正确返回(代码跑完结果是否正确),退出码为0代表运行结果正确,非0表示运行结果错误

获取最近一次进程执行完的退出码:echo $?

main函数返回值意义是返回给上一级进程,用来评判该进程执行结果用。如果关心返回值,可以对返回值做判断决定结果是否正确,可以方便程序运行不正确时定位错误原因

退出码转换成字符串错误信息:strerror

Linux 【进程】_第11张图片

Linux 【进程】_第12张图片 对应错误码2

程序崩溃

退出码无意义,进程只要跑起来,就是OS范畴,本质是OS杀掉了进程,本质是发送信号的方式

exit和_exit

exit直接退出进程并返回退出码,和return区别是return在main函数才是退出,函数return返回值。

_exit是系统调用函数,两者的区别是exit调用了_exit,并且执行了用户定义的清理函数和刷新缓冲区,关闭流(stdin,stdout,stderror),从侧面可以看出缓冲区是由C标准库维护的

进程等待

父进程创建了子进程,如果不管子进程变成僵尸进程会导致内存泄漏,kill -9也没办法杀掉一个已经死去的进程。父进程通过进程等待的方式,既可以回收子进程,也可以获取子进程退出时的信息

Linux 【进程】_第13张图片

进程等待的方法

wait:

阻塞式等待一个进程直到状态发生变化(如果在fork子进程中继续fork子进程,由于wait回收了第一次fork的子进程,第二次fork子进程并不会僵尸而是被1号进程领养) 

#include
#include
pid_t wait(int*status);

status参数: 输出型参数,获取子进程退出状态,不关心则可以设置成为NULL

返回值: 成功返回被等待进程pid,失败返回-1。

Linux 【进程】_第14张图片

waitpid:

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

返回值pid_t:

当正常返回的时候waitpid返回收集到的子进程的进程ID;

如果设置了选项WNOHANG,而调用中waitpid发现没有已退出的子进程可收集,则返回0;

如果调用中出错,则返回-1,这时errno会被设置成相应的值以指示错误所在;

pid:

pid: Pid=-1,等待任一个子进程。与wait等效。

Pid>0.等待其进程ID与pid相等的子进程。

options:

默认为0,表示阻塞式等待(父进程在此期间只等待子进程退出,只有子进程先退出父进程才调waitpid)

WNOHANG,表示非阻塞式等待: 若pid指定的子进程没有结束,则waitpid()函数返回0,不予以等待。若正常结束,则返回该子进程的ID。

阻塞等待:在内核中阻塞,等待被唤醒

非阻塞等待:父进程通过调用waitpid来进行等待,如果子进程没有退出,waitpid系统调用马上返回,继续执行waitpid之后的代码(非阻塞调用的轮询检测方案)

Linux 【进程】_第15张图片

Linux 【进程】_第16张图片 非阻塞调用的轮询检测方案

status输出型参数(传入变量取地址获得信息):

参数NULL表示不关心

status并不是按照整数整体使用,而是按照比特位方式将32个比特位进行划分

低16位的次低八位,表示子进程的退出码,用来表示进程结果是否正确

两个宏:

WIFEXITED(status): 若为正常终止子进程返回的状态,则为真。(查看进程是否是正常退出) WEXITSTATUS(status): 若WIFEXITED非零,提取子进程退出码。(查看进程的退出码)

Linux 【进程】_第17张图片

Linux 【进程】_第18张图片

最低7个比特位,表示进程收到的信号,如果进程崩溃,是OS发送了信号导致进程挂掉,那我们可以通过查看OS是否有发送信号来判断进程是否崩溃/正常跑完

程序异常,可能不光是本身代码问题,有可能收到信号结束

Linux 【进程】_第19张图片

Linux 【进程】_第20张图片

进程具有独立性,为什么父进程能拿到子进程退出码数据?

子进程退出变成僵尸进程后,还会保留对应进程的PCB信息task_struct,里面保留了任何进程退出时的退出信息,一直保留直到wait\waitpid读取。

进程退出时退出码和退出信号会写入task_struct中等待别人读取

Linux 【进程】_第21张图片

注意事项

如果子进程已经退出,调用wait/waitpid时,wait/waitpid会立即返回,并且释放资源,获得子进程退出信息。

如果在任意时刻调用wait/waitpid,子进程存在且正常运行,则进程可能阻塞。

如果不存在该子进程,则立即出错返回。

5.Linux操作系统进程状态

OS理论状态:

新建:初步具备进程相关数据结构,没有入队列中时叫做新建状态,字面意思

运行:进程的task_struct结构体在CPU运行队列中排队等待调度,就叫做运行态。

阻塞:等待非CPU资源就绪时(例如网卡硬盘传输,或者scanf时等待键盘输入),就叫做阻塞状态

挂起:和进程访问某种资源关系不大,当内存不足时,操作系统将长时间不执行的进程的代码和数据,通过适当的置换到磁盘中,此时进程的状态就叫做挂起

OS实际进程状态

一个进程可以有几个状态(在 Linux内核里,进程有时候也叫做任务)

Linux 【进程】_第22张图片

R 运行状态(running):

并不意味着进程一定在运行中,它表明进程要么是在运行中要么在运行队列里

S 睡眠状态(sleeping):

意味着进程在等待事件(某种资源,类似阻塞状态)完成(这里的睡眠有时候也叫做可中断睡眠 (interruptible sleep))

while1循环时,我们看到死循环进程状态是R+;while1循环内部printf时,我们看到死循环进程状态是S+;和R状态不同的原因是:

在cpu看来运行printf时间只需要纳秒级别,其实有运行状态,只不过显示器在冯诺依曼体系结构中处于外设,进程向显示器打印时,printf的PCB放入CPU时间比运行队列速度快,导致看到的大部分是S状态;

当压力过大内存严重不足时,操作系统可能会通过一定手段干掉一些进程(S睡眠状态),起到节省空间的作用

可中断睡眠

改变sleep状态,可以发送信号来更改状态

状态后带+

带+:意味任务处于前台进程(前台进程一启动,执行其他命令失效,可以被ctrl+c终止,占用bash对话框)

不带+:需要后台运行时,在执行时加 &即可,随后显示后台进程pid,不可被ctrl+c终止,bash可以使用

D 磁盘休眠状态(Disk sleep)

有时候也叫不可中断睡眠状态(uninterruptible sleep),在这个状态的进程不可被被动唤醒,通常会等待IO的结束。

当压力过大内存严重不足时,操作系统无法干掉D状态

T暂停/调试状态(stopped/tracing stop):

可以通过发送 SIGSTOP 19号信号给进程来停止(T)进程。

被暂停的进程可以通过发送 SIGCONT 18号信号让进程继续运行。kill -18 pid

打断点停下调试==进程暂停

X死亡状态(dead):

这个状态瞬时性很强,通常只是一个返回状态,你不会在任务列表里看到这个状态。

Z(zombie)-僵尸进程

是什么:一个进程已经退出,但是还不允许被OS释放其所属资源,处于一个被检测的状态(进程状态检测一般是父进程或者操作系统检测)

当进程退出并且父进程没有读取到子进程退出的返回代码时就会产生僵尸进程,僵尸进程会以终止状态保持在进程表中,并且会一直在等待父进程读取退出状态代码。 父进程还在运行,但父进程没有读取子进程状态,子进程进入Z状态

为什么要维持该状态:为了让父进程和OS回收(最终Z-->X)

僵尸进程危害:不被任何进程读取,也不会和外设交互,但是其创建的PCB进程基本信息一直存在,造成内存资源浪费(内存泄漏)

循环监测mytest进程状态:

while :; do ps axj | head -1 && ps axj | grep mytest | grep -v grep; sleep 1; echo "#############################################"; done

#include 
  2 #include 
  3 #include                                         
  4 int main()
  5 {
  6    pid_t id = fork();
  7    if(id < 0)
  8    {
  9       perror("fork");
 10        return 1;
 11    }
 12     else if(id > 0)
 13     { //parent
 14         while(1)
 15         {
 16 
 17          printf("I am parent,pid%d,ppid%d\n", getpid(),getp    pid());
 18          sleep(3);
 19         }
 20     }
 21    else
 22    {
 23      //child
 24      while(1)
 25      {
 26        printf("I am child,pid :%d, ppid %d\n", getpid(),get    ppid());
 27        sleep(3);
 28        exit(-1);   
 29      }
 30    }
 31     return 0;
 32    }  

孤儿进程

父进程提前退出,子进程就称之为“孤儿进程”,孤儿进程此时被1号init进程(系统本身)领养,init进程进行回收

Linux 【进程】_第23张图片

如果父进程退出,此时子进程从前台进程转化为后台进程,使用kill -9即可(照常输入)

杀掉子进程后进入Z状态,直接被操作系统回收


环境变量

main函数最多有三个参数(int argc,char* argv[],char* env[]),前两个是命令行参数(启动程序时,传入的参数选项,意义是同样一个参数,通过传入不同的选项,使用同一个程序的不同子功能),最后一个是环境变量参数(每一个进程启动时,启动该进程的进程传递给main函数的环境变量信息,都可以以该参数传导)

env保存环境变量字符串,以指针数组的形式维护起来

Linux 【进程】_第24张图片 获得环境变量方式

环境变量的参数一般是由父进程继承导入,默认所有的环境变量都会被子进程继承

批量化注释+去注释

注释:ctrl+v进入视图模式,shift+g批量化选中所有内容,大写I+双//+esc即可

去注释:ctrl+v,使用hjkl键上下左右选中,然后d即可

6.进程替换

fork创建子进程后如果想执行不同的代码(不是父子执行不同的代码段),就需要用到程序替换。

注意事项:

1.该进程的用户空间代码和数据被新程序替换。

2.进程替换没有创建新进程(内核数据结构+代码+数据)

3.通过特定的接口:exec系列函数,物理内存加载磁盘上全新的代码和数据,重新建立页表映射,放到调用进程的地址空间

exec系列函数

Linux 【进程】_第25张图片

exec函数程序替换后,调用成功后,加载新的程序。此操作会将当前进程的所有代码和数据进行替换(包括已执行和未执行)

调用出错返回值-1

没有成功的返回值,因为成功后exec本身也被替换。

l(list) :传递形式像

v(vector) :传递形式像

p(path) : 自动搜索环境变量,从环境变量中寻找PATH

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

使用

const char* path表示文件名路径

系统命令其实也是文件路径下的可执行程序

Linux 【进程】_第26张图片

 ...表示可变参数列表,可以传入多个不定参数。命令行如何执行,参数列表对应填入命令(ls -a -l)

argv[0]代表ls,argv[1]表示-a,是一个个的字符串。最后必须以NULL结尾

execve

execve才是系统调用,以上的其他exec系列函数由系统封装,以满足不同场景

execl(list,像链表结点一样传参)

Linux 【进程】_第27张图片

 Linux 【进程】_第28张图片

execlp(path不用带路径,直接从环境变量中寻找)

第一个ls代表要执行谁,目的为了找到程序,第二个ls表示如何执行,传递选项

Linux 【进程】_第29张图片

Linux 【进程】_第30张图片

execv(vector,先把需要执行的命令构建成数组)

 char *const agrv[]是指针数组,数组内容是char*指向一个个字符串

Linux 【进程】_第31张图片

Linux 【进程】_第32张图片

Linux 【进程】_第33张图片

 

 execle

Linux 【进程】_第34张图片 环境变量是K/V模型 Linux 【进程】_第35张图片 获取环境变量getenv

 

如何执行自己编写的程序

绝对路径和相对路径都可以实现

Linux 【进程】_第36张图片

Linux 【进程】_第37张图片 mycmd.c

 

execlp可以执行其他语言程序,功能是加载器的底层接口,可以执行任何程序

 

 自制shell

shell运行原理:父进程解析命令+等待子进程执行,子进程执行命令

思路:

1.首先一定是死循环(常驻进程)

2.打印一行命令行提示符

3.获取用户输入的指令和选项(ls -a),需要用到fgets,从特定的文件流中获取字符串数据(自动添加\0)放入到缓冲区中,大小是size

4.命令行解析,把指令拆分成子串:strtok (把空格设置为\0,每个子串起始位置用指针标识)

代码实现

次模式删除需要ctrl+del

#include 
#include 
#include                                                                                                                                      
#include 
#include 
#include 

#define SIZE 32
#define NUM 1024
char _buffer[NUM];//保存完整的命令行
char* g_argv[SIZE];//保存打散之后的命令行字符串,把子串的起始地址保存起来
    
    
    int main()
   {
      while(1)
    {
      printf("[root@testshell]# ");
      fflush(stdout);
    
      memset(_buffer,'\0',sizeof(_buffer));
      if(fgets(_buffer,sizeof _buffer,stdin) == NULL )
      {
        continue;
      }
     _buffer[strlen(_buffer)-1] = '\0';
//读取字符串时把\n也读取到了,此时字符串内容是xxxx\n\0,找到\n位置-1下标置为\0;
      g_argv[0] = strtok(_buffer," ");//strtok要求必须字符串,得是双引号
    //  g_argv[1] = "--color=auto";
      int i = 1;
      if(strcmp(g_argv[0],"ls") == 0)
      {
        g_argv[i++] = "--color=auto"; //增加颜色
      }

   while( g_argv[i++] = strtok(NULL," "));
//strtok的用法,第一次调用要传入要传入原始字符串,第二次如果继续解析相同字符串,传入NULL
   //  int j = 0;//测试是否成功
   // for(j = 0;g_argv[j];++j)//g_argv[i]最后肯定以NULL结尾
   //     printf("g_argv[%s]\n",g_argv[j]);
       
      pid_t id = fork();
      if(id == 0)
      {
        execvp(g_argv[0],g_argv);//带p后就不用带路径
        exit(1);
      }
     int status = 0;
      pid_t ret = waitpid(id,&status,0);
      if(ret > 0)
      {
        printf("exit code:%d",WEXITSTATUS(status));
      }
    }
      return 0;
   }           

 Linux 【进程】_第38张图片

 

退回命令

 自制的shell无法完成返回上级目录操作,本质是子进程返回而父进程没有返回,只会影响当前子进程

Linux 【进程】_第39张图片

 

chdir修改工作目录 

Linux 【进程】_第40张图片

Linux 【进程】_第41张图片

Linux 【进程】_第42张图片

你可能感兴趣的:(Linux,linux,1024程序员节)