初识进程1.0

Posted WoLannnnn

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了初识进程1.0相关的知识,希望对你有一定的参考价值。

进程

基本概念

  • 课本概念:程序的一个执行实例,正在执行的程序等
  • 内核观点:担当分配系统资源(CPU时间,内存)的实体。

自己的理解:

  1. 程序文件加载进内存后,就变成了进程
  2. 可执行程序与管理进程所需要的数据结构的集合

描述进程-PCB

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

task_struct——PCB的一种

  • 在Linux中描述进程的结构体叫做task_struct。
  • task_struct是Linux内核的一种数据结构,它会被装载到RAM(内存)里并且包含着进程的信息。

task_ struct内容分类

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

组织进程

可以在内核源代码里找到它。所有运行在系统里的进程都以task_struct链表的形式存在内核里。

查看进程

  1. 可以通过ps aux命令查看进程,如果我们想查看某个进程,就可以使用管道+grep,比如查看test,输入命令 ps aux | grep test即可

    写出下面这个程序

    #include <stdio.h>
    #include <unistd.h>
    int main()
    
     //无限打印hello
     while(1)
     
         printf("hello\\n");
         sleep(1);
     
    
    	return 0;
    
    

    查看该进程信息:

    我们先运行程序,再通过复制会话执行ps aux | grep test ,就可以看到下列结果:

    ps aux是获取所有进程信息

    ps aux有多个选项,详情参见文章 ps aux 指令详解

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

    同样,我们可以通过上述进程的编号,查看进程

    先获取进程编号15228

    输入ls /proc

    进入进程15228:

    我们看到里面有一个进程的相关信息,简单解释一下圈出来的cwd,当我们创建文件没有指定路径时,就会默认在当前目录下创建,那操作系统怎么知道我们的当前目录呢,就是通过cwd来了解的

    上面/proc文件中的编号为1的进程可以理解为是操作系统进程,但实际上操作系统不是进程

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

  • 进程id(PID)
  • 父进程id(PPID)

获取当前进程编号及父进程编号

  • getpid() #获取当前进程编号
  • getppid() #获取父进程编号
#include<stdio.h>
#include <unistd.h>

int main()

  while (1)
  
    printf("pid: %d ", getpid());
    printf("ppid: %d\\n", getppid());
    sleep(1);
  
  return 0;


我们重复运行test程序,得到的进程编号都是不同的:

但是,其父进程编号都是相同的,也就是说,父进程一直没有结束

那么其父进程究竟是什么呢?

我们通过grep其进程编号6966来查看:

父进程-bash

bash叫做命令行解释器,通过创建子进程,让子进程去完成对应的任务。

bash也是一个进程,bash是不能挂的

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

fork()可以创建一个子进程

  • fork有两个返回值

    父进程的fork 返回的是子进程的pid,子进程返回的是0

    如果fork失败了,返回值小于0

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

  • 父子进程从fork执行相同的代码(只读)

#include <stdio.h>
#include <unistd.h>
int main( )

//在fork之前是一个进程
int ret = fork();
//fork之后是会进行分支执行的
printf(“hello proc : %d!, ret: %d\\n”, getpid(), ret);
sleep(1);

return 0;


![image-20211002005924943](https://img-blog.csdnimg.cn/img_convert/7658a9728654a96acc2e6cdd3b9042e9.png)





* fork 之后通常要用 if 进行分流  

```c
#include <stdio.h>
#include <unistd.h>
int main()

    int ret = fork();
    if(ret < 0)
    
        perror("fork");
        return 1;
    
    else if(ret == 0)//child
     
    	printf("I am child : %d!, ret: %d\\n", getpid(), ret);
    
    else//father
     
    	printf("I am father : %d!, ret: %d\\n", getpid(), ret);
    

    sleep(1);
    return 0;

理解进程创建

创建进程,是系统多了一个进程,多一个进程系统就要多一组管理进程的数据结构+该进程对应的代码和数据

fork父子执行顺序,及代码和数据复制的问题

父子执行顺序不一定,由调度器决定

进程数据 = 代码+数据

父进程创建子进程的时候,代码是共享的,数据是各自私有一份(写时拷贝)

代码是逻辑,一般是不可被修改的,也就是只可读
数据是既可读又可写的

通过数据私有,表现出进程具有独立性

fork为什么有两个返回值

fork函数执行的内容大概是给子进程的task_struct对象赋值等,最后返回pid

当fork执行完,子进程已创建完成,而父子进程都要使用fork,函数的返回值是数据,各自私有一份,所以返回的函数值不同

进程状态

看看Linux内核源代码怎么说

  • 为了弄明白正在运行的进程是什么意思,我们需要知道进程的不同状态。一个进程可以有几个状态(在
    Linux内核里,进程有时候也叫做任务)。
    下面的状态在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) : 并不意味着进程一定在运行中,它表明进程要么是在运行中要么在运行队列里。
    是R状态的进程,一定在CPU上面跑吗? 不一定,我们先了解:进程是分队列分配的,有运行队列,等待队列,阻塞队列(是等待队列的一种)
    R状态是指可以被调度,要么在运行,要么在运行队列里。

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

我们看一个简单的代码:

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

 int main()
 
     while (1)
    	
     	printf("hello world\\n");
     	sleep(1);
   	

     return 0;
 

当代码在死循环地打印时,我们查看test进程的状态:

先不管S后面的 +是什么,我们看到S代表休眠,也就是该进程在等待,为什么呢,因为CPU比输出设备快多了,输出设备还在输出,CPU就没事做了,所以该进程一直在等待输出设备,而我们看到这么多次查看都是在S,说明等待时间占大部分时间。

再来讲讲 + 是什么意思,+表示当前进程在前台运行,可以用ctrl+c停下。如果我们想让其在后台运行,可以在 ./test后加上 &,此时就不能用ctrl + c停下了,而要用kill -9 [pid]

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

    关于D状态的解释

    当一个进程要从硬盘里面读取数据时(硬盘读取数据很慢),进程要等待硬盘(等待IO),硬盘需要查找数据,拷贝数据,都需要花费时间,而可能碰巧在进程等待的时候,计算机的内存已经不足了,此时操作系统为了管理内存,就会杀掉一些进程,而操作系统发现这个进程还在等待,操作系统就说:“内存都快没了,你还在这等什么。”于是,该进程就被操作系统给杀掉了,可硬盘还在查找数据,当它找到数据的时候,却尴尬地不知道把数据给谁。于是D状态就是保护这类的进程不被杀掉。

注意:D状态保证进程无法被杀掉,S状态可被杀掉

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

我们可以写一个死循环程序,再使用相关命令使进程进入T状态:

  #include <stdio.h>
  #include <unistd.h>
  int main()
  
    while (1)
    
      printf("hello world\\n");
      sleep(1);
    
    return 0;
  

输入kill -l查找使进程停下的选项

使用效果:

kill -18 24230 #使进程继续运行

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

进程状态查看

ps aux / ps axj 命令

ps axj可以查看父进程编号

Z(zombie)-僵尸进程

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

来一个创建僵死进程例子:

子进程打印五次结束运行,父进程一直运行:

#include <stdio.h>
#include<unistd.h>
#include <stdlib.h>
int main()

    
	pid_t ret = fork();
	if(ret < 0)
    
		perror("fork");
		return 1;
  	
	else if(ret > 0)//parent
     
        while (1)
        
            printf("I am parent! pid is: %d\\n", getpid());
			sleep(1);
        
		
	
    else
 	
        int count = 0;
        while (count < 5)
        
			printf("I am child! pid is: %d\\n", getpid());
			sleep(2);
            ++count;	
        
        
		exit(0);
	
    
	return 0;


利用监控脚本来查看父子进程的信息:

while :; do ps axj | head -1 && ps axj | grep test | grep -v grep; sleep 1 ; echo “##############”; done

效果:

绿圆圈表示父进程状态
红方框表示子进程状态

当执行5次后,子进程就进入Z+(僵尸状态)了,而父进程还在运行

运行效果:

为什么有僵尸状态? 有什么特征

子进程结束,父进程需要检测子进程运行结束的结果:是否正常运行完?是否异常?发生了什么异常?

特征:一般处于僵尸状态时,task_struct是会被保留的,进程的退出信息,是放在进程控制块中的

僵尸进程危害

  • 进程的退出状态必须被维持下去,因为他要告诉关心它的进程(父进程),你交给我的任务,我办的怎
    么样了。可父进程如果一直不读取,那子进程就一直处于Z状态?是的!
  • 维护退出状态本身就是要用数据维护,也属于进程基本信息,所以保存在task_struct(PCB)中,换句话
    说, Z状态一直不退出, PCB一直都要维护?是的!
  • 那一个父进程创建了很多子进程,就是不回收,是不是就会造成内存资源的浪费?是的!因为数据结构
    对象本身就要占用内存,想想C中定义一个结构体变量(对象),是要在内存的某个位置进行开辟空间!
  • 造成内存泄漏

孤儿进程

  • 父进程如果提前退出,那么子进程后退出,进入Z之后,那该如何处理呢?
  • 父进程先退出,子进程就称之为“孤儿进程”
  • 孤儿进程被1号进程领养,当然要由1号进程回收
#include <stdio.h>
#include<unistd.h>
#include <stdlib.h>
int main()

    
	pid_t ret = fork();
	if(ret < 0)
    
		perror("fork");
		return 1;
  	
	else if(ret > 0)//parent
     
        int count = 0;
        while (count < 5)
        
            printf("I am parent! pid is: %d\\n", getpid());
			sleep(1);
            ++count;	
        
		
	
    else
 	
        while (1)
        
            printf("I am child! pid is: %d\\n", getpid());
			sleep(1);
        
        
		exit(0);
	
    
	return 0;


同样也可以用脚本观察父子进程的状态

父进程运行五次就结束了,子进程还在继续,且变成了后台运行

运行效果:

如果进程的父进程进入僵尸,该进程的资源(内存)就没有办法回收,造成内存泄漏

OS考虑了这个问题,孤儿进程是要被领养的,具体被1号进程领养

Linux中所有进程都有父进程,除了1号进程

以上是关于初识进程1.0的主要内容,如果未能解决你的问题,请参考以下文章

初识OpenGL 片段着色器(Fragment Shader)

初识OpenGL 片段着色器(Fragment Shader)

初识Spring源码 -- doResolveDependency | findAutowireCandidates | @Order@Priority调用排序 | @Autowired注入(代码片段

初识Spring源码 -- doResolveDependency | findAutowireCandidates | @Order@Priority调用排序 | @Autowired注入(代码片段

Java初识方法

初识线程