目录
基础概念
搜索文件“15、系统编程第二天”
增加:
程序和进程
程序运行起来,产生一个进程
程序,是指编译好的二进制文件,在磁盘上,不占用系统资源(cpu、内存、打开的文件、设备、锁....)
进程,是系统资源分配的最小单位。是一个抽象的概念,进程是活跃的程序,占用系统资源。在内存中执行。
进程的状态
- 初始态:一般与就绪态归为一类
- 就绪态:一切都准备好了,就等待CPU分配时间片了
- 运行态(执行态):占用CPU中
- 挂起态:等待除了CPU以外的其他资源,主动放弃CPU(遇到缺少资源或发生中断)
- 终止态
为什么要有三态?
多个进程一起进入执行态可能会发生多进程死锁,动不了了。三态的好处就是有一个缓冲区,可以一个一个去处理它
如何创建一个进程
运行一个可执行文件,fork,vfork,exec函数族,system(clone是啥?)
并发与并行
并发执行:就是CPU轮换的执行,当前进程执行了一个短暂的时间片(ms)后,切换执行另一个进程,如此循环往复,由于时间片很短,
在宏观上我们会感觉到所有的进程都是在同时运行的,但是在微观上cpu每次只执行某一个进程的指令。(单核CPU)
并行执行:如果cpu是多核的话,不同的cpu核可以同时独立的执行不同的进程,这种叫并行运行。所以当cpu是多核时,并发与并行是同时存在的。
进程互斥
进程互斥是指当有若干进程都要使用某一共享资源时,任何时候最多允许一个进程使用,其他要使用该资源的进程必须等待,直到占用该资源者释放了该资源为止。
进程同步
一组并发进程按一定的顺序执行的过程称为进程间的同步。
进程同步包含(保证)进程互斥。
就像上厕所,如果没有同步,没有访问顺序,一个人出来,其他人谁先抢到谁用,有了同步,厕所外可似乎排队,一个一个按顺序用。
具有同步关系的一组并发进程称为合作进程,合作进程间互相发送的信号称为消息或事件。
临界资源&临界区
- 临界资源:操作系统中将一次只允许一个进程访问的资源称为临界资源。
- 临界区:进程中访问临界资源的那段程序代码称为临界区。为实现对临界资源的互斥访问,应保证诸进程互斥的进入各自的临界区。
为什么要保持进程同步——进程死锁
多个进程因竞争资源而形成一种僵局,若无外力作用,这些进程都将永远不能再向前推进。
进程调度
操作系统的核心就是任务(进程)管理
- 先来先服务调度算法
- 最短作业优先调度
- 基于优先级调度
- 循环调度或时间片轮转法
linux进程特点
Linux系统是一个多进程的系统,它的进程之间具有并行性、互不干扰等特点。也就是说,每个进程都是一个独立的运行单位,拥有各自的权利和责任。其中,各个进程都运行在独立的虚拟地址空间,因此,即使一个进程发生异常,它也不会影响到系统中的其他进程。
每个进程拥有独立进程空间的优缺点
优点:
- 对编程人员来说,系统更容易捕获随意的内存读取和写入操作
- 对用户来说,操作系统将变得更加健壮,因为一个应用程序无法破坏另一个进程或操作系统的运行(防止被攻击)
缺点:
- 多任务实现开销较大
- 编写能够与其他进程进行通信,或者能够对其他进程进行操作的应用程序将要困难得多
进程控制编程
获取ID
#include <sys/types.h>
#include <unistd.h>
pid_t getpid(void); //返回当前进程的id
pid_t getppid(void); //返回父进程的id
例.
#include <sys/types.h>
#include <unistd.h>
int main()
{
pid_t pid;
pid = getpid();
printf("pid = %d\n",pid);
while(1);
return 0;
}
进程创建 fork()
#include <sys/types.h>
#include <unistd.h>
pid_t fork(void);
/*
返回值:
1)正值(新创子进程的进程ID):父进程;
2)0:子进程:;
3)负值:出现错误;
*/
- 当fork()顺利完成任务时,就会存在两个进程,每个进程都从fork()返回处开始继续执行。
- 两个进程执行相同的代码(text)段,但是有各自的堆栈(stack)段、数据(data)段以及堆(heap)。
- 子进程的stack、data、heap segments是从父进程拷贝过来的。(读时共享写时复制)
- fork()之后,哪一个进程先执行不确定。如果需要确保特定的执行顺序,需要采用某种同步(synchronization)技术(semaphores,file locks...)。
- 父子进程共享:文件描述符,mmap建立的映射区(两个进程间建立一个映射区,完成进程值之间数据传递)
- 父子进程相同之处:全局变量(是数据值相同,不是共享!)、.data、.text、栈、堆、环境变量、用户ID、宿主目录、进程工作目录、信号处理方式
- 父子进程不同之处:1.进程ID 2.fork返回值 3.父进程ID 4.进程运行时间 5.闹钟(定时器) 6.未决信号集
读时共享,写时复制
表面看起来fork()创建子进程子进程拷贝了父进程的地址空间(早期系统的确是这样的)其实不然 刚调用完fork()之后,子进程只是拥有一份和父进程相同的页表,其中页表中指向RAM代码段的部分是不会改变的,而指向数据段,堆段,栈段的会在我们将要改变父子进程各自的这部分内容时,才会将要操作的部分进行部分复制
shell并不知道运行的进程创建了子进程,所以shell进程在进程结束之后就开始执行自己,如果此时子进程并为结束运行,shell与子进程共同抢占CPU,所以会出现以下情况
例.
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
int main()
{
pid_t pid;
fork();
fork();
pid = fork();
if(pid < 0)
{
perror("fork");
return -1;
}
else if(pid > 0)
{
printf("parent pid is %d\n",getpid());
while(1);
}
else if(0 == pid)
{
printf("child pid is %d\n",getpid());
while(1);
}
return 0;
}
3个fork()调用后,共有8个进程(1生2,2生4,4生8)
vfork() (比较少使用)
#include <sys/types.h>
#include <unistd.h>
pid_t vfork(void);
- 子进程共享父进程的代码,数据,堆栈资源(父进程与子进程共享空间,变量可互相使用,改了一个变量之后另一个进程变量也改变了。)
- 使用vfork后,直接运行exec,节省了资源拷贝的时间
- 使用vfork,创建子进程后直接运行子进程,父进程被阻塞,直到子进程执行了exec()或者exit()。
- 子进程退出使用return会破坏父进程的堆栈环境(会释放数据段),产生段错误,所以退出使用exit或_exit
目的
vfork是为子进程立即执行exec的程序而专门设计的:
- 无需为子进程复制虚拟内存页或页表,子进程直接共享父进程的资源,直到其成功执行exec或是调用exit退出。
- 在子进程调用exec之前,将暂停执行父进程
clone()
fork()是全部复制,vfork()是共享内存,而clone()是则可以将父进程资源有选择地复制给子进程,
而没有复制的数据结构则通过指针的复制让子进程共享,具体要复制哪些资源给子进程,由参数列表中的clone_flags来决定。
另外,clone()返回的是子进程的pid。
exec函数族
无成功返回值,成功不返回,之后的程序不执行,失败返回-1,并执行之后的程序。一般exec之后只跟perror与exit两句就行了(也不用判断了直接写就行)
exec函数族和fork的区别
fork创建子进程后执行的是和父进程相同的程序。而子进程可以调用exec函数从而能执行另一个程序。当进程调用一种exec函数时,该进程的用户空间代码和数据完全被新程序替换,从新程序的启动例程开始执行。调用exec并不创建新进程,所以调用exec前后该进程的id并未改变。
exec函数名中英文字母意义(方便记忆)
- l (list) 命令行参数列表(命令写在".........","........","......",NULL)
- p (path) 在用户的绝对路径path下查找可执行文件,该文件必须在用户路径下,可以只指定程序文件名
- v (vector) 使用命令行参数数组(命令全都写在数组中,只传数组进去)
- e (environment) 为新进程提供新的环境变量
事实上,只有execve是真正的系统调用,其它五个函数最终都调用execve
一般来说
- execlp:系统可执行程序
- execl:用户自定义可执行程序
一些可能会用到的命令
- whereis 查看命令的路径
- pwd 查看文件的路径
execl ()
#include <unistd.h>
int execl(const char * path, const char* arg1,...)
/*
参数:
path : 被执行程序名(含完整路径);
arg1 - argn: 被执行程序所需的命令行参数,含程序名。以空指针(NULL)结束.
*/
例.
#include <stdio.h>
#include <unistd.h>
#include <errno.h>
int main()
{
int ret;
#if 0
ret = execl("/bin/ls","ls","-a","/home",NULL); //第一个参数是ls的绝对路径,-a表示显示隐藏文件,/home表示列举home路径下的文件
if(ret < 0)
{
perror("execl");
return -1;
}
#endif
ret = execl("/mnt/hgfs/share/2019/0119/exe5_8","exe5_8",NULL); //执行该绝对路径下的文件
if(ret < 0)
{
perror("execl");
return -1;
}
}
注:execl执行成功后自行结束了程序,所以execl函数之后的它都不会去执行。execl会载入你调用的程序,覆盖原有代码段,相当于你本来的程序的代码段被替换成execl执行的了,所以execl后面的都不会输出了。正确使用方法应该是fork一个子进程,在子进程中调用execl。
execlp ()
#include <unistd.h>
int execlp(const char * path, const char* arg1,...)
/*
参数:
path : 被执行程序名(不含路径,将从path环境变量中查找该程序;
arg1 - argn: 被执行程序所需的命令行参数,含程序名。以空指针(NULL)结束.
*/
例.
#include <stdio.h>
#include <unistd.h>
#include <errno.h>
int main()
{
int ret;
#if 0
ret = execlp("ls","ls","-a","/home",NULL); //第一个参数是ls的相对路径,-a表示显示隐藏文件,/home表示列举home路径下的文件
if(ret < 0)
{
perror("execlp");
return -1;
}
#endif
#if 1
ret = execlp("../0119/exe5_8","exe5_8",NULL); //执行该相对路径下的文件
if(ret < 0)
{
perror("execlp");
return -1;
}
#endif
}
execv ()
#include <unistd.h>
int execv(const char * path, const char *argv[])
/*
参数:
path : 被执行程序名(含完整路径);
argv[]: 被执行程序所需的命令行参数数组。
*/
例.
#include <stdio.h>
#include <unistd.h>
#include <errno.h>
int main()
{
int ret;
#if 0
char *argv[] = {"ls","-a","/home",NULL};
ret = execv("/bin/ls",argv);
if(ret < 0)
{
perror("execv");
return -1;
}
#endif
#if 1
char *argv1[] = {"exe5_8",NULL};
ret = execv("/mnt/hgfs/share/2019/0119/exe5_8",argv1);
if(ret < 0)
{
perror("execv");
return -1;
}
#endif
}
system ()
#include <stdlib.h>
int system(const char* string)
/*
函数说明:
创建子进程,并加载新程序到子进程空间,运行起来。
调用fork产生子进程,由子进程来调用 /bin/sh -c string来执行参数string所代表的命令。命令行怎
么输,string里面就怎么写
*/
例.
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
int main()
{
int ret;
system("../0119/exe5_8");
return 0;
}
进程终止 exit() _exit() —— 一个刷缓冲区一个不刷
exit()
#include<stdlib.h>
void exit(int status);
/*
函数说明:
exit()用来正常终结目前进程的执行,并把参数status返回给父进程。
参数:
用于标识进程的退出状态,shell或父进程可获取该值
0:表示进程正常退出
-1/1:表示进程退出异常
2-n:用户可自定义
*/
_exit()
#include<unistd.h>
void _exit(int status);
/*
函数说明:
此函数调用后不会返回,并且会传递SIGCHLD信号给父进程,父进程可以由wait函数取得子进程结束状
态。
*/
正常退出
- main调用return
- 任意地方调用exit库函数
- 任意地方调用_exit函数
异常退出
- 被信号杀死
- 调用abort函数
孤儿进程
父进程先于子进程结束,则子进程成为孤儿进程,子进程的父进程成为init进程,称为init进程领养孤儿进程。init进程的id可能为1也有其他。孤儿进程最终肯定都由init进程回收。
其执行顺序大致如下:在一个进程终止时,内核逐个检查所有活动进程,以判断它是否是正要终止的进程的子进程,如果是,则该进程的父进程ID就更改为1(init进程的ID);
僵尸进程
进程终止,父进程尚未回收(获得终止子进程的有关信息,释放它仍占用的资),子进程残留资源(PCB)存放于内核中,变成僵尸(Zombie)进程
残留的PCB是为了让父进程知道子进程的死亡状态,如果意外死亡可能需要报仇。如果一个进程在其终止的时候,自己就回收所有分配给它的资源,系统就不会产生所谓的僵尸进程了。如果不好好回收进程,这些残留的PCB会占用很多内存直至溢出。
产生过程(看看就行):
- 父进程调用fork创建子进程后,子进程运行直至其终止,它立即从内存中移除,但进程描述符仍然保留在内存中(进程描述符占有极少的内存空间)。
- 子进程的状态变成EXIT_ZOMBIE,并且向父进程发送SIGCHLD 信号,父进程此时应该调用 wait() 系统调用来获取子进程的退出状态以及其它的信息。在 wait 调用之后,僵尸进程就完全从内存中移除。
- 因此一个僵尸存在于其终止到父进程调用 wait 等函数这个时间的间隙,一般很快就消失,但如果编程不合理,父进程从不调用 wait 等系统调用来收集僵尸进程,那么这些进程会一直存在内存中。
怎么回收僵尸进程
除了wait和waitpid函数,用户用kill命令其实回收不了,因为它本身已经死了。还有一个办法就是杀死父进程,这样僵尸进程变为孤儿进程,被init领养,最后都由init回收。
有init领养的进程不会称为僵死进程,因为只要init的子进程终止,init就会调用一个wait函数取得其终止状态。这样也就防止了在系统中有很多僵死进程。
守护进程
守护进程(daemon)详解与创建:下面代码具体的函数信息以及为什么要这么做都在这里面
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <stdlib.h>
int main()
{
pid_t pid;
int i;
pid = fork();
if(pid < 0)
return -1;
else if(pid > 0)
exit(0);
setsid();//创建一个会话,把它变成该会话的组长
pid = fork();
if(pid < 0)
return -1;
else if(pid > 0)
exit(0);
chdir("/"); //改变文件目录
umask(0); //文件掩码
for(i = 0; i < getdtablesize(); i++) //关掉所有文件描述符
{
close(i);
}
while(1)
{
system("echo test >> /test.log"); //每隔5秒往文件里写一个test(echo没有>>是向屏幕打印的意思,有>>是重定向,向文件里写入内容)
sleep(5);
}
return 0;
}
该函数实现了一种后台进程:每隔5秒相test.log里写入一个英文单词test
echo:相当于输出。echo 1>1的意思是向文件1输入1
ps =ef | grep a.out:显示进程,用以验证a.out的确是在运行中的
ps aux:显示所有正在运行的进程及其具体信息?
tail -f | test.log:定时刷新显示test.log中的内容,以验证的确是每隔5秒被写入一个test
kill -9 5344:杀死进程id为5344的进程,告诉它终止的信号是编号为9的信号
kill -l:查看信号以及对应编号
进程等待 wait() waitpid()
wait()
#include <sys/wait.h>
pid_t wait(int *status);
/*
返回值:
若成功返回回收的子进程ID,若出错返回-1(也就是没有子进程可以回收了)。
*/
功能
- 阻塞等待子进程退出(子进程不退出,父进程就等待,不执行其他程序)
- 回收子进程残留资源
- 获取子进程结束状态(退出原因)(若不想知道原因,直接wait(NULL)即可)
有4个互斥的宏可以用来获取进程终止的原因:
- WIFEXITED(status) —— Wait IF EXITED
若子进程正常终止,该宏返回true。
此时,可以通过WEXITSTATUS(status)获取子进程的退出状态(exit函数的参数或者return的参数,此宏返回一个int)。
- WIFSIGNALED(status)
若子进程由信号杀死,该宏返回true。所有进程异常退出的根本原因是收到了信号。
此时,可以通过WTERMSIG(status)获取使子进程终止的信号值。
- WIFSTOPPED(status)
若子进程被信号暂停(stopped),该宏返回true。
此时,可以通过WSTOPSIG(status)获取使子进程暂停的信号值。
- WIFCONTINUED(status)
若子进程通过SIGCONT恢复,该宏返回true。
如果一个子进程已经终止,并且是一个僵死进程,wait立即返回并取得该子进程的状态,否则wait使其调用者阻塞直到一个子进程终止。如果有多个子进程,只有一个wait,wait只能回收最先终止的进程。
while(wait(NULL)); //可以将子进程全部回收完,再做下面的工作
例.
#include <stdio.h>
#include <stdlib.h> //exit函数要用到
#include <sys/wait.h>
#include <unistd.h>
int main()
{
pid_t pid, wpid;
int i, status;
pid = fork();
if(pid < 0)
{
perror("fork error:");
exit(1);
}
else if(pid == 0)
{
printf("child:%d,parent:%d\n",getpid(),getppid());
sleep(3);
exit(78);
}
else
{
for(i = 0; i < 10; i++)
{
printf("----parent:%d----\n",getpid());
}
wpid = wait(&status);
if(wpid == -1)
{
perror("wait error:");
return 76;
}
if(WIFEXITED(status))
{
printf("child exit with %d\n",WEXITSTATUS(status));
}
if(WIFSIGNALED(status))
{
printf("child kill by %d\n",WTERMSIG(status));
}
}
for(i = 0; i < 10; i++)
{
printf("-----after wait------\n");
}
}
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
#include <errno.h>
#include <sys/wait.h>
int main()
{
pid_t pid;
pid = fork();
int status;
if(pid < 0)
return -1;
else if(0 == pid)
{
printf("child pid is %d\n",getpid());
//while(1);
_exit(0);
}
else
{
printf("parent pid is %d\n",getpid());
wait(&status);
printf("WIFEXITED %d",WIFEXITED(status));
}
return 0;
}
/*
程序结果:
while(1)的话,status那个不会输出,应为被阻塞了
_exit(0)的输出为1
*/
waitpid()
#include <sys/wait.h>
pid_t waitpid (pid_t pid, int * status, int options)
/*
函数功能:
作用同wait,但可指定pid进程清理,可以不阻塞。
参数:
*status:如果不在意结束状态值,则参数status可以设成NULL。
pid:欲等待的子进程识别码:
pid> 0 回收指定ID的子进程
pid=-1 等待任何子进程,相当于wait()。
pid<-1 回收指定进程组为pid绝对值内的任意子进程
pid=0 回收和当前调用waitpid一个组的所有子进程
option:可以为 0 或下面的 OR 组合
0:跟wait一样,子进程没结束就一直阻塞
WNOHANG: 如果没有任何已经结束的子进程则马上返回,不予以等待。
此时返回值为0表示有子进程正在运行
WUNTRACED :如果子进程进入暂停执行情况则马上返回,但结束状态不予以理会。
返回值:
如果执行成功则返回回收的子进程ID,如果有错误发生则返回-1(也就是没有子进程可以回收了)。失败原因存于errno中。
*/
注意:一次wait或waitpid调用只能清理一个子进程,清理多个子进程应使用循环。
waitpid的不阻塞是指运行到此函数如果有进程还没结束,它能继续执行下面的程序,但不代表它还能等这个进程执行完再返回这个函数进行回收。所以不管wait还是waitpid要是想清理所有进程都得使用循环。
进程调度
操作系统的核心就是任务(进程)管理
进程调度器
将有限的CPU资源分配给多个进程
目的:最大化处理器效率,让多个进程同时运行,互不影响
调度机制分类:
- 协同式(非抢占性/时间片轮转):谁先创建谁先执行,按顺序执行。一个进程运行完自己的时间片,主动退出,CPU无权访问。实时性不够。当一个进程出现异常,产生中断,或者需要做紧急的事情的时候,不能优先执行,要等到自己的时间片到来才能。
- 抢占式:实时性好。时间片到了或右更高优先级、调度器抢占CPU进行任务切换。能及时相应一些异常和突发状况。每个进程有优先级,先执行优先级更高的。
linux之前是协同式,之后协同式和抢占式共同工作。
调度器把进程分为两类:
- 处理器消耗型:渴望获取更多的CPU时间,并消耗掉调度器分配的全部时间片。如while死循环,科学计算,影视渲染(很消耗CPU资源)
- I/O消耗型:由于等待某种资源,通常处于阻塞状态,不需要较长的时间片。如父进程做输入的时候,不做输入的时候就会把时间片让出去
调度器发现你是I/O消耗型,就用优先级调度你,你要用的时候把你的优先级调高,处理器消耗型就用协同式,不让别人打断你,让你把自己的时间片消耗掉,再把使用权让给别人。
面试小结
谈谈你对进程的理解
进程是什么,进程如何创建,创建的方法有哪些,进程如何退出,退出的方法,区别。创建过程中产生的问题僵尸孤儿进程,如何解决。多个进程同时运行需要对进程做调度,哪两类调度,调度策略。多个进程之间传输数据做进程通信
进程是操作系统中分配资源的最小单位。
每个进程都有自己独立的虚拟内存空间,能达到互不干扰,并发并行运行
创建进程的方法fork,vfork,exec,system,各种的特点
进程的退出又分为正常退出和异常退出,如何让进程正常退出?
进程有可能产生僵尸进程和孤儿进程,分别产生的原因?引出init进程
解决僵尸进程,通过进程等待,用wait,waitpid等待
—————————————————————————
进程调度分为抢占式和协同式,抢占式的目的是根据优先级使操作系统的策略更具实时性;协同式根据时间片轮转,一个一个分配对等的时间片让它去执行
进程间通信
为什么需要进程间通信?进程有独立的地址空间,每个进程没有交集,没有交集就不能通信。所有进程有个最大的交集的操作系统,也就是内核空间,所以通过内核空间进行.......