1.进程创建
1)fork函数
fork函数从已经存在的进程中创建一个新进程,新进程为子进程,而原进程为父进程。
进程调用fork,当控制转移到内核中的fork代码后内核做如下的事情
- 分配新的内存块和内核数据结构给子进程
- 将父进程部分数据结构内容拷贝到子进程
- 添加子进程到系统进程列表中
- fork返回,开始调度器调度
fork有两个返回值,一个接受值(一父多子,一子一父)。
子进程返回0。
父进程返回子进程的PID。
失败返回-1。
父子进程代码共享,数据各自开辟空间,私有一份(采用写时拷贝)。
fork之后通常用if进行分流,父子进程运行具有独立性,由调度算法决定。
#include
#include
#include
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 parent : %d,ret : %d\n",getpid(),ret);
}
sleep(1);
return 0;
}
2)调度算法了解
1.时间片轮转调度算法(RR) : 给每个进程固定的执行时间,根据进程到达的先后顺序让进程在单位时间片内执行,执行完成后便调度下一个进程执行,时间片轮转调度不考虑进程等待时间和执行时间,属于抢占式调度。优点是兼顾长短作业;缺点是平均等待时间较长,上下文切换较费时。适用于分时系统。
先来先服务调度算法(FCFS) :根据进程到达的先后顺序执行进程,不考虑等待时间和执行时间,会产生饥饿现象。属于非抢占式调度,优点是公平,实现简单;缺点是不利于短作业。
优先级调度算法 (HPF):在进程等待队列中选择优先级最高的来执行。
多级反馈队列调度算法 :将时间片轮转与优先级调度相结合,把进程按优先级分成不同的队列,先按优先级调度,优先级相同的,按时间片轮转。优点是兼顾长短作业,有较好的响应时间,可行性强,适用于各种作业环境。
高响应比优先调度算法 :根据“响应比=(进程执行时间+进程等待时间)/ 进程执行时间”这个公式得到的响应比来进行调度。高响应比优先算法在等待时间相同的情况下,作业执行的时间越短,响应比越高,满足段任务优先,同时响应比会随着等待时间增加而变大,优先级会提高,能够避免饥饿现象。优点是兼顾长短作业,缺点是计算响应比开销大,适用于批处理系统。
注:上述算法来自网络搜索
3)fork的写时拷贝
通常父子代码共享,父子不再写入时,数据也是共享的,当任意一方试图写入,便以写时拷贝方式各自一份副本。
写时拷贝考虑因素
- 内存资源
- 性能,更合理的使用空间
4)fork调用失败原因
- 系统中有太多的进程
- 实际用户的进程数量超过了限制
- 一个父进程希望复制自己,使父子进程同时执行不同的代码段。
- 一个进程要执行一个不同的程序。例如:子进程从fork返回后,调用exe函数。
- vfork也是用于创建子进程。而vfork之后的父子进程共享地址空间,也就是说他们除了PCB不一样,别的都一样,fork的子进程则具有独立的地址空间。
- vfork的子进程保证让子进程先运行,在调用exec后父进程才可能被调度。
#include
#include
#include
#includeint num=30;
int main()
{
pid_t id=vfork();
if(id<0){
perror("use fork");
exit(1);
}
else if(id==0){//child
sleep(4);
num=80;
printf("child:num=%d\n",num);
exit(0);
}
else{//parent
printf("parent:num=%d\n",num);
}
return 0;
}
运行结果是:先等待4秒,然后先执行子进程,而子进程对数据修改为80,,父进程也是80,子进程直接改变了父进程的变量值,因为子进程在父进程的地址空间运行。
2.进程等待 1)进程等待的必要性
1.子进程退出,父进程如果不管不顾,就可能造成“僵尸状态”,进而造成内存泄漏
2.进程一旦变成僵尸状态,就刀匠不如,kill -9也无能为力
3.父进程派给子进程的任务完成的如何,我们需要知道,如:子进程运行完成,结果对还是不对,或者是否正常退出
4.父进程通过进程等待的方式,回收子进程,获取子进程退出信息。
2)进程等待的方法
1>wait
#include
#includepid_t wait(int status);
返回值:
成功返回被等待进程pid,失败返回-1.
参数:
输出型参数,获取子进程退出状态,不关心则可以设置成为NULL
进程调用wait后会阻塞自己,分析是否存在僵尸子进程,找到就销毁并返回,没找到就一直阻塞。wait只要有一个退出,父进程知道子进程id.
2>waitpid 【Linux进程控制】函数
#include
#includepid_t waitpid(pid_t pid,int*status,intoptions);
d
waitpid的返回值
当正常返回的时候waitpid返回收集到的子进程的进程ID;
如果设置了选项WNOHANG,而调用中waitpid发现没有已经退出的子进程可收集,则返回0;
如果调用中出错,则返回-1,这时error值会被设置成相应的值以指示错误所在。
参数
1>pid
pid>0,等待其进程ID与pid相等的子进程,只要等待的子进程还没有结束,waitpid就会一直等下去。
pid=-1,等待任一个子进程,与wait等效。
pid=0,等待同一个进程组中的任何子进程,如果子进程已经加入到别的进程waitpid便不会再对它进行处理。
pid<-1,等待一个指定进程组中的任何子进程,这个进程组的id等于pid的绝对值。
2>status
1.WIFEXITED(status) 判断子进程是否为正常退出的,如果是,它会返回一个非零值
2.WEXITSTATUS(status) 当WIFEXITED返回非零值时,我们可以用这个宏 来提取子进程的退出码,如果子进程调用exit(25)退出,WEXITSTATUS(status)就会返回25。如果进程不是正常退出的,WIFEXITED返回0,这个值就无意义。
3>options
WNOHANG(非阻塞 1):若pid指定的子进程没有结束,则waitpid()函数返回0,不予以等待,若正常结束,则返回该子进程的ID。
- 如果子进程已经退出,调用wait/waitpid时,wait/waitpid会立刻返回,并且释放资源,获得子进程退出信息。
- 如果在任意时刻调用wait/waitpid,子进程存在且正常运行,则进程可能阻塞。
- 如果不存在该子进程,则立刻出错返回。
WUNTRACED(阻塞 0)
如果不使用设为0就好,(-1,NULL,0);
阻塞与非阻塞理解:假如梦先生在等烧水,阻塞式表示他一直在等,子进程没有退出,父进程就卡住;非阻塞式表示,他可以在等的过程中干别的比如玩玩手机,聊聊天什么,子进程没退出,父进程可退出,不会卡住。
5)获取子进程的status
- wait和waitpid,都有一个status参数,该参数是一个输出型参数,由操作系统填充。
- 如果传递NULL,表示不关心子进程的退出状态信息。
- 否则,操作系统会根据该参数,将子进程的子进程信息反馈给父进程。
- status不能简单的当做整型来看待,只有它的低16位被用作status,可以当做位图来看待,具体细节如下:
正常终止次2低8位(8-15)是退出码,0~7是0。
被信号杀死,status的低7位是退出码。
文章图片
#include
#include
#include
#include
#include
#includeint main(){
pid_t pid=fork();
if(pid==-1){
perror("use fork");
exit(1);
}
else if(pid==0){//child
sleep(5);
exit(10);
}
else{//parent
int st;
//status
int ret=wait(&st);
if(ret>0&&(st&0X7F)==0)
printf("child exit code is [%d]\n",(st>>8)&0XFF);
else
printf("sig code is[%d]\n",st&0X7F);
}
}
开启两个终端,一个执行后,打开另一个终端,杀死子进程,再执行一次。
7)waitpid的使用
阻塞式
#include
#include
#include
#include
int main(){
pid_t id=fork();
if(id<0){
perror("use fork");
exit(1);
}
else if(id==0){//child
printf("child is run,pid is [%d]\n",getpid());
sleep(5);
exit(37);
}
else{//parent
int st;
//status
pid_t ret=waitpid(-1,&st,0);
//0代表阻塞式
printf("this is test for waitpid\n");
if(WIFEXITED(st)&&ret==id)//WIFEXITED判断是否正常返回
printf("wait child 5s success,child return code is [%d]\n",WEXITSTATUS(st));
//WEXITSTATUS宏获取退出码
else{
printf("wait child failed,return.\n");
return 1;
}
}
return 0;
}
文章图片
非阻塞式
#include
#include
#include
#includeint main(){
pid_t id=fork();
if(id<0){
perror("use fork");
exit(1);
}
else if(id==0){//child
printf("child is run,pid is [%d]\n",getpid());
sleep(5);
exit(1);
}
else{//parent
int st;
//status
pid_t ret=0;
do{
ret=waitpid(-1,&st,WNOHANG);
//WNOHANG是1,代表非阻塞式等待
if(ret==0){
printf("child is running\n");
}
sleep(1);
}while(ret==0);
if(WIFEXITED(st)&&ret==id)
printf("wait child 5s success,child return code is [%d]\n",WEXITSTATUS(st));
else
printf("wait child failed,return.\n");
return 1;
}
return 0;
}
文章图片
3.进程的程序替换 1)基本概念
用fork创建子进程后执行的是和父进程相同的程序,当然可以执行不同的分支(如果我们用fork创建一个子进程之后让子进程做和父进程同样的事,那么这个子进程没有任何意义)。
所以在fork之后,我们应该调用exec函数用来替换子进程的程序和数据,让子进程执行和父进程不同的程序。当进程调用exec函数时,该进程的用户空间得到代码和数据完全被新的程序替换。
调用exec并不创建新的进程,所以调用exec前后的进程的id并未改变。
2)替换函数
int execl(const char *path, const char *arg, ...);
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 *file, char *const argv[],char *const envp[]);
3)函数解释
这些函数如果调用成功则加载新的程序从启动端开始执行,不再返回。
(成功无返回值)
exec函数只有出错的返回值而没有成功的返回值。
如果出错,会设置erron值并返回-1.常见的错误有:
- 找不到文件或者路径,erron值被设置为ENOENT。
-数组argv或envp网际用NULL结束,此时errno被设置为EFAULT。 - 没有对要执行文件的运行权限,此时errno被设置为EACCES。
1.l(list):表示参数采用列表例如:execl("/bin/ls",“ls”,"-a",“NULL”)
2.v(vecctor):参数用数组
例如:
char* myargv[]={“ls”,"-a,“NULL”};
execv(“bin/ls”,myargv);
3.p(path):有p自动搜索环境变量PATH
4.e(env):表示自己维护环境变量
5)使用
char * const argv[ ] ={ "ps","-ef",NULL);
char * const envp[ ]={"PATH=/bin:/usr/bin","TERM=console",NULL);
execl("/bin/ps","ps","-ef",NULL);
//带P的,可以使用环境变量PATH,无需写全路径
execlp("ps","ps","-ef",NULL");
//带e的自己组装环境变量
execle("ps","ps","-ef",NULL,envp);
execv("/bin/ps",argv);
//带P的,可以使用环境变量PATH,无需写全路径
execvp("ps",argv);
//带e的自己组装环境变量
execve("/bin/ps",argv,envp);
注:这6个函数只有execve是真正的系统调用,其他5个函数都是在execve上包装的,它们6个函数有以下的关系。
文章图片
4.进程终止 1)进程退出场景
1.代码运行完毕,结果正确
2.代码运行完毕,结果不正确
3.代码异常终止
2)进程常见退出方法
a.正常退出 1.main函数返回
2.调用exit,
3._exit
b.异常退出 1.ctrl+c ,信号终止
2kill -9 PID ,给进程发送9号信号
3)_exit与exit函数
a._exit函数
#include
void _exit(int status);
参数status定义了进程的终止状态,父进程通过wait获取该值(0正常返回,1异常返回),虽然status是int,但是仅有低8位可以被父进程所用,所以_exit(-1)时,在终端执行$?发现返回值为255.
b.exit函数
#include
voidexit(int status);
- exit与_exit函数的区别:exit刷新缓存区,_exit不刷新。