Linux 进程控制

一、Linux进程建立:

vfork()

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

int main()
{
   pid_t pid;
   int status;
    if((pid=vfork())==0)
    {
      sleep(2);
        printf("child running \n");
        printf("child sleeping \n " );
        sleep(5);
        printf("child dead \n ");
        exit(0);
   }
   else if(pid>0)
   {
        printf("parent running \n ");
        printf("parent exit \n " );
        exit(0);
  }
  else
   {
        printf("fork error \n ");
        exit(0);
  }

return 0;
}

将其中的vfork函数换成fork函数:

 由此可以看出,vfork函数创建的子进程进程和父进程共享资源,父进程调用vfork函数时,自身将被暂时阻塞,子进程会借用父进程的地址空间运行,直到子进程退出,父进程才继续执行,这与fork函数不同。

1.1fork函数内部完成的工作:

pid_t fork(void)

返回值:

>0    --返回给父进程,告诉其子进程的PID

==0      --返回给子进程,

<-1   --返回给父进程,表示创建失败。

创建子进程、子进程拷贝父进程的PCB;

-->分配新的内存块、内核数据结构(task_struct)给子进程;

(进程在操作系统中都有一个户口,用于表示这个进程。这个户口操作系统被称为PCB(进程控制块),在linux中具体实现是 task_struct数据结构。)

-->将父进程的部分数据结构内容添加到子进程

(子进程继承了父进程的整个地址空间,其中包括了进程上下文,堆栈地址,内存信息进程控制块(PCB)等)

-->将子进程添加到系统的进程列表中,添加到双向链表中

**进程队列的组织:

为了便于对进程进行调度、管理,需要对于进程实行合理的组织,即对于进程控制块PCB的管理组织;

PCB的组织通常有两种方式:

线性表:

 

 链表:

 

 

 

 

 链表或者进程队列,是将进程按照不同的状态,将其的PCB放入不同的队列中,每种队列用PCB的队列指针指向下一个PCB,最后一个PCB的指针为" 0",表示队尾;

当一个正在运行的队列由于等待某一事件时,就将其相应进程的PCB放入阻塞队列,而等到某个事件到来时,就将其送入就绪队列。

这样管理进程的方式,可以使得PCB的数量不受限制,实现列动态申请、释放,但其缺点是动态分配PCB所占内存的算法较为复杂,额外的指针也会占用一定空间。

就绪队列按照何种次序排列PCB取决于进程调度算法

以下是几种常用的进程调度算法:

1.静态优先级法:

按照进程要执行任务的轻重缓急,使得每个进程有了优先级的区分,使用优先数来表示优先级。不同操作系统的具体对应方法不同,有的优先数大表示优先级大,有的则反之。

而如果在创建进程时就确定了其优先级,且在运行时不会改变,则将其称为静态优先级法:

静态优先级确定进程优先级的方式:

(1)按进程类型:

通常,操作系统中有两类进程:系统进程、用户进程,一个进程可以有两种运行状态:用户态、核心态。由于系统的许多操作(存储、输入输出等)都需要系统进程,所以系统进程的优先级应该比用户进程高。(在批量处理、分时结合的操作系统中,为了加快和分时用户的交互速度,前台进程的优先级高于后台进程)

(2)按作业的资源要求:

根据作业要求系统提供的处理时间、内存大小等;时间越长、内存占用越大,其优先级越低,从而有益于短作业,这也使得作业的平均运转时间最小、改善了进程调度。

(3)按作业到达的时间(先到先来)

使得较早到达的作业有较高优先级,该方式构成了先来先服务算法(FCFS)

(4)按用户类型和要求确定

系统可以按照用户提出的要求设置进程的优先级,即用户花钱购买优先级

2.动态优先级算法:

静态优先级实现较为简单,但不能反应进程在运行时的各种变化;若按照变化情况对各个进程的优先级进行适当的调整,则可以获得更好的调度结果。

3.时间片轮转法:

虽然上述的算法可以使得多道批量处理系统可以获得较为满意的调度效果,但是只有优先级高的进程执行完之后,优先级低的进程才能执行,要等待较长的时间,因此,在分时系统中,常采用时间片轮转的方式。

系统先将所有就绪的基础按照FCFS规则排成一个队列,首先将处理机分配给队列中的第一个进程,并使其执行一段规定的时间,用完规定的时间片后,就将其放入队尾,然后又把处理机分给下一进程,如此循环。若某一个进程的时间片未用完,却在等待I/O事件,则将其放入相应的阻塞队列,I/O事件完成后,再将其放入就绪队列的尾部。

在时间片轮转中,有:T=N*q,其中T为系统的响应时间、N为就绪队列的进程数目、q为时间片的大小,若系统分配的时间片q足够大,则所有进程都能在规定时间内完成,此时就变成了FCFS法,但这对于短作业、I/O操作较多的作业都很不利,但若q过于小会导致系统的开销增大,因此时间片的选择必须适当。

**时间片的长短选择因素:

(1)系统的响应时间

(2)就绪队列的进程数目

   (3)  进程之间的转换时间

 (4)计算机的处理能力,计算机的运算速度、读取速度越快、时间片就可以越短。

但采用固定时间片、一个就绪队列,系统的服务质量依然不够理想,可做两方面改进:

(1)改时间片为可变时间片

(2)将单就绪队列改为多就绪队列     

可变时间轮转法:

1.系统根据 q=T/N计算时间片,每一轮开始,系统根据就绪队列已有的进程数计算一次q值,再进行轮转,此时所到达的基础都不进入就绪队列,等此次轮转完后再进入,再计算新一轮的q值。

2.时间片的长短取决于优先级的高低,优先级高的时间片长。

3.短作业的时间片短、长作业的时间片长。

4.多队列轮转法:

若一个进程经过多次的轮转才到达运行的终点,则会增大系统的开销,为此可以采用多队列轮转法,eg:设置三个队列,时间片的长度分别为;0.02s、0.2s、2s,若一较短时间片队列的一个进程未执行完,就将其放入较长时间片队列的末尾,当较短时间片队列中的进程都执行完后,再执行较长时间片队列中的进程。因此,短作业可以较快的占用处理机,长作业可以使用较长时间,从而避免了频繁调度造成的系统开销。

**作业、进程、程序之间的区别、联系:

作业:就是用户在一个事务处理中要求计算机系统所作的工作的总和,一个用户作业通常包括程序、数据、操作说明书。当一个作业被作业调度程序选中时 ,为其创建作业步进程。所以,一个作业可以划分为多个进程、而每个进程又有其实体--程序和数据集合。

进程与程序:一个进程通常由进程控制块、程序、数据集合组成,说明程序是进程的一部分。进程是程序的一次执行,而程序是一组有序的指令。一个进程可以执行多个程序;一个程序可由多个进程同时运行;程序是一种可以长期保留的软件资源,而进程因 创建而生、因 调度 而执行,因 得不到资源而阻塞, 因 撤销 而死亡。

进程具有独立性、相互制约性(进程的互斥、同步);进程是系统中资源分配的基本单位,而线程是运行的基本单位。

-->fork返回,操作系统开始调度(先来先服务、短作业优先、优先级优先、时间片轮转)

1.2用户空间&内核空间:

操作系统中有两类进程:系统进程、用户进程,一个进程可以有两种运行状态:用户态、核心态。

Linux操作系统、驱动程序都运行在内核空间,系统调用的函数都是在内核空间运行的;

而程序员写的代码都是运行在用户空间的,当用户空间的程序中调用了系统调用函数,则会切换到内核空间,执行完毕后再转换到用户空间,执行用户的代码。

 

进程与内核空间:

 

-->每一个进程都有一个页表结构

-->fork创建的子进程,也会拷贝父进程的页表关系

 写时拷贝:

父进程创建子进程,子进程拷贝父进程的PCB,页表也是拷贝父进程的,则虚拟地址到物理地址的映射关系也相同,即操作系统没有给子进程分配物理内存的空间进行存储,子进程的变量也是父进程中的内容。

当发生改变时,才以写时拷贝的方式进行拷贝,此时父子进程才通过不同的页表指向不同的地址空间,而不发生改变时,父子进程共享同一数据。

 **创建的子进程的特性:

--> 父子进程是独立运行的,互相之间不干扰,各自有各自的虚拟地址空间和页表,数据不会篡改。

--> 父子进程是抢占式运行的,执行的顺序不确定

--> 子进程是从fork之后运行的(由程序计数器、上下文指针控制)

--> 父子进程代码共享、数据独有。

**fork的一些用法:

守护进程:父进程创建了子进程,让其执行真正的业务,父进程负责守护着子进程,当子进程执行任务时意外崩溃,父进程就负责重启子进程,让其继续提供服务。 

二、Linux 进程终止

正常:

从main函数中的return 返回

调用了exit函数(库函数):在底层会调用系统的_exit函数,在此之前会清空stdio的缓存区,main函数的return 就等价于调用了该函数。

exit()函数:

int atexit(void *(function)(void))

参数含义:函数指针,即为保存函数的地址

atxit->注册函数保存的函数地址到内核中,但注册不是调用了该函数指针指向的函数

1.执行用户定义的清理函数

2.刷新缓冲区

使用:

 

 在该实例中,atexit函数注册了回调函数:atexit_callback,然后将下一行的printf 缓冲区刷新到了屏幕,然后又调用了回调函数。

调用了_exit函数(系统调用函数):关闭所有的文件描述符,然后退出进程。

*异常:

1.解引用空指针:

程序运行时发生了段错误; segmentation fault

2.double free 

3.内存访问越界

**通过进程退出状态信息判断是否是正常退出还是异常退出:

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

int main()
{
pid_t ret=fork();
if(ret<0)
{
 perror("fork ");
return 0;
}
else if(ret==0)
{
 printf("i am child! \n" );
 int *lp=NULL;
 *lp=10;
}
else
{
 int status;
 wait(&status);

 printf("exit signal: %d ]n", status& 0x7f); //0x7f -->0111 1111 
 printf("coredump flag: %d \n",(status>>7) & 0x1);
}
return 0;

 

 

看退出信号值:

 

0:正常退出

>0: 异常退出

三、进程等待:

1.进程等待的作用(必要性):

父进程等待子进程,等待其

<sys/wait.h>

wait()函数:

pid_t wait(int *status)

其中参数为输出型参数,

返回值:成功时返回被等待进程的pid,失败返回-1,可以通过该函数获取进程的退出状态信息;

pid_t  wait(int *status) 阻塞性函数,当调用该函数,如果有一个子进程已经终止,则函数立即返回,并释放子进程的所有资源,若没有子进程退出,但有正在执行的子进程,则wait将阻塞直到有子进程终止时才返回,若没有正在执行的子进程,则返回-1。

阻塞属性的验证:

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

int main()
{
pid_t ret=fork();
if(ret<0 )
{
 return 0;
}
else if(ret==0)
{
 sleep(40);
 printf("i am child \n");
}
else
{
wait(NULL);
}
return 0;
}

使用pstack查看进程运行情况: 

 

 

waitpid()函数;该函数对等待哪个进程终止以及是否采用阻塞操作方式给出了更多的控制

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

pid: pid=-1,且options=0 时,该函数等同于wait()函数,pid=-1时,等待任何子进程的终止状态;当pid>0时,要求知道进程为pid的子进程的终止状态

 status:退出状态信息

options:用户指定的附加选项,常用选项为 WHO_HANG,它设置函数为非阻塞状态,通知内核在没有已经终止的子进程时,不要阻塞(若pid指定的子进程没有结束,则函数返回值为0,不等待。)

用法:

非阻塞函数一定搭配循环使用  eg:

pid_t pid;
int data;
while((pid=waitpid(-1,&status,WNOHANG))>0)
{        
   printf("child proc *d terminated\n",pid); 
}

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值