Linux入门——05 进程

1.预备知识

1.1冯诺伊曼体系结构

  • 存储器:内存(掉电已失)(较快)
  • 运算器+控制器+其他 = 中央处理器(CPU(寄存器:CPU内部的高速存储器,速度快,数量少),芯片):含有运算器和控制器等(快)
  • 外设:磁盘(外存)永久性储存能力 输入/输出设备(较慢)
  • 输入设备:包括键盘, 鼠标,扫描仪, 写板,摄像头等
  • 输出设备:显示器,打印机等
  • 输入输出设备:磁盘&&网卡

1.2计算机的内存与CPU

CPU其实很笨,只能接受别人的指令,别人的数据-------》执行别人的指令,计算别人数据的目的

先要认识别人的指令(有自己的指令集)

我们写代码,经过编译成二进制可执行程序,才能被CPU计算

CPU读数据从内存中拿(在数据层面只和内存打交道,为了提高整机的效率)

内存数据永远在磁盘上,在CPU要之前,已经搬到了内存中(开机时加载,内存相当于缓存)

加载啥数据由操作系统决定

计算机就算硬件与软件的完美结合

1.3IO过程(INPUT&&OUTPUT)

IO过程就是由内存到外设的过程

1.4结论

这里的CPU能且只能对内存进行读写,不能访问外设(输入或输出设备)

内存写出,也一定是写到外设中去

能解决的问题:程序为啥要运行必须加载到内存,因为CPU要执行我的代码,只能从内存中读取(体系结构规定!)

外设和CPU在控制层面上,有交互,这里是中断

CPU可以进行数术运算和逻辑运算

1.4操作系统

操作系统的作用:向下管理硬件,向上提供接口

Linux子系统

1.进程管理:管理进程的创建、调度、销毁等

2.内存管理:管理内存的申请、释放、映射等

3.文件系统:管理和访问磁盘中的文件

4.设备管理:硬件设备及驱动的管理

5.网络协议:通过网络协议栈(TCP、IP...)进行通信

2.进程

进程:一个运行起来的(加载到内存)的程序

在内存中的程序叫进程

进程和程序相比,进程具有动态属性

如何管理这些进程呢?先描述再组织------------》PCB

task_struct | PCB

在Linux中描述进程的结构体叫做task_struct。

课本上称之为PCB(process control block),Linux操作系统下的PCB是: task_struct

进程的属性

        改进程对应的代码和属性地址

        struct task_struct *p1 = malloc(struct task_struct);

        p1->...= xxx

        p1->addr = 代码和数据的地址

所谓对进程的管理就是对PCB结构作管理(对链表的增删查)

struct task_struct 内核结构体 ------》 创建内核对象 -------》 该结构将你的代码和数据关联起来 --------》完成了:先描述再组织的工作

进程 = 内核数据结构+进程对应的磁盘代码

查看进程(1)

ps ajx | grep "myproc" //grep行过滤

可以grep -v grep//把grep进程过滤掉

ps ajx | head -1 //显示标题

ps ajx | head -1 && ps ajx | grep "myproc"

查看进程(2)

ls /proc //进程可以被当作文件来查看

查看进程详细信息

进入该进程所在目录然后ll

会出现exe,为该可执行程序在磁盘上的位置

当把进程可执行程序删除,进程还在运行,但exe在闪,提示你该文件已经被删除

说明该程序被加载到内存,已经和磁盘没有关系了,也有特殊情况。

杀掉进程kill -9 PID号

2.1如何获得进程的pid

系统调用 man 2 gitpid

gitpid

pit_t是整数类型的

父进程常见调用

man getppid //获取父进程ID

父进程为bash(这就是shell对用户的指令进行创建子进程)

你的程序是以子进程执行的,不怕你有BUG(王婆派实习生去说媒)

当把bash执行kill -9 32114时,系统就会崩溃

每当操作系统开机的时候,就会指派一个SHELL(bash)

--------->命令行上执行的命令,没有特殊情况,父进程都是BASH

2.2创建子进程

子进程创建fork

fork() //创建子进程,是一个函数-----函数执行前:只有一个父进程------函数执行后: 父进程+子进程

函数执行时出现两个子进程

第一个是第二个的父进程

第一个的父进程是bash

函数执行时出现

fork的返回值,给父进程返回自身(子进程)id,给子进程返回0

同一个id,有两个返回值?????????为什么???????(写实拷贝)

fork之后,会有父进程+子进程在执行后续代码

fork后续代码是父子进程共享

通过返回值不同,让父子进程执行后续共享的代码

2.3进程的状态

不同的状态就是task_struct在不同的队列中,等待不同资源

2.3.1系统层面

宏观概念

1.os

struct dev_keyboard

{

        task_struct *wait_queue;

        属性

}

struct dev_display

{

        task_struct *wait_queue;

        属性

}

struct 网卡

{

        task_struct *wait_queue;

        属性

}

struct 磁盘

{

        task_struct *wait_queue;

        属性

}

。。。。

struct task_struct

{

        进程属性 -------》.exe文件

}

进程的不同状态为了应对不同的运行场景。

struct runqueue《--------CPU找到要执行的进程(1个CPU一个运行队列)

{

        task_struct *head;

        //其他属性

}

所谓的进程运行,就是让进程task_struct进入队列(先进先出)

在运行队列里的进程PCB就称为运行状态,不是进程再次运行,才是运行状态。

状态是进程内部的属性-------》task_struct-------->int(1:run 2:stop 3:hup 4:dead.....)

2.驱动

3.硬件:键盘 显示器 网卡 显卡 磁盘

CPU很快,硬件很慢,但进程或多或少都要访问他。但这些硬件数量也很少。当这写硬件被占用的时候,这些进程也只能等待。

不要只意味,你的进程只会等待(占用)CPU资源,你的进程随时访问外设资源。

每一种外设也有自己的task_struct *wait_queue属性,当外设被占用,CPU的运行队列,会将该进程放到外设(磁盘)队列中,当外设开始执行该队列的时候,再通知CPU来执行。 把该进程再放到运行队列中去。

  • 就绪(新建)状态:就是该进程被创建好。
  • 运行状态(等待CPU资源): 并不意味着进程一定在运行中,它表明进程要么是在运行中要么在运行队列里。
  • 阻塞状态(等待外设资源):CPU在执行进程时,需要访问外设,而外设在被占用,该进程所处的状态为阻塞状态。
  • 挂起状态:当多个阻塞的进程时,CPU为了腾空间,将部分进程的代码和数据重新写入磁盘(唤出),当运行空间充足。进程再次被执行,重新将数据和代码加载到内存(唤入)。

阻塞不一定挂起,但挂起一定阻塞。

2.3.2Linux的底层实现

/*
* 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): 并不意味着进程一定在运行中,它表明进程要么是在运行中要么在运行队列里。
  • S睡眠状态(浅度睡眠)(sleeping阻塞状态): 意味着进程在等待事件完成(这里的睡眠有时候也叫做可中断睡眠(interruptible sleep))。(printf ---》显示器(外设)-----》慢-----》 等显示器就绪--------》 CPU花很长时间。%99都在等待%1在执行代码)
  • D(深度睡状态)磁盘休眠状态(Disk sleep)有时候也叫不可中断睡眠状态(uninterruptible sleep),在这个状态的进程通常会等待IO的结束,无法被操作系统杀掉,只能通过断电,或进程自己醒来,结束。
    • 当进程A在将1000条用户信息,写到磁盘上时,磁盘开始慢慢悠悠的开始写,进程就在等待,这时候操作系统内存吃紧,已经不能靠挂起进程来解决了,这时候就会出现杀掉进程来解决,就看见进程A在闲着,于是把它杀了,这时候磁盘就反馈写入失败了,进程说操作系统把我给杀了,甩锅给操作系统,操作系统就说我在更好的分配资源。于是人们就给进程A一个免死金牌,操作系统没法杀该进程,这就是深度睡眠状态,当进行大量文件IO操作时会出现。
  • 挂起状态:看不到,这是操作系统内部执行的状态。
  • T停止状态(stopped阻塞状态): 可以通过发送 SIGSTOP 信号给进程来停止(T)进程。这个被暂停的进程可以通过发送 SIGCONT 信号让进程继续运行。kill -19 进程编号(19代表暂停)(18继续)
    • +号代表是前台进程,没有+的叫后台进程(不能用CTRL+C终止,可以kill -9 进程编号)。

  • t追踪停止状态(tracing stop): 当该进程在被GBD调试的时候,代码中有断点就会出现该状态。
  • X死亡状态(dead):这个状态只是一个返回状态,你不会在任务列表里看到这个状态。
  • N:低优先级的进程
  • L:有内存分页分配并锁在内存中

2.4(zombie)僵尸进程

进程被创建出来是为了完成任务---------1.要知道他完成的如何?2.可以不关心结果(你可以不要,但我不能没有)

进程退出的时候,不能立即释放该进程占有的资源。保存一段时间,让父进程或OS读取。在结束到等回收的状态就成为僵尸状态。

进程退出了,但是没有被回收(父进程 os)=====>创建子进程,让父进程不要退出,且什么也不做,让子进程正常退出。

  • 僵死状态(Zombies)是一个比较特殊的状态。当进程退出并且父进程(使用wait()系统调用,没有读取到子进程退出的返回代码时就会产生僵死(尸)进程
  • 僵死进程会以终止状态保持在进程表中,并且会一直在等待父进程读取退出状态代码。
  • 所以,只要子进程退出,父进程还在运行,但父进程没有读取子进程状态,子进程进入Z状态
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int main()
{
    pid_t id = fork();
    if(id < 0){
        perror("fork"); // err
        return 1;
    }else if(id > 0){   //parent
        printf("parent[%d] is sleeping...\n", getpid());
        sleep(30); 
    }else{              //child
        printf("child[%d] is begin Z...\n", getpid());
        sleep(5);
        exit(EXIT_SUCCESS);//exit(1);
    }
return 0;
}
while:;do ps ajx | head -1 && ps ajx | grep mypross | grep -c grep; sleep 1;done

2.4.1僵尸进程危害

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

2.5不可被杀的进程 Z D X

2.6孤儿进程

  • 父进程如果提前退出,那么子进程后退出,进入Z之后,那该如何处理呢?
  • 父进程先退出,子进程就称之为“孤儿进程”
  • 孤儿进程被1号init进程领养,当然要有init进程回收。
  • 子进程被操作系统会被操作系统领养,就称为孤儿进程,状态变成S(后台进程),只能killl -9杀掉。
  • 如果子进程不被领养,就会成为僵尸进程。
nclude <unistd.h>
#include <stdlib.h>
int main()
{
        pid_t id = fork();
        if(id < 0){
            perror("fork");
            return 1;
        }
        else if(id == 0){//child
            printf("I am child, pid : %d\n", getpid());
            sleep(10);
        }else{//parent
            printf("I am parent, pid: %d\n", getpid());
            sleep(3);
            exit(0);
         }
        return 0;
}

当用kill parent进程的时候,并没有出现僵尸进程,这是因为创建parent进程的父进程是bash,他会回收资源。

当父进程死掉,child进程的parent直接变成1。

1号进程对应的就是操作系统

2.7进程的优先级

cpu资源分配的先后顺序,就是指进程的优先权(priority)。

优先权高的进程有优先执行权利。配置进程优先权对多任务环境的linux很有用,可以改善系统性能。还可以把进程运行到指定的CPU上,这样一来,把不重要的进程安排到某个CPU,可以大大改善系统整体性能。

什么是优先级?

能做,先做还是后做的问题。

为什么有优先级?

资源太少了

Linux优先级特点========很快

优先级的本质

PCB里面的整数(一个或几个)

在linux或者unix系统中,用ps –la命令则会类似输出以下几个内容:

我们很容易注意到其中的几个重要信息,有下:

  • UID : 代表执行者的身份
  • PID : 代表这个进程的代号
  • PPID :代表这个进程是由哪个进程发展衍生而来的,亦即父进程的代号
  • PRI :代表这个进程可被执行的优先级,其值越小越早被执行
  • NI :代表这个进程的nice值(越NICE,优先级越低)

2.8PRI vs NI

PRI也还是比较好理解的,即进程的优先级,或者通俗点说就是程序被CPU执行的先后顺序,此值越小进程的优先级别越高

那NI呢?就是我们所要说的nice值了,其表示进程可被执行的优先级的修正数值

PRI值越小越快被执行,那么加入nice值后,将会使得PRI变为:PRI(new)=PRI(old)+nice (LINUX下PRI(old) = 80)

这样,当nice值为负值的时候,那么该程序将会优先级值将变小,即其优先级会变高,则其越快被执行

所以,调整进程优先级,在Linux下,就是调整进程nice值

nice其取值范围是-20至19,一共40个级别。

普通用户调整 NI 值的范围是 0~19,而且只能调整自己的进程。

普通用户只能调高 NI 值,而不能降低。如原本 NI 值为 0,则只能调整为大于 0。

只有 root 用户才能设定进程 NI 值为负值,而且可以调整任何用户的进程。

需要强调一点的是,进程的nice值不是进程的优先级,他们不是一个概念,但是进程nice值会影响到进

程的优先级变化。

可以理解nice值是进程优先级的修正数据

2.8.1查看进程优先级的命令

top

进入top后按“r”–>输入进程PID–>输入nice值

 2.9其他概念

  • 竞争性: 系统进程数目众多,而CPU资源只有少量,甚至1个,所以进程之间是具有竞争属性的。为了高效完成任务,更合理竞争相关资源,便具有了优先级
  • 独立性: 多进程运行,需要独享各种资源,多进程运行期间互不干扰(父子进程也是相互独立的)
  • 并行: 多个进程在多个CPU下分别,同时进行运行,这称之为并行
  • 并发: 多个进程在一个CPU下采用进程切换的方式,在一段时间之内,让多个进程都得以推进,称之为并发

2.10.进程之间的切换

CPU中有寄存器(一套寄存器),其中有PC寄存器(程序计数器,用于存储当前取址指令的地址),LR寄存器(链接寄存器),SP寄存器(栈寄存器),CPSR寄存器(状态寄存器)。其中又有指令流水线,3级流水线(取址,译码,执行)。

当我们进程在运行的时候一定会产生许多临时数据。这份数据属于当前进程,CPU内部虽然存在一套寄存器硬件,寄存器里面保存的数据是属于当前进程的。寄存器(硬件)!= 寄存器内部数据

进程在运行的时候占用CPU,进程不是一直要占有到进程结束,如while(1)

进程在运行的时候,都有自己的时间片!所以都有跑一段时间就离开的情况。当兵(学习保留学籍)上下文保护,当兵回来,恢复学籍(上下文恢复)。这就相当于进程切换。

进程在切换的时候,临时数据保存起来(上下文保护),放到PCB(暂时这样理解)中,再放另一个进程。当该进程会来的时候数据重新回来(上下文恢复

在任何时候,CPU里面的的寄存器里面的数据,看起来是在大家都能看到的寄存器上,但是,寄存器的数据,只属于当前运行的进程。

寄存器被所有进程共享,寄存器内的数据,是每个进程各自私有的-------上下文数据

2.11进程地址空间

地址空间:是OS内实现进程管理,所设计的一种虚拟化解决方案,通过地址空间,可以让每个进程都独占系统资源。

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

#define USER "USER"
int global_val=100;

int main()
{
    pid_t id = fork();
    if(id < 0)
    {
        printf("fork error\n");    
        return 1;
    }
    else if(id== 0)
    {
        int cnt = 0;
        while(1)
        {
            printf("child,pid:%d,ppid:%d  | global_val:%d,global_val:%p\n",getpid(),getppid(),global_val,&global_val);
            sleep(1);
            cnt ++;
            if(cnt == 10)
            {
                global_val = 300;
                printf("gaila.........\n");            
            }
        }

    }
    else
    {
        while(1)
        {
            printf("father,pid:%d,ppid:%d  | global_val:%d,global_val:%p\n",getpid(),getppid(),global_val,&global_val);
            sleep(2);
        }    
    }
    return 0;
}

global_val被改后,父子进程的值发生了改变,因为父子进程是独立的,但两个global_val的地址是一样的,说明这里的地址不是物理地址-------》曾经我们学习的语言指针对应的地址,不是物理地址。是虚拟地址(线性地址)【逻辑地址】,C/C++打印的地址都是虚拟地址。

上述结果产生的原因是:

在创建子进程的时候,会将父进程的所有东西进行以下拷贝。但当要修改其中的数据的时候。OS保证进程之间的独立性-----》就对改数据,重新在物理地址上进行拷贝。要修改也是只改重新拷贝好的数据,对与虚拟地址空间没有改变。------------》这样的技术就叫写时拷贝

OS,为了保证进程的独立性,做了很多工作!!!!-----》通过地址空间,通过页表,让不同的进程,映射到不同的物理内存处

进程的独立性:1.独立的内核数据结构。2.不同进程的数据进行分离

  1. 地址空间的存在可以更方便地进行进程和进程的数据代码的解耦,保证了进程之间的独立性
  2. 让进程以统一的视角,来看待进程对应的代码和数据等各个区域,方便编译器也以统一的视角进程编译代码。(方便使用,编译完,就能使用)

其实每个进程都会认为自己独占系统所有空间,(事实不是)-------》(就如你在睡觉,被别人搬开,睡醒之前再诺回来,你会以为你以一直在床上)

地址空间的本质:是内核的一种数据结构:mm_struct

1.地址空间描述的基本空间大小是字节

2.32位-----》2^32次方个地址

3.2^32字节4*2^30=4GB空间范围

4.每个字节都要有唯一的地址------》要表示2^32个地址-----》要保证地址的唯一性-------》用32位的数据就可以了0000 0000 0000 0000 0000 0000 0000 0000

        所以地址从下到上,是从低地址到高地址排列0000 0000 0000 0000 0000 0000 0000 0000----》ffff ffff,这叫编制。

5.堆区,栈区怎么解释呢?(尺子+桌子,给桌子画了个线,这就是区域划分)

struct Destop

{

        //给男生画区域

        unsigned int nan_start;

        unsigned int nan_end;

        //给女生画区域

        unsigned int nv_start;

        unsigned int nv_end;

}

struct Destop d = {1,50,51,100}; //每一个数值就可以代表桌面的位置

对区域进行调整

struct Destop d = {1,45,55,100};

对女孩的区域扩大

struct Destop d = {1,30,31,100};

同理,计算机的区域划分,就是对2^32个地址进行划分(unsigned int) ,地址调整也是对start和end进行修改

struct mm_struct

{

        unsigned int code_start,code_end;

        unsigned int data_start, data_end;

        unsigned int heap_start, heap_end;

        unsigned int stack_start, stack_end;

        ....

}

首先申请*mm = malloc(struct mm_struct);

区域的地址,称为虚拟地址,之间的每一个数就代表一个地址(2^32)

区域调整,就是在修改end和start ------>在定义局部变量,malloc new堆空间------》扩大栈区或堆区

函数到用完毕,或free地址空间的时候------》缩小栈区或堆区

内存在和磁盘在进行输入输出的时候叫IO,操作系统在运行IO的时候,基本单位是4KB,1KB=1024bit

磁盘的可执行文件被加载的内存中去(物理内存)跟虚拟地址空间的关系是:

可以把内存看作一个大数组 struct page_mem[4GB/4KB]------>对应4GB/4KB个页---》要访问一个页,只要知道页的起始地址+页偏移就可以了

假设虚拟地址中的一个地址(0x1234 5678)被保存在了物理地址(0x1111 2222),在页表中,左侧保存虚拟地址,右侧保存物理地址,这样两者就存在了影射关系。

C中&c ----->就是虚拟地址,再查页表,找到对应的物理地址,访问空间和数据,对数据进行修改,就完成了写入。

这里的页表其实是多级页表,是个树状结构

这里的虚拟地址空间也叫线性地址(按线性排列的)

为什么存在地址空间?

  1. 如果让进程直接访问物理内存,万一进程越界非法操作,非常不安全。

(压岁钱,放到父母那,我给你管着,要的时候,给你,当你要买不好的东西时候,就不给你,这样防止你被骗,变相的保护你)

页表不仅仅只做影射,还会变相的保护你,当你访问某个空间,把需求发给页表,页表会对不合理请求进行拦截。(OS在执行,所有进程都要遵循)

重新理解地址空间

在我们的可执行程序里面有没有地址呢?(在没有加载到内存的时候?)(汇编的时候代码就已经有地址了(逻辑地址),函数是有地址的)

虚拟地址空间,不仅仅是OS在遵守。编译器也在遵守。编译器在编码的时候,就是在按照虚拟地址空间的方式,就对我们的代码和数据进行编码。这些编译后的程序中的代码是有地址的,能够实现函数的调用和跳转,是虚拟地址。

代码要在物理内存中保存,只要加载到物理内存中,此时代码也就拥有了物理地址。

当程序被加载到了物理内存的时候,该程序对应的指令和数据,就天然的具有了物理地址。将代码对应的(虚拟地址)内部地址,直接放到虚拟地址中去。

当我们CPU的pc指针访问的是代码的虚拟地址。通过页表映射,可以找到代码的物理地址。main函数的地址被我们调到CPU中,再去访问代码区其他函数的时候,还是调用虚拟地址。CPU就没见过物理地址

这就是我们在编译程序的时候选择32/64位编!!!!!!!!!!!

3.进程的控制

3.1进程的创建

在linux中fork函数时非常重要的函数,它从已存在进程中创建一个新进程。新进程为子进程,而原进程为父进程。

#include <unistd.h>

pid_t fork(void);

pit_t是整数类型的

返回值:自进程中返回0,父进程返回子进程id,出错返回-1

fork调用失败的原因可能是:

        1.系统中太多个进程

        2.实际用户的进程超过了限制

进程调用fork,当控制转移到内核中的fork代码后,内核做:

  • 分配新的内存块和内核数据结构给子进程
  • 将父进程部分数据结构内容拷贝至子进程(代码共享)
  • 添加子进程到系统进程列表当中
  • fork返回,开始调度器调度

fork常规用法

1.一个父进程希望复制自己,使父子进程同时执行不同的代码段。

2.一个进程要执行不同的程序,根据fork的返回值不同。

Linux中所有的进程都会被放到一个哈希表中

        进程控制块 struct task_struct *hash_table[1000000] //结构体指针数组

                pid作为健值去访问这个数组。他的返回值就是进程控制块的起始地址。

3.1.1fork的两个返回值

fork是OS的函数,在fork函数中,会进行子进程的创建,在return前,而子进程的创建早在函数中创建好了,这个子进程可能在OS的运行队列中,准备被调度了。这是就会出现两个执行流,fork之后代码共享,就产生了两个return。

  • 父进程返回子进程pid,给子进程返回0,孩子找父亲的是有唯一性的。这就给父进程返回子进程的pid的原因。

3.1.2写实拷贝

通常,父子代码共享,父子再不写入时,数据也是共享的,当任意一方试图写入,便以写实拷贝的方式各自一份副本。

3.2.进程终止

3.2.1进程退出码

1.why main里面会有return 0;

这个return 0,是进程退出的时候,对应的退出码。

这个退出码可以标定进程执行的结果是否正确。

//写代码就是为了完成某件事情,我是如何得知我的任务跑的如何呢?

进程退出码--------》echo $?

./mytest ------->运行代码 echo $? ------->永远记录最近一个进程在命令行中运行完毕时对应的退出码(main ---->return) 再执行echo $?----->返回的是0,因为上一个echo $? 也是一个进程。

2.以后如何设置return的进程退出码呢?

1.不关心进程退出码-------》return 0;

2.关心进程退出码的时候-------》特定的错误,返回特定的错误

成功用0表示,非0表示失败(1野指针,2逻辑错误)数值对人不直观

一般而因,退出码都有文字描述strerror,也可以自己定义

//查看strerror对应的错误信息
#include <stdio.h>
#include <string.h>

int main()
{
	for(int i = 0 ;i< 200 ;i++)
	{
		printf("%d:%s\n",i,strerror(i));	
	}
	return 0;
}

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

int main()
{
	printf("hello world\n");
	exit(12);
	return 0;
}

3.2.2进程退出的情况

  • 代码运行结束,结果正确------return 0;
  • 代码运行结束,结果不正确-----return !0;
  • 代码没跑完,程序异常了-----退出码无意义(除0,越界,野指针)

3.2.3.进程的结束方式

1.main函数结束

2.任意地方exit(int status)-----终止这个进程,这里的status就是进程退出码

3._exit(int status)-----终止这个进程,这里的status就是进程退出码

#include

#include

void exit(int status);

void _exit(int status);

void _Exit(int status);

结束当前的进程并将status返回

exit结束进程时会刷新(流)缓冲区

1.exit() vs _exit()

exit为库函数,在系统调用之上的,底层就是调用_exit()

_exit为系统调用(操作系统层调用)

在执行

        printf("hello world"); //没有\n

        sleep(2);

        exit(1);

执行会出现,当系统终止的时候才打印出hello world

将exit换成_exit

执行会出现不打印hello world,直接结束

原因:这是因为hello world在缓冲区,没有被刷新到屏幕上,当线程结束的时候,执行exit(1)时,是调用底层的_exit(),主动刷新缓冲区,会刷新到屏幕上,而在执行_exit(1)时,系统直接关闭线程,不会再打印。调用

3.3进程等待

  1. Z状态:僵尸状态:子进程退出,父进程没有获取到子进程的状态。
  2. 通过进程等待的方式,解决僵尸进程。

1.进程等待必要性

  • 之前讲过,子进程退出,父进程如果不管不顾,就可能造成‘僵尸进程’的问题,进而造成内存泄漏。
  • 另外,进程一旦变成僵尸状态,那就刀枪不入,“杀人不眨眼”的kill -9 也无能为力,因为谁也没有办法杀死一个已经死去的进程。
  • 最后,父进程派给子进程的任务完成的如何,我们需要知道。如,子进程运行完成,结果对还是不对,或者是否正常退出。
  • 父进程通过进程等待的方式,回收子进程资源,获取子进程退出信息。

进程等待的目的:回收子进程的资源,获取子进程退出的信息

3.3.1.进程等待的方法

1.wait等待成功(返回值为子进程Pid,失败为-1)

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

int main()
{
    pid_t id=fork();

    if(id == 0)
    {
        int cnt =10;
        while(cnt)
        {
            printf("son,pid:%d,ppid:%d,cnt:%d\n",getpid(),getppid(),cnt--);
            sleep(1);
        }
        exit(0);
    }
    sleep(15);
    pid_t id2 = wait(NULL);
    if(id2 >0)
    {
        printf("wait success %d\n",id2);    
    }
         sleep(5);

}
2. waitpid()等待获取子进程状态信息(返回值为子进程Pid,失败为-1)

pid_t id2 = waitpid(id,&status,0); //参数:等待进程id,等待状态(子进程的进程退出码),0阻塞式等待

pid_t id2 = waitpid(id,&status,WNOHANG); //WNOHANG:非阻塞等待

作用:

        1.让OS释放子进程的僵尸状态

        2.获取值进程的退出结果

        3.在等待期间,子进程没有退出的时候,父进程只能在阻塞状态。

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: 若pid指定的子进程没有结束,则waitpid()函数返回0,不予以等待。若正常结束,则返回该子进程的ID。
if(WIFEXITED(status))
{
    printf("子进程正常退出,进程退出码:%d\n",WEXITSTATUS(status));
}
else{
    printf("子进程异常退出\n")
}

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

int main()
{
	pid_t id=fork();

	if(id == 0)
	{
		int cnt =10;
		while(cnt)
		{
			printf("son,pid:%d,ppid:%d,cnt:%d\n",getpid(),getppid(),cnt--);
			sleep(1);
		}
		exit(10);
	}
	//sleep(15);
        int status = 0;
	pid_t id2 = waitpid(id,&status,0); //参数:等待进程id,等待状态(子进程的进程退出码),阻塞式等待
	if(id2 >0)
	{
		printf("wait success %d,status:%d\n",id2,status);	
	}
         sleep(5);
}

 

执行上述代码的时候会发现:

这里的status=2750;

注意:这里的 status不是被整体使用的,有自己的位图结构

进程退出的三种方式

1.运行完

  • 代码运行结束,结果正确------return 0;
  • 代码运行结束,结果不正确-----return !0;

2.异常

  • 代码没跑完,程序异常了-----退出码无意义(除0,越界,野指针)

通过status要体现上面的三种情况。

status不是被整体使用的,有自己的位图结构

次第8位退出状态,第7位终止信号-----》表示是否正常结束。

退出状态就是进程退出码

kill -l就可以看到进程退出的信号

所以退出的时候就要这样看

printf("wait success %d,
          sig_number:%d,
          child_exit_code:%d\n"
          ,id2,(status&0x7F),(status>>8)&0xFF);

总结:

1.进程退出会变成僵尸------会把自己的退出结果写在自己的task_struct(pcb)

2.wait/waitpid是一个系统调用--------》OS有资格和能力去读取子进程的task_struct(pcb)

3.从退出子进程的task_struct(pcb)中获取

 3.非阻塞VS阻塞

用打电话来类比

1.张三不挂电话-----》检测李四的状态=====》阻塞

2.张三不时的打电话李四-----》本质,状态检测,如果李四没有就绪,张三直接返回====》非阻塞(每次打电话都是非阻塞等待,多次非阻塞等待称为轮询

3.把打电话的状态叫做系统调用wait/waitpid,张三----》父进程,李四------》子进程。

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

int main()
{
	pid_t id=fork();
	assert(id >= 0);

	if(id == 0)
	{
		int cnt =10;
		while(cnt)
		{
			printf("son,pid:%d,ppid:%d,cnt:%d\n",getpid(),getppid(),cnt--);
			sleep(1);
		}
		exit(0);
	}
	int status = 0;
	while(1)  //非阻塞轮询等待
	{
	   	pid_t ret = waitpid(id,&status,WNOHANG);//WNOHANG:非阻塞等待----》子进程没有退出,父进程检测,立即返回
		if(ret == 0) 
		{
			//子进程没有退出,我的waitpid没有失败,仅仅是检测到子进程没有退出。
			printf("wait done,but child is runing....\n");
				
		}else if(ret >0)
		{
			//1.waitpid调用成功&&子进程退出了
			printf("wait success,exit code :%d, sig :%d\n",WEXITSTATUS(status),status&0x7F);	
		}else
		{
			//waitpid调用失败
			printf("wait pid failed\n");
		}
		sleep(1);
	}
}

为什么要非阻塞等待

不会占用父进程的所有精力,可以在轮询的时候,可以干点别的。

3.4进程的替换

3.4.1预备知识

1.环境变量

环境变量(environment variables)一般是指在操作系统中用来指定操作系统运行环境的一些参数

要在执行一个指令,要先找到程序。

./mypross -> ./表示当前路径----》找到该程序

如果想让./mypross 不加./执行

方法1

        sudo cp mypros /usr/bin/

不推荐,因为你写的代码是没有经过测试的,sudo rm /usr/bin/mypros

为什么在/usr/bin文件夹下,就可以直接被找到,因为存在环境变量。

方法 2

将当前路径添加到环境变量去

        export PATH = /xxxx/xxxxx/xxxxxx(不敢这样,会覆盖原来的环境变量)

        export PATH=$PATH:/XXX/xxx/xxx/xxx (这样可以)

环境变量:

echo $HOME //当前用户的工作目录

echo $HOSTNAME //主机名

echo $LOGNAME //当前登陆的用户

echo $HISTSIZE //历史命令

env直接查看系统所有的环境变量

操作系统在启动BASH的时候就预先给我们设置好,未来我们要用到的变量,这就是环境变量。

命令行也是可以添加环境变量的,命令行也是进程,运行时可以对空间进行动态调整,就可以保存数据。变量的本质就是数据。

2.环境变量的操作

getenv(获得环境变量man 3 getenv) vs putenv(改变环境变量)

myval = 1234567

echo $myval

当执行env | grep myval时,是什么也没有的

当用set | grep myval 时,会出现

set既显示环境变量,又显示本地变量

unset myval //取消环境变量

这里的myval为本地变量(就像C中的局部变量)

export myval //可以将本地变量转为环境变量,当执行./myval时会输出myval = 1234567

unset myval //取消环境变量

当执行./myval 的时候 -----bash(系统进程)会把myval变成一个进程(bash的子进程)(其实是fork())------环境变量具有全局属性------是会被子进程继承下去(为什么被继承,为了不同的应用场景)『如认证身份,获取身份信息』本地变量只会在当前进程(bash)内有效。

3.命令行参数

int main()函数可以有几个参数(3个)

int argc,char* argv[ ] char *env[ ] //命令行参数(整型,指针数组(字符串),环境变量指针数组(字符串)),他的作用:

程序名 选项,依次传给argv的,程序名+选项一共有几个,argc就等于几。

//ls -a -b -c -d =======>这整体是个大字符串---》“ls” “-a” “-b” “-c” “-d” NULL,argv存储的就是该字符串。

这些是SHELL和系统来做到操作,将命令行解析成上面的样子。

其中环境变量指针数组跟指针数组一样,以NULL 结尾

for(int i= 0;env[i];i++)
{
    printf("env[%d]:%s\n",i,env[i]);
}

当然,main可以不传环境变量,可以用getenv获取值

也可以通过extern char **environ(第三方环境变量,系统给的,指向环境变量指针数组的指针,二级指针)

每个程序都会收到一张环境表,环境表是一个字符指针数组,每个指针指向一个以’\0’结尾的环境字符串

4.三种获得环境变量的方法
  1. getenv ---------->推荐使用,直接获得值
  2. char* env[]
  3. extern char**environ

3.4.2进程替换的方式

进程替换就是将指定的程序加载到内存中去,让指定进程进行执行。

1.找到程序 2.如何执行新程序

1.exec函数

进程调用exec函数族执行某个程序

  • 进程当前内容被指定的程序替换

实现让父子进程执行不同的程序

父进程创建子进程

子进程调用exec函数族

父进程不受影响

进程号不会改变,最后一定要加NULL

其实有六种以exec开头的函数,统称exec函数族:

#include <unistd.h>

int execl(const char *path, const char *arg, ...);//调用失败返回-1,成功没有返回值,这是因为,成功就和后面的代码没有关系了。只要返回一定错了。...为可变参数列表

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 *path, char *const argv[], char *const envp[]);

创建子进程的目的?

  • 让子进程执行父进程代码的一部分---》执行父进程对应磁盘代码中的一部分。
  • 想让子进程执行一个全新的程序----》让子进程想办法,加载磁盘上指定程序,执行新程序的代码和和数据(进程的程序替换)
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include <assert.h>

int main()
{
	
	
	printf("process is running....\n");
	pid_t id = fork();
	assert(id >=0);
	if(id == 0)
	{
		execl("/bin/ls","ls","-a","-l",NULL);
		exit(1);
	}
	else
	{
		int status = 0;
		pid_t ret = waitpid(id,&status,0);
		if(ret > 0) printf("wait success,code_exit:%d,sig:%d\n",(status>>8)&0xff,status&0x7f);
	}

	return 0;
}

上述代码的子进程就只执行ls -a -l的指令,然后退出。

程序替换的原理就是:

将指定程序的代码和数据加载到指定的位置和数据! 覆盖自己的代码数据。

进程替换的时候,没有创建新的进程。

(最后的输出语句没有执行)的原因是:execl执行完毕的时候,代码已经完全被覆盖,开始执行新的代码了。

虚拟地址空间+页表保证进程独立性,一旦有执行流替换代码和数据,就会发生写实拷贝。

 1.l:list

int execl(const char *path, const char *arg, ...);//调用失败返回-1,成功没有返回值,这是因为,成功就和后面的代码没有关系了。只要返回一定错了。

execl("/bin/ls","ls","-a","-l",NULL);

2.l:list p:path //不用告诉我路径,只说是谁就行,自动在环境变量中找可执行程序。

int execlp(const char *file, const char *arg, ...);

execlp("ls","ls","-a","-l",NULL);//参数1执行谁,2执行命令

3.l:list e:自定义环境变量

int execle(const char *path, const char *arg, ...,char *const envp[]);

mybin内容
printf("PATH:%s\n",getenv("PATH"));
printf("PWD:%s\n",getenv("PWD"));
printf("MYPATH:%s\n",getenv("MYPATH"));
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@

char* const envp[] = {(char*)"MYPATH=111122233",NULL}             
//上面是自定义环境变量
execle("./mybin","mybin",NULL,envp);

extern char**environ
execle("./mybin","mybin",NULL,environ);
//environ为系统环境变量,默认环境变量你不传,子进程也能获取
当既想要系统的环境变量,也想要你自己的环境变量就可以使用putenv
这就是为什么有的exec*不加e
将自己的变量加入环境变量
put((char*)"MYPATH=111122222333");

4.v:vector可以将所有的执行参数放入数组中,统一传入,不用使用可变参数传参了。

int execv(const char *path, char *const argv[]);

char * const argv_t[] = {"ls", "-a","-l",NULL}; execv("/bin/ls",argv_t);

5.v:vector p:path

int execvp(const char *file, char *const argv[]);

char * const argv_t[] = {"ls", "-a","-l",NULL}; execvp("ls",argv_t);

int execve(const char *path, char *const argv[], char *const envp[]);

这个才是真正的系统调用,其他的都是封装,为了让你有更多的选择性。

事实上,只有execve是真正的系统调用,其它五个函数最终都调用execve,所以execve在man手册 第2节,其它函数在man手册第3节。

程序替换,可以使用程序替换,调用任何后端语言对应的可在执行程序====程序。

为什么我们的程序要加载到内存里,因为冯诺伊曼体系结构,必须要CPU运行代码,如何让加载呢?--------》 Linux用的是exec*系列的接口,exec加载器。

先加载execle还是执行main(int argc,char* argv[],char* env[])函数呢?先加载。

int execle(const char *path, const char *arg, ...,char *const envp[]);

main函数的参数就是从execle中来的,你不传,系统也会给你传。

2.system

#include

int system(const char *command);

成功时返回命令command的返回值;失败时返回EOF

当前进程等待command执行结束后才继续执行

与exec不同的是,他不会替代程序,只是向进程中插入

#include <stdio.h>
#include <stdlib.h>

int main(int argc, char const *argv[])
{
    system("ls -a -l ./");
    printf("没有被替换\n");
    return 0;
}

4.进程常用命令

4.1PS命令(快照系统进程)

ps -ajx //显示进程的详细信息,包括进程的作业控制信息以及与该进程相关的其他进程。

ps -elf 普通使用标准查看

ps -aux BSD标准查看

ps 命令详细参数:

-e:显示所有进程

-l:长格式显示更加详细的信息

-f 全部列出,通常和其他选项联用

 4.2top命令(实时任务管理器)

shift+>向后翻页

shift+

top -p PID查看某一进程

4.3nice改变正在运行进程的优先级&& 用top命令更改已存在进程的nice:

top

进入top后按“r”–>输入进程PID–>输入nice值

nice指定的优先级运行进程

nice -n NI值 要启动的进程

renice改变正在运行进程的优先级

renice [NI值] PID

4.4kill终止进程命令

使用kill命令终止进程

–kill  [-signal] PID

signal是信号,PID是进程号

kill 命令向指定的进程发出一个信号signal,在默认的情况下,kill 命令向指定进程发出信号15,正常情况下,将杀死那些不捕捉或不忽略这个信号的进程

kill -l查看信号

 4.5jobs 查看后台进程

程序+&运行起来的状态为后台运行

也可以使用ctrl+Z让进程进入T状态,进入后台,用jobs可以查看

4.6bg 将挂起的进程在后台运行

4.7fg 把后台运行的进程放到前台运行

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值