Linux 进程概念 (上)

                  

 好久没更新Linux博客了~

      Linux是一个非常综合的学科,学起来博主感觉比C++还要难上一截。主要是有很多概念没听过~其中还涉及一些硬件知识,Linux是跨学科的!不过不要怕,正是因为难,才能体现我们与其他人的差距。

目录

冯诺依曼体系结构

注意

冯诺依曼体系结构优点

更进一步理解

操作系统(Operator System)

OS是什么?

为什么要有OS?

OS怎么办?管理!

总结

系统调用和库函数概念

进程

进程是什么?

进程 vs 程序

​编辑 task_ struct内容分类

 上下文数据

查看进程

 通过系统调用获取进程标示符

通过系统调用创建进程-fork初识

fork

如何理解fork创建子进程

进一步理解

fork的返回值

进程状态

下面的状态在kernel源代码里定义

R运行状态(running)

S睡眠状态(sleeping)

D休眠状态(Disk sleep)

T停止状态(stopped)

X死亡状态(dead)

Z僵尸状态(zombie)

进程状态代码的模拟

R运行状态

S睡眠状态

T停止状态

Z僵尸状态

孤儿进程


冯诺依曼体系结构

我们常见的计算机,如笔记本。我们不常见的计算机,如服务器,大部分都遵守冯诺依曼体系。

注意:我们在这里只需要关心数据信号 。

计算机是由一个个的硬件组成的:

1、输入设备: 键盘、磁盘、网卡、显卡、话筒、摄像头等

2、输出设备:显示器、磁盘、网卡、显卡、音响等

3、存储器:内存

4、运算器&&控制器:CPU(中央处理器)

注意

1、我们发现有些硬件既是输入设备,又是输出设备,比如:磁盘、网卡...

2、对于存储器,在这里注意,不是磁盘!!这也是很多人误认为的。实际上存储器是内存,也就是我们常说的内存条。

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

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

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

冯诺依曼体系结构优点

老铁们可曾听过木桶理论?

                          

       一个木桶装多少水不是由最长的那一个木板决定的,而是由最短的那一个木板决定的。计算机设计有很多是来源于生活启发的(待会将OS更能体现)。也就是说,计算机计算速度也是受最慢的硬件一定程度的限制。

        磁盘(输入输出设备)读取和写入时间单位是ms甚至是s级别的。CPU运行速度是纳秒级别的。两者之间的速度差是非常大的,如果让CPU直接与外设打交道,那么CPU绝大多数时间都在等外设就绪。这对于CPU资源无疑就是浪费!所以冯诺依曼就设计出了一种结构,所有设备都只能和内存打交道。

       内存的运行速度比外设是高很多的。存储器首先从输入设备读取数据,CPU从存储器中拿数据进行处理之后返再还给存储器,最后存储器把数据交给输出设备。这样做的话,CPU就不会花费大量时间等待了。有了内存,CPU就不需要直接与外设打交道了!内存是体系结构的核心设备!

实际上,离CPU越近的设备运行越快,并且越贵~

总结:冯诺依曼体系结构大大提高了计算机运行速度,同时降低了计算机成本。

更进一步理解

     对冯诺依曼的理解,不能停留在概念上,要深入到对软件数据流理解上,请解释,从你登录上qq开始和某位朋友聊天开始,数据的流动过程。从你打开窗口,开始给他发消息,到他的到消息之后的数据流动过程。如果是在qq上发送文件呢?

操作系统(Operator System)

OS是什么?

一款专门针对软硬件资源进行管理工作的软件。

为什么要有OS?

方式-> 对下:管理好软硬件资源。

目的-> 对上:给用户提供稳定的、高效的、安全的得运行环境。

OS怎么办?管理!

对于管理老铁们肯定不陌生。生活中我们也有管理的情况。那么OS系统怎么管理好各种软硬件资源呢?我们先从现实联系一下~

 我们进一步联系到OS上:

这就好比OS相当于校长(决策者)、驱动程序相当于辅导员(执行者)、硬件相当于学生(被管理者)。 

站在校长的角度:

如何聚合同一个学生的数据  ---  先描述

如何将多个学生的聚合数据之间产生关联 ---  使用数据结构组织起来 

小总结:

通过以上了解,我们就有了结论。OS如何管理进程呢??先描述,再组织! 

描述:描述进程的结构体--PCB(进程控制块)

组织:将被管理者对象使用特定的数据结构(比如红黑树、哈希表等)组织起来。

所以说,数据结构这门学科就是从操作系统衍生出来的~

总结

我们再来探索一下:OS为用户提供良好的运行环境。那么OS是如何提供的呢??

       这里有一个很重要的概念:OS不相信任何人! 那我们疑惑了,OS既然不相信任何人那为什么还要给我们提供服务呢?这就好比银行,银行不相信任何人,但是还要服务我们存钱取钱,假如银行相信我们的话,那银行的防弹玻璃和保安是防谁的呢?还不如干脆直接把钱放到门口,我们自己来存取,哈哈,开个玩笑。

     OS也是一样,它不相信任何用户,但还是要提供服务。这就要求用户通过一定方式来让OS帮我们做事。这个方式就是:系统调用接口(system call)!!

这个博主在以后的博客会不断介绍的一些系统调用函数的~

系统调用和库函数概念

       在开发角度,操作系统对外会表现为一个整体,但是会暴露自己的部分接口,供上层开发使用,这部分由操作系统提供的接口,叫做系统调用。
       系统调用在使用上,功能比较基础,对用户的要求相对也比较高,所以,有心的开发者可以对部分系统调用进行适度封装,从而形成库,有了库,就很有利于更上层用户或者开发者进行二次开发

进程

有了前面的知识储备,我们学进程会更好理解一些~

课本上的概念:加载到内存的程序,就叫做进程。

进程是什么?

补充知识:

课本描述:OS上面,PCB-进程控制块,就是一个结构体类型。

在Linux系统中,PCB -> struct task_struct{  //进程的所有属性 } 

PCB和 struct task_struct之间的关系就好比shell -> bash、水果->苹果之间的关系。struct task_struct是对应一款具体的操作系统的PCB结构。

注意:task_struct是Linux内核的一种数据结构,它会被装载到RAM(内存)里并且包含着进程的信息。

我们先来创建一个进程观察一下:

代码:

                              

 我们来查看进程:

[cyq@VM-0-7-centos test]$ ps axj | grep test

我们看到了一个进程./test。

其中第二行呢也是一个进程。因为我们在输入指令时本身也是程序加载后创建出来的进程,我们在这里先不用关心。

 注意:曾经我们所有启动程序的过程,本质上都是在系统上创建进程。

进程 vs 程序

我们再来进一步分析并理解进程:

 task_ struct内容分类

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

比如进程编号PID,每个进程都有属于自己的编号,且不相同,就是为了和其它进程进行区分。(待会代码会演示)
状态: 任务状态,退出代码,退出信号等。

任务状态比如运行态、休眠态等等;退出信号博主在以后的博客会详细讲解~
优先级: 相对于其他进程的优先级。

是决定进程先后被调度的顺序问题。  vs  权限(决定的是能还是不能)
程序计数器: 程序中即将被执行的下一条指令的地址。

比如我们的代码在运行,我们正当执行当前行代码,这时候会有一个PC指针指向下一行将要执行的代码,也就是提前要记录要执行代码的下一行的位置。

            
内存指针: 包括程序代码和进程相关数据的指针,还有和其他进程共享的内存块的指针
上下文数据: 进程执行时处理器的寄存器中的数据。
I/O状态信息: 包括显示的I/O请求,分配给进程的I/O设备和被进程使用的文件列表。
记账信息: 可能包括处理器时间总和,使用的时钟数总和,时间限制,记账号等。

比如时间片。当一个进程时间片到了,就会被切走,时间片信息也是被记账信息记录的。
其他信息

 上下文数据

上下文数据概念是非常重要的,也是后面学习线程的重要基础。

我们先引入概念,时间片和进程切换运行:

       CPU调度一个进程时不是把一个进程的代码执行完了才结束再去调用下一个进程,因为这么做的话的后果:如果一个进程需要执行2min,那么在这2min内,其余进程都在挂起等待状态,这样的话其余进程代码执行没有被推进,这样做不太合理!

那么CPU是怎么调度多个进程呢?

进程A的时间片到后,开始执行进程B:

        当然,CPU肯定不会那么傻,又重新执行A进程的代码。当进程A被切走时,首先进程A会保存其CPU寄存器内处理的临时数据,即保存上下文数据

       等下次进程A再次被调用时,进程A会告诉CPU:"先等等,我有上一次未执行完的临时数据~",然后进程A把保存在PCB里面的数据加载到CPU寄存器中,即恢复上下文数据。

这样以后CPU就接着上一次进程A之星代码的进度继续执行。

通过上下文,我们能感受到进程是被切换的!!生活中,比如我们电脑运行qq、ppt、浏览器的时候,我们可能会认为他们(这些进程)是同时运行的,实际上这些进程就是CPU不断切换调度进程来实现的,只不过时间片时间很短加上CPU运行速度很快,给我们带来了进程共同执行的错觉。

保存上下文和恢复上下文:为了让你做其他事,但不耽误当前,并且当你想回来的时候,可以接着之前学习的内容,继续学习。 

查看进程

进程的信息可以通过 /proc 系统文件夹查看

[cyq@VM-0-7-centos test]$ ls /proc

 感觉几乎看不懂,实际上那些数字时进程标识符PID。

我们来查看一个具体的进程:

先来执行这段代码:

 #include<stdio.h>

   int main()
   {
     while(1)
     {}                                                                                                     
     return 0;
   }
   
[cyq@VM-0-7-centos test]$ ls /proc/24746 -al

 当我们找到/proc目录下这个我们运行的代码创建的进程的详细信息时,会看到cwd 和exe

exe -> /home/cyq/practice/test/test:当前正在运行的可执行程序。

cwd -> /home/cyq/practice/test:当前工作目录。

大多数进程信息同样可以使用top和ps这些用户级工具来获取

[cyq@VM-0-7-centos test]$ ps axj | grep test
[cyq@VM-0-7-centos test]$ ps axj | grep test | grep -v grep

 通过系统调用获取进程标示符

PID:当前进程id

PPID:父进程id

我们先认识两个系统调用接口:getpid()、getppid()

getpid():返回当前进程的id

getppid():返回当前进程的父进程的id

测试代码:

#include<stdio.h>
#include<sys/types.h>
#include<unistd.h>
 
int main()
{
  while(1)
  {
    printf("pid: %d ppid: %d\n", getpid(), getppid());
     sleep(1);                                                                                            
   }
   return 0;
}

测试结果:

我们通过打印结果和查看进程id可以发现,两者打印父子进程的id是一致的,说明这是正确的。 

当然,如果想显示全一些可以这样指定:

[cyq@VM-0-7-centos test]$ ps axj | head -1 && ps axj | grep test

通过系统调用创建进程-fork初识

fork

1、fork()之后该进程会创建一个子进程。

2、同时fork()之后有两个返回值。在这里返回类型pid_t可以当作我们平常见到的int类型就好了。 

我们先来看现象,再慢慢解释:

#include<stdio.h>
#include<unistd.h>

 int main()
 {
   fork();
   printf("child_pid: %d parent_pid: %d\n", getpid(),getppid());
   sleep(1);                                                                                              
   return 0;
 }

运行结果:

 我们可以看到20354进程的父进程是20353.那么20353的父进程19607又是什么呢?

[cyq@VM-0-7-centos test]$ ps axj | head -1 && ps axj | grep 19607

       我们发现进程19607进程是bash指令,也就是命令行处理器,通常运行于文本窗口中,并能允许用户直接输入命令。

如何理解fork创建子进程

1、./test、运行指令、fork:在操作系统角度,这些创建进程的方式是没有差别的。

我们再来思考:fork本质是创建进程,这样系统里就多了一个进程,同时与进程相关的内核数据结构+进程的代码和数据在系统里多了一份!那么,我们只是fork了,创建了子进程,但是子进程的代码和数据呢???

我们假设一个现实生活场景:比如小明的父亲是做手机的企业家,如果有一天,小明的父亲想锻炼小明,提前适应社会,于是打算把自己的产业给儿子继承做一部分,这时候小明就和父亲就同时经营父亲的工厂,也就是父子两人共享工厂资源。但是父亲肯定有私房钱的,父亲肯定不会让小明占有的,也就是说这份私房钱是父亲私有的。

站在OS角度:父进程创建子进程,默认情况下,子进程会"继承"父进程的代码和数据。内核数据结构task_struct也会以父进程为模板,初始化子进程的task_struct。

总结:既然子进程和父进程是父子关系,那么就好比小明和它父亲有很多基因是相似的,也就是对应内核数据结构task_struct也会以父进程为模板,初始化子进程的task_struct;子进程共享父进程的代码和数据,就好比小明和他父亲共同经营父亲的工厂;至于私房钱,默认情况下就好比每个进程的数据,虽然是共享的,但是要考虑修改情况,实际上,数据是每个进程私有的(不要觉得冲突,这和Linux这里写时拷贝有关)。

进一步理解

fork()之后,子进程和父进程的代码是共享的!

代码存在代码区,也就是常量区,所以说,代码是不可以被修改的!

数据呢??默认情况下,数据也是共享的,不过要考虑修改的情况!

       刚开始父子进程指向同一块数据空间,因为子进程和父进程可能使用同样的数据,如果再开一段空间去拷贝数据这样对空间和时间都有一定的资源浪费。所以当进程数据被修改后再来开空间加初始化,这样在一定程度上提高了效率~ 

注意:进程具有独立性,它们是通过写时拷贝来完成进程间数据的独立性。

fork的返回值

我们创建子进程是为了让父子进程干不同的事情,否则是没有意义的。我们怎么保证让不同进程干不同的事情呢?? 通过返回值判断来完成。

pid_t fd = fork();

如果fd < 0:则创建进程失败。

如果fd > 0:创建进程成功,同时给父进程返回子进程的pid(因为父进程:子进程 = 1 :n的关系,父进程不能直接在多个子进程中找到特定的子进程,所以子进程要返回给父进程它的pid,这样父进程能找到子进程,进而达到控制子进程的目的)。得到的是父进程。

如果fd == 0:创建进程成功,得到的是子进程。(子进程只有一个父进程,所以父进程不需要把其pid给子进程,子进程找父进程是没有二义性的,可以直接找到父进程)

我们用代码举个栗子:

 #include<stdio.h>
 #include<sys/types.h>                                                                                    
 #include<unistd.h>
 
 int main()
 {
   pid_t fd = fork();
  if(fd == 0)
  {
    //child 子进程
   }
   else if(fd > 0)
  {
     //parent 父进程
    printf("i am parent process pid: %d\n",getpid());
   }
   else{
     //失败
    return -1;
   }
   sleep(1);
   return 0;
}

运行结果:

我们发现在if else 语句中竟然两个判断语句的内容都被执行了,说明这时候有两个进程再跑~通过打印输出也可以证明fd返回值的区分父子进程的正确性。现实中,我们把打印内容换成任务,这时候就保证了不同进程执行不同的代码了。

进程状态

下面的状态在kernel源代码里定义

/*
* 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资源!!

S睡眠状态(sleeping)

意味着进程在等待事件完成(这里的睡眠有时候也叫做可中断睡眠(interruptible sleep))。

D休眠状态(Disk sleep)

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

应用场景:比如一个进程要往磁盘里写入数据,这需要非常多的时间,在此过程中进程一直处于等待状态,如果长时间等待OS就会想:"我在这里这么忙,你这个进程竟然在这里发呆?",此时OS就把这个进程杀掉了。等了一会磁盘把数据写入完毕之后,要给进程发信号是写入成功了还是失败了。这时候,磁盘找不到等待自己的进程了,因为它被OS干掉了!假如说磁盘写入数据失败了,但是没有反馈给用户,用户就误认为写入成功了,那么当用户有一天发现没有找到自己曾经写的文章时,请问这个锅谁来背??OS?进程?磁盘?这时候,进程就需要一个不能被终端休眠的状态,即不可中断睡眠,或者深度睡眠状态。

注意:进程处于D状态,不可被杀掉!!

T停止状态(stopped)

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

暂停态和休眠态区别:T状态是彻底暂停了,进程不会被推进或修改,休眠态在休眠的过程中有些数据是可以被修改的,比如已休眠时间等等。

X死亡状态(dead)

这个状态只是一个返回状态,你不会在任务列表里看到这个状态。

相当于这个进程已经终止掉了。-> 回收进程资源 = 进程相关的内核数据结构 + 进程的代码和数据

Z僵尸状态(zombie)

僵死状态(Zombies)是一个比较特殊的状态。
当父进程没有读取到子进程退出的返回代码时就会产生僵死(尸)进程
僵死进程会以终止状态保持在进程表中,并且会一直在等待父进程读取退出状态代码。
所以,只要子进程退出,父进程还在运行,但父进程没有读取子进程状态,子进程进入Z状态

为什么要有僵尸进程?

是为了辨别退出死亡原因!!然后进程退出信息(也就是数据)记录到task_struct中,然后反馈给父进程。也就是一个进程死亡前是僵尸状态,然后获取一定信息后变成了死亡状态。

现实场景:就好比警察办案,对受害人并不是第一时间处理(这时候这个资源还被占着),而是收集附近足够的证据后,才开始清理现场(清理资源)。

进程状态代码的模拟

R运行状态

代码:

int main()
{
  while(1);
  return 0;
}

运行后并查看进程状态:

我们让进程不断死循环,也就是CPU一直在运行,这时候我们可以看到一个R+状态的进程。 

+:表示前台运行

S睡眠状态

代码:

int main()
{
  while(1)
  {
    printf("hello wmm\n");
    sleep(1);
  }
  return 0;
}

运行后的状态:

我们把上面的代码改造一下为什么进程就变成了休眠状态了呢?

因为在循环中printf,这时候就和外设打交道了,CPU的速度很快,外设的速度是很慢的,大部分时间里CPU都是在等待IO(也就是内存,主要指文件IO、网络IO),因为IO等待外设就绪是需要花时间的。 

T停止状态

我们先先来看一组信号(以后博主会在以后的客详细讲解~)

[cyq@VM-0-7-centos test]$ kill -l

SIGSTOP:暂停信号

SIGCONT: 继续信号

我们可以使用编号或者使用宏来发送信号。

 我们用代码模拟一下停止状态:

[cyq@VM-0-7-centos test]$ kill -19 24972

当我们给正在运行的进程发送19号(暂停信号,发现进程就变成了T状态) 

我们使用18号信号就可以让T状态的进程继续执行:

[cyq@VM-0-7-centos test]$ kill -18 2497

这是胡我们再使用18号信号进程状态恢复过来了,但是发现后面的+号不见了,这表示进程当前处于后台运行状态,我们在前台输入ctrl+c是终止不掉它的!这时候我们就可以用9号命令来干掉进程。 

[cyq@VM-0-7-centos test]$ kill -9 24972

                  

 对于后台进程,我们就可以使用kill -9 +进程id可以干掉,当我们创建一个后台进程可以这么创建。

[cyq@VM-0-7-centos test]$ ./test &

注意:在后台运行的进程我们输入的指令还是可以执行的(只不过不能ctrl掉,只能kill -9掉)。比如touch file当我们再打开一个终端可以发现一个file文件就被创建好了。

Z僵尸状态

代码:

int main()
{
  pid_t id = fork();
  if(id == 0)
  {
    //child
      printf("i am child... pid: %d\n", getpid());
      sleep(5);
  }
  else if(id > 0)
  {
    //parent
    printf("i am parent... pid: %d\n",getpid());
    sleep(50);
  }
  else
  {
    exit(-1);
  }
  return 0;
}

这段代码意思是fork之后有两个进程,子进程休眠5s后就退出,父进程休眠50s后,子进程在退出时,父进程还在休眠,没有及时读取子进程的退出状态(也就没办法检测回收子进程),这时候子进程就是僵尸状态。

僵尸进程的危害:

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

2、维护退出状态本身就是要用数据维护,也属于进程基本信息,所以保存task_struct(PCB)中,换句话说,Z状态一直不退出,PCB一直都要维护?是的!
3、那一个父进程创建了很多子进程,就是不回收,是不是就会造成内存资源的浪费?是的!因为数据结构对象本身就要占用内存,想想C中定义一个结构体变量(对象),是要在内存的某个位置进行开辟空间!
4、内存泄漏?是的 

僵尸进程的解决方案在以后的博客会有专门介绍~

进程状态讲完之后,还有一个特殊的进程:

孤儿进程

父进程先退出,子进程就称之为“孤儿进程”
孤儿进程被1号init进程领养,当然要有init进程回收喽

代码模拟:

int main()
{
  pid_t id = fork();
  if(id == 0)
  {
    //child
      printf("i am child... pid: %d\n", getpid());
      sleep(30);
  }
  else if(id > 0)
  {
    //parent
    printf("i am parent... pid: %d\n",getpid());
    sleep(10);
    return 0;
  }
  else
  {
    exit(-1);
  }
  return 0;
}

我们让子进程休眠30s,期间父进程10s后就退出了,此时子进程的父进程是谁呢?

我们用一个脚本语言查看一下:

[cyq@VM-0-7-centos test]$ while :; do ps axj | head -1 && ps axj | grep 31448; sleep 1; echo "####################"; done

这时候我们发现子进程1551的退出后它的父进程就变成了1号进程,就是被1号(init进程)领养了。 

注意:1号进程就是操作系统。

 看到这里,给博主支持一下吧~

猜你喜欢

转载自blog.csdn.net/qq_58724706/article/details/125031764