多任务并发(1)进程入门
多任务简介
多任务处理是指用户可以在同一时间内运行多个应用程序,每个应用程序被称作一个任务。Linux、windows就是支持多任务的操作系统,比起单任务系统它的功能增强了许多。
当多任务操作系统使用某种任务调度策略允许两个或更多进程并发共享一个处理器时,事实上处理器在某一时刻只会给一件任务提供服务。因为任务调度机制保证不同任务之间的切换速度十分迅速,因此给人多个任务同时运行的错觉。多任务系统中有3个功能单位:任务、进程和线程。
进程总括
什么是进程
进程,顾名思义即进行中的程序。
—》进程:系统分配资源的最小单位
—》线程:系统执行任务的最小单位
(1)**一个特殊的进程 —》init ,进程号是1 —》他是所有进程的祖先 **
有一个更加特殊的进程,调度进程,也叫做系统进程,他的进程号是0
(2)管理进程的结构体 task_struct ,也叫做进程控制块(PCB),这个结构体在 sched.h 中,这个结构体包含进程运行时所需的所有资源 (堆、栈、数据段等)。find /usr/ -name sched.h
进程的一生
进程状态
- 就绪态、运行态:TASK_RUNNING
- 睡眠态、挂起态:TASK_INTERRUPIBLE /TASK_UNINTERRUPIBLE
- 暂停态:TASK_STOPPED / TASK_TRACED
- 僵尸态:EXIT_ZOMBIE
- 死亡态:EXIT_DEAD
(1)从上图中可以看到,一个进程的诞生,是从其父进程调用fork()/vfork()开始的。当我们运行一个程序时,其需要相应的资源(堆 、栈、数据段),那么fork()/vfork()后,相当于复制了一份同样的资源给子进程。
(2)进程刚被创建出来时,处于就绪态,处于该状态的进程可以是正在进程等待队列中排队,等待系统调度,内核中的函数sched()称为调度器,它会根据各种参数来选择一个等待的进程去占用CPU,进程占用CPU后进入执行态。
进程占用CPU之后就可以真正运行了,运行时间有个规定,比如20ms,这就是“时间片”的概念。时间片耗光的情况下如果进程还没有结束,那么会被系统重新放入等待队列中等待。另外,正处于“执行态”的进程即使时间片没有耗光,也可能被别的更高优先级的进程“抢占”CPU,被迫重新回到等待队列中等待。
(3)当进程收到SIGSTOP或SIGTSTP中的一个信号时,状态会被置为暂停态(TASK_STOPPED) ,该状态下的进程不再参与调度,但系统资源不释放,直到收到SIGCONT信号后被重新置为就绪态。
当进程被追踪时(典型情况是被调试器调试时),收到任何信号状态都会被置为TASK_TRACED,该状态与暂停态是一样的,一直要等待SIGCONT才会重新参与系统进程调度。
(4)进程处于“执行态”时,可能会由于某些资源的不可得而被置为“睡眠态/挂起态”,比如进程要读取一个管道文件数据而管道而为空,或者进程要获得一个锁资源而当前锁不可获取,或者自己调用sleep()来强制自己挂起,此时状态变为TASK_UNINTERRUPIBLE或TASK_INTERRUPIBLE。
TASK_UNINTERRUPIBLE称为深度睡眠,不能响应信号。
TASK_INTERRUPIBLE称为浅度睡眠,可以响应信号。
当进程所等待的资源变得可获取时,会重新回到等待队列中等待。
(5)运行的进程跟人一样,迟早都会死掉。进程的死亡可以有很多方式,可以是正常退出,也可以是异常退出。上图中,在主函数内return或调用exit(),包括在最后线程调用pthread_exit()都是正常退出,而受到致命信号死掉的情况则是异常死亡。不管怎么死亡,最后内核都会调用do_exit()的函数来使得进程的状态变成所谓的僵尸态EXIT_ZOMBIE。
(6)一个进程死掉后,其“死亡信息”都会被一一封存在该进程的PCB当中,这时父进程会调用wait()/waitpidf()来查看孩子的“死亡信息”,顺便将孩子的状态设置为死亡态EXIT_DEAD,因为处于这个状态的进程的PCB才能被系统回收。因此,父进程应及时调用wait()/waitpid(),否则系统会充满越来越多的“僵尸”,导致内存溢出,程序崩溃。
(7)有没有可能在那些子进程变为僵尸时,父进程已经先死亡了呢?答案是有可能的,这时子进程会变为孤儿进程,那么,该如何保证父进程及时调用wait()/waitpid()从而避免僵尸进程泛滥呢?其实解决办法在之前已经提到过了,一个特殊的进程init。Linux系统保证任何一个进程(除了init)都有父进程,也许是其真正的生父,也许是其祖先init,它会收养这些孤儿进程。
进程的API
- pid_t pid = fork( );
- 将当前的进程复制一份,然后这两个进程同时从本函数的下一语句开始执行。
- 所有的代码、变量都会复制成两份。
- 返回值:在原先的进程中返回一个大于零的子进程的PID在新建的进程中返回0。
- 两个进程是并发执行,没有先后次序。
- 要控制进程的进度,要依赖于信号量、互斥锁、条件量
- wait( int *status )
- 主要功能:让父进程阻塞等待子进程直到子进程结束;回收子进程的资源;获取子进程的退出状态(包括退出值、终止信号)。
- wait无法指定回收特定的子进程,哪个子进程先死掉就收哪个。
- 如果 status 为 NULL, 代表父进程不关心、不获取、不需要子进程的退出状态。
- status 用来存放子进程的退出状态。
- waitpid( pid_t pid , int *status , int option)
- 主要功能:让父进程回收子进程的资源;获取子进程的退出状态(包括退出值、终止信号)。
- 参数 pid 用来控制究竟想要回收哪个子进程。
- 如果 status 为 NULL, 代表父进程不关心、不获取、不需要子进程的退出状态。
- 参数 option 用来控制等待的策略,如果是0,那就是默认的阻塞等待,如果是WNOHANG,这代表不阻塞,指定的子进程如果当时已经是僵尸了那么就立即回收,否则waitpid()立即返回。
- atexit( void (*cleanup)(void) )
- 主要功能:注册一个进程收尾时自动执行的清理函数。
- 函数 cleanup 的接口规定: void cleanup (void)
- 函数 cleanup 的内容,一般就写释放内存、关闭文件等收尾工作。
- 可以为一个进程,注册多个退出处理函数,他们的调用顺序根注册顺序相反。
- exit( int code )
- 主要功能:退出当前进程。
- 退出值 code 的取值范围 0-255,一般而言0代表正常退出,非0代表错误。
- 退出的时候,会自动干两件事情:
1、调用由 atexit() 注册过的退出处理函数。
2、刷新缓冲区中的数据。
- _exit( int code ) / _Exit( code )
- 主要功能:退出当前进程。
- 退出值 code 的取值范围 0-255,一般而言0代表正常退出,非0代表错误。
- 退出的时候,直接走人,不管退出处理函数,也不管缓冲区。
进程API的运用
1、使用进程复刻技术,在一个程序中同时不断循环输出阿拉伯数字、英文字母。
#include <stdio.h>
#include <unistd.h>
#include <errno.h>
int main(void)
{
int i;
// 产生一个子进程
pid_t pid = fork(); // 复刻一个进程
// 父进程
if(pid > 0)
{
printf("PID: %d, PPID: %d\n", getpid(), getppid());
for(i=0; ; i++)
{
sleep(1);
i %= 26;
fprintf(stderr, "%c", 'a'+i);//标准错误,不带缓冲
//fprintf(stdout, "%c", 'a'+i);//标准输出,带缓冲区
//printf("%c", 'a'+i);
}
}
// 子进程
else if(pid == 0)
{
printf("PID: %d, PPID: %d\n", getpid(), getppid());
for(i=0; ; i++)
{
sleep(1);
i %= 10;
fprintf(stderr, "%c", '0'+i);
}
}
else
{
perror("创建进程失败");
}
return 0;
}
2、编程产生一个进程扇,即一个父进程产生一系列子进程,要求每个子进程输出自己的PID后退出,父进程等所有子进程都退出之后,也输出自己的PID,然后退出。
#include<stdio.h>
#include<stdlib.h>
#include <unistd.h>
int main()
{
pid_t ret;
int i;
int num = 5;
for(i=0;i<num;i++)
{
ret = fork();
if(ret > 0)
{
continue;
}
if(ret == 0)//子进程
{
printf("子进程pid = %d,对应的父进程pid = %d\n",getpid(),getppid());
exit(1);//结束进程
}
}
for(i=0;i<num;i++)
{
//阻塞等待任意子进程,回收子进程物理内存
wait(NULL);
}
printf("父进程pid = %d\n",getpid());
return 0;
}
3、编程产生一个进程链,父进程派生一个子进程后,输出自己的PID,然后退出,该子进程继续派生子进程,然后打印PID,然后退出,以此类推。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <sys/wait.h>
int main(void)
{
int i, n;
printf("请输入子进程数:");
scanf("%d", &n);
#if 1//父进程比子进程先打印PID,PID递增
for(i=0; i<n; i++)
{
printf("PID:%d,PPID:%d\n", getpid(),getppid());
pid_t pid = fork();
if(pid > 0)
{
exit(0);
}
else if(pid == 0)
{
printf("===========\n");
continue;
}
}
#endif
#if 0//子进程比父进程先打印PID,PID递减
for(i=0; i<n; i++)
{
pid_t pid = fork();
//父进程删库跑路
if(pid > 0)
{
//等待子进程完成之后,打印PID和PPID
wait(NULL);
printf("PID:%d,PPID:%d\n", getpid(),getppid());
exit(0);
}
else if(pid == 0)
{
continue;
}
}
#endif
return 0;
}
**进程入门总结**
学习了多任务编程可以让我们更加方便了解系统运行原理,了解系统多任务之间的交流。