您的位置:首页 > 其它

进程的基本控制

2017-12-10 23:19 281 查看
前言:整个操作系统都在围绕进程这一概念具体展开,所以对于进程的控制就显得十分重要,这篇文章主要讲述以下几点:

1. 进程创建

2. 进程退出

3. 进程等待

4. 进程程序替换

进程创建

在操作系统中,对于父子进程的概念非常重要,必要linux自带的bash,对于你在命令行输入的一些指令,它是不会自己去处理你这些请求的,而是通过创建子进程去处理,它只需要知道子进程返回的消息就好了。

为什么要基于这样的父子进程关系呢?试想一下,我们和操作系统打交道是通过shell内建的bash,如果用户的什么请求都经由bash去亲自执行,那么一个bash也不够用啊,其次,如果一旦请求中出了问题,那么bash挂掉的话,谁来帮我们向操作系统传达我们的请求呢?

基于上面提出的种种问题,就引出进程创建子进程的必要性了。

进程的创建方式:

pid_t fork(void);

pid_t vfork(void);

认识fork函数

pid_t fork(void)
返回值:pid_t其实就是一个整型,typedef成pid_t只是为了一眼看上去知道这是一个进程号
子进程中返回值为0.
父进程中返回操作系统给子进程分配的pid号。
fork失败返回-1


进程调用fork,当控制权限转移到内核中的fork代码后:

分配新的内存块和数据结构给子进程

将父进程的大多数数据结构拷贝至子进程

添加子进程到系统进程列表

fork返回,操作系统进行进程调度

当一个进程fork出一个子进程后,就有两个二进制代码相同的进程,并且运行到相同的地方,但每个进程都将开始执行自己的代码。

如下:

int main()
{
printf("Before fork: pid is:%d\n",getpid())//getpid函数为获取进程pid
pid_t id = fork();
if(id < 0)
{
perror("fork error");
exit(1);
}
printf("After fork:pid is %d\n",getpid());

return 0;
}




这里需要注意的是,先执行子进程还是父进程完全取决于操作系统的进程调度器决定。

fork失败的原因:

系统中的进程数达到了上限

系统的内存不足

系统不支持,如Windows不支持fork

认识vfork函数

对于vfork来说,其他的都是fork函数用法一样,只要记住最重要的两个特性就好。

子进程一定先于父进程执行。

子进程调用exec或者exit之后父进程才能执行

进程终止

进程退出的场景:

代码运行完,结果正确

代码运行完,结果不正确

代码异常终止

常见进程退出:

正常终止:

1. main函数返回

2. 调用exit函数

3. 调用_exit函数

exit函数和_exit函数的区别:

exit会进行清理工作,如刷新缓冲区等,而exit直接退出程序

_exit是系统调用,exit最终也会调用_exit。

异常终止

CTRL+C/kill -9

具体的程序退出部分,可见博客尾部的链接。

进程等待

进程等待是非常重要的,如果父进程对子进程不管不顾的话,那么可能会产生僵尸进程,从而造成内存泄漏。

并且作为父进程,创建子进程是让它完成一些任务的,总要知道它返回的结果,完成的怎么样。

wait函数

pid_t wait(int *status)//阻塞式等待
返回值:成功返回等待进程的pid,失败返回-1
参数:输出型参数,获取子进程的退出状态,不关心可以为NULL,该参数由操作系统初始化


status



所以查看的话,先查看低七位是否为0 ,如果是0代表程序正常退出,可以查看高八位具体的退出码。

如果低七为不为0,则代表信号终止,高八位就没有意义了,可以查看低七位的具体信号。

core dump是指进程终止时所记录的现场。

以下面的代码为实例:

#include<stdio.h>
#include<wait.h>

int main()
{
pid_t id = fork();
if(id > 0)
{
//father
int status = 0;
int ret  = wait(&status);
if(ret > 0 && (status&0x7f) == 0)
{
//success
printf("child exit code:%d\n",(status>>8));
}
else
{
//signal exit
printf("signal code:%d\n",(status>>8)&0xff);
}
}
else if(id == 0)
{
//child
sleep(3);
exit(5);//子进程的退出码
}
else
{
perror("fork");
}
return 0;
}


当子进程正常退出时,会返回退出码。

运行结果:



接下来,我们直接kill -9掉该进程,结果应该返回9号信号,看如下运行结果:



我的Ubuntu是最新的,本应该返回9,但是操作系统将9这个数字,对应成第九个信号的名称显示出来。

下面是Linux下的信号:



waitpid函数

pid_t waitpid(pid_t pid,int *status,int option)//如果最后一个参数设置了WNOHANG就是非阻塞式等待


返回值:

正常返回收集子进程的进程ID

如果设置了选项WNOHANG,而调用waitpid发现没有已退出的子进程可以回收,则返回0,就是轮询等待的意思

如果调用中出错,返回-1,errno会被设置成相应的值以指示错误所在。

参数:

pid:

pid = -1,等待任意一个子进程,与wait等效

pid > 0,等待其进程ID与pid相等的进程

status:

WIFEXITED(status):若为正常终止子进程返回的状态,则为真(相当于上面的检查低7位为是否为0)

WEXITSTATUS(status):若WIFIXITED非零,提取子进程退出码(相当于上面的查看高8位的退出码)

option:

WNOHANG:若pid指定的子进程没有结束,则waitpid()函数返回0,不予以等待,若正常结束,则返回该进程的ID

需要注意的是:

如果子进程已经结束,调用wait/waitpid时,函数会直接返回,并且释放资源,获得子进程退出信息。(对应的场景就是子进程退出时,父进程在沉睡,如果这时父进程不予以处理子进程则会产生僵尸进程)

如果在任意时刻调用wait/waitpid,子进程存在且正常运行,则进程可能阻塞

如果不存在该子进程,则立即出错返回

如下代码示例:

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

int main()
{
pid_t id = fork();
if(id > 0)
{
//father
int status = 0;
pid_t ret = 0;
do
{
ret = waitpid(-1,&status,WNOHANG);//no-blocking
if(ret == 0)
{
printf("child is running\n");
}
sleep(1);
}
while(ret == 0);

if(WIFEXITED(status) && ret == id)
{
printf("wait child 3s success,child return code is:%d\n",WEXITSTATUS(status));
}
else
{
printf("wait child failed,return\n");
return 1;
}

}
else if(id == 0)
{
//child
printf("child is run ,pid is:%d\n",getpid());
sleep(3);
exit(1);
}
else
{
printf("%s fork error\n",__FUNCTION__);
return 1;
}
return 0;
}


进程程序替换

替换原理

用fork创建子进程后执行的是和父进程相同的程序(但有可能执行不同的代码分支),子进程一般要调用一种exec函数以执行另一个程序。当进程调用exec函数族时,该进程的用户空间代码和数据完全被新的程序替换,从新程序的启动例程开始执行,调用exec并不创建新进程,所以调用exec前后该进程的pid并未改变。

exec函数族

#include<unistd.h>

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 *path,char *const argv[],char *const envp[]);


函数解释

l(list):表示参数采用列表

v(vector):表示参数采用数组

p(path):带p的函数会自动搜索环境变量PATH

e(env):表示自己维护环境变量

需要特别注意的时,

如果exec函数族调用成功,则从新程序的启动代码开始,所以没有返回值

如果调用失败,则返回-1

代码示例:

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

int main()
{
const *const argv[] = {"ls","-al",NULL};
char *const envp[] = {"PATH=/bin:/usr/bin",NULL};//环境变量
execl("/bin/ls","ls","-al".NULL);

//带p的函数,不必再给出全路径
execlp("ls","ls","-al",NULL);

//带e的,需要自己配置环境变量
execle("ls","ls","-al".NULL,envp);

execv("/bin/ls",argv);

//带p的,不需要给出全路径
execvp("ls",argv);

//带e的,需要自己配置环境变量
execve("/bin/ls",argv,envp);

exit(0);
}




虽然exec函数族有六个函数,但是只有execve函数是系统调用,其他几个函数最终都会调用execve函数。

基于程序替换,可以实现一个简单的shell。

exit函数详解http://blog.csdn.net/qq_36528114/article/details/71321390

实现一个简单的shell:http://blog.csdn.net/qq_36528114/article/details/72582588
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息