Linux进程控制

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调用失败原因
  • 系统中有太多的进程
  • 实际用户的进程数量超过了限制
5)fork的常规用法
  • 一个父进程希望复制自己,使父子进程同时执行不同的代码段。
  • 一个进程要执行一个不同的程序。例如:子进程从fork返回后,调用exe函数。
6)vfork函数
  • 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)

这是两个常数,可以使用“|”把他们连起来使用,eg:waitpid(-1,NULL,WNOHANG|WUNTRACED);
如果不使用设为0就好,(-1,NULL,0);
阻塞与非阻塞理解:假如梦先生在等烧水,阻塞式表示他一直在等,子进程没有退出,父进程就卡住;非阻塞式表示,他可以在等的过程中干别的比如玩玩手机,聊聊天什么,子进程没退出,父进程可退出,不会卡住。
5)获取子进程的status
  • wait和waitpid,都有一个status参数,该参数是一个输出型参数,由操作系统填充。
  • 如果传递NULL,表示不关心子进程的退出状态信息。
  • 否则,操作系统会根据该参数,将子进程的子进程信息反馈给父进程。
  • status不能简单的当做整型来看待,只有它的低16位被用作status,可以当做位图来看待,具体细节如下:
    正常终止次2低8位(8-15)是退出码,0~7是0。
    被信号杀死,status的低7位是退出码。
    Linux进程控制
    文章图片
6)wait使用
#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; }

Linux进程控制
文章图片

非阻塞式
#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; }

Linux进程控制
文章图片

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。
4)命名理解
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个函数有以下的关系。
Linux进程控制
文章图片

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不刷新。
4)return退出 return退出是一种常见的进程退出方法,return m等同于exit(m),因为调用main的运行时函数会把main的返回值当做exit的参数。

    推荐阅读