·在Linux中描述进程的结构体叫做task_struct.
·task_struct是Linux内核的一种数据结构,它会被装载到RAM(内存)里包含着进程的信息
3.task_struct内容分类·标示符:描述本进程的唯一标示符,用来区别其他进程
·状态:任务状态,退出代码,退出信号
·优先级:相对于其他进程的优先级
·程序计数器:程序中即将被执行的下一条指令的地址
·内存指针:包括程序代码和进程相关的指针,还有和其他进程共享的指针
·上下文数据:进程执行时处理器的寄存器中的数据
·I/O状态信息:包括显示的I/O请求,分配给进程的I/O设备和被进程使用的文件列表
·记账信息:可能包括处理器时间总和,使用的时钟数总和,时间限制,记账号等
给张图方便理解:
二、进程创建(fork&vfork)在Linux系统内,创建子进程的方法是使用系统调用fork()函数。fork()函数是Linux系统内一个非常重要的函数,它与我们之前学过的函数有一个显著的区别:fork()函数调用一次却会得到两个返回值。
fork()函数所需头文件:#include
#include
函数原型: pid_t fork()
函数返回值:
== 0 子进程
>0 父进程,返回值为创建出的子进程的PID
<0 出错
fork()函数用于从一个已经存在的进程内创建一个新的进程,新的进程称为“子进程”,相应地称创建子进程的进程为“父进程”。使用fork()函数得到的子进程是父进程的复制品,子进程完全复制了父进程的资源,包括进程上下文、代码区、数据区、堆区、栈区、内存信息、打开文件的文件描述符、信号处理函数、进程优先级、进程组号、当前工作目录、根目录、资源限制和控制终端等信息,而子进程与父进程的区别有进程号、资源使用情况和计时器等。
练习:创建一个子进程,打印父子进程的pid
#include
#include
#include
int main()
{
printf("linux66\n");
pid_t ret = fork();
if(ret < 0)
{
perror("失败");
return 0;
}
else if(ret == 0 )
{
//child
printf("i am child :[%d] - [%d] \n", getpid(),getppid());
}
else
{
printf("i am father :[%d] - [%d] \n", getpid(),getppid());
}
return 0 ;
}
在使用fork()函数创建子进程的时候,要有一个概念:在调用fork()函数前是一个进程在执行这段代码,而调用fork()函数后就变成了两个进程在执行这段代码。两个进程所执行的代码完全相同,都会执行接下来的if-else判断语句块。
当子进程从父进程内复制后,父进程与子进程内都有一个"pid"变量:在父进程中,fork()函数会将子进程的PID返回给父进程,即父进程的pid变量内存储的是一个大于0的整数;而在子进程中,fork()函数会返回0,即子进程的pid变量内存储的是0;如果创建进程出现错误,则会返回-1,不会创建子进程。
fork()函数一般不会返回错误,若fork()函数返回错误,则可能是当前系统内进程已经达到上限,或者内存不足。
注意:父子进程的运行先后顺序是完全随机的(抢占式执行),也就是说在使用fork()函数的默认情况下,无法控制父进程在子进程前进行还是子进程在父进程前进行。
vfork()函数
所需头文件:#include
#include
函数原型:pid_t vfork()
返回值:
== 0 子进程
>0 父进程,返回值为创建出的子进程的PID
<0 出错
vfork()函数功能与fork()函数功能类似不过更加彻底:内核不再给子进程创建虚拟空间,直接让子进程共享父进程的虚拟空间。当父子进程中有更改相应段的行为发生时,再为子进程相应的段创建虚拟空间并分配物理空间。在vfork()函数创建子进程后父进程会阻塞,保证子进程先行运行。
vfork()函数创建的子进程会与父进程(在调用exec函数族函数或exit()函数前)共用地址空间,此时子进程如果使用变量则会直接修改父进程的变量值。因此,vfork()函数创建的子进程可能会对父进程产生干扰。另外,如果子进程未调用exec函数族函数或exit()函数,则父子进程会出现死锁现象。
fork()函数与vfork()函数的主要区别如下:
1.vfork()函数保证子进程先行运行,在子进程调度exec函数族函数或者exit()函数后父进程才会被调度运行。如果子进程需要依赖父进程的进一步动作,则会产生死锁
2.fork()函数需要拷贝父进程的进程环境,而vfork()函数则不需要完全拷贝父进程的进程环境,在子进程调用exec函数族函数或者exit()函数之前,子进程与父进程共享进程环境(此时子进程相当于线程),父进程阻塞等待。
三、进程状态&环境变量
1、进程状态:
R:运行状态
S:睡眠状态
D:磁盘睡眠状态
T:暂停状态
Z:僵尸状态(僵尸进程)
孤儿进程:
孤儿进程不是进程状态,而是一种进程的种类名称
孤儿进程产生原因:是由于父进程先于子进程退出,导致子进程需要将自己的退出状态信息返回给 f进程。
这个进程通常是操作系统当中的1号进程,也称之为Init程。Init程本身就会创建许多进程,本身Init程创建的子进程我们不能称他们为孤儿进程
2、环境变量
·环境变量是为了让我们的当前的操作系统执行的更加愉快
·常见的环境变量:
PATH:查看可执行程序的环境变量
HOME :保存是当前用户的家目录的环境变量
SHELL :保存当前所使用的shell的环境变量
LD_LIBRARY_PATH:程序运行时,库文件的搜索路径的环境变量
LIBRARY_PATH :程序编译的时候,库文件的搜索路径的环境变量
·常见的命令:
echo $[环境变量名称]:查看特定的环境变零值
export(在命令行中修改,只在当前bash中生效)
export[环境变量名称]=$[环境变量名称]:[新增的环境变量]
3.环境变量的组织方式
4.使用代码获取环境变量
1.从main函数的参数中获取
#include
int main(int argc, char* argv[], char* env[])
{
int i = 0;
for(; i < argc; i++)
{
printf("argv[%d] = [%s]\n", i, argv[i]);
}
for(i = 0; env[i]; i++)
{
printf("env[%d] = [%s]\n", i, env[i]);
}
return 0;
}
argc:命令行参数的个数
argv[0]:命令行参数
env[0]:当前用户的环境变量
2.从environ中获取
#include
#include
int main()
{
extern char** environ;
for(int i = 0; environ[i]; i++)
{
printf("environ[%d] = [%s]\n", i, environ[i]);
}
return 0;
}
3.使用getenv函数获取环境变量
#include
#include
int main()
{
char* path = getenv("PATH");
printf("path = [%s]\n", path);
return 0;
}
四、进程虚拟地址空间
写时拷贝:当父进程定义了一个全局变量,fork出来一个子进程,当自己成发生对该全局变量发生修改的时候,操作系统会重新开辟一段空间来保存子进程更改后的值,即子进程会更改自己的页表映射关系
五、进程终止(exit&_exit)
当我们需要结束一个进程的时候,我们可以使用exit()函数或_exit()函数来终止该进程。当程序运行到exit()函数或_exit()函数时,进程会无条件停止剩下的所有操作,并进行清理工作,最终将进程停止。
exit()函数
所需头文件:#include
函数原型: void exit(int status)
函数参数:status表示让进程结束时的状态(会由主进程的wait();负责接收这个返回值【也可以不接收】-->类似函数的返回值),默认使用0表示正常结
_exit()函数
所需头文件:#include
函数原型:void _exit(int status)
函数参数: status表示让进程结束时的状态(会由主进程的wait();负责接收这个返回值【也可以不接收】-->类似函数的返回值),默认使用0表示正常结
exit()函数与_exit()函数用法的区别的:
_exit()函数直接使进程停止运行,当调用_exit()函数时,内核会清除该进程的内存空间,并清除其在内核中的各种数据。
exit()函数则在_exit()函数的基础上进行了升级,在退出进程之间增加了若干工序。exit()函数在终止进程之前会检测进程打开了哪些文件,并将缓冲区内容写回文件。
exit()函数与_exit()函数最主要的区别就在于是否会将缓冲区数据保留并写回。_exit()函数不会保留缓冲区数据,直接将缓冲区数据丢弃,直接终止进程运行;而exit()函数会将缓冲区内数据写回,待缓冲区清空后再终止进程运行。
练习:exit与_exit
#include
#include
//使用exit()函数终止进程
int main()
{
printf("This is the content in buffer");
exit(0);
}
//使用_exit()函数终止进程
#include
#include
int main()
{
printf("This is the content in buffer");
_exit(0);
}
练习1会输出"This is the content in buffer",而练习2不会输出
六、进程等待(wait&waitpid)
目的:进程等待就是为了防止僵尸进程的产生
使用wait()函数与waitpid()函数让父进程回收子进程的系统资源,两个函数的功能大致类似,waitpid()函数的功能要比wait()函数的功能更多。
wait()函数
所需头文件:#include
函数原型: pid_t wait(int *status)
函数参数: status 保存子进程结束时的状态(由exit();返回的值)。使用地址传递,父进程获得该变量。若无需获得状态,则参数设置为NULL
返回值: 成功:已回收的子进程的PID
失败:-1
waitpid()函数
所需头文件: #include
函数原型: pid_t waitpid(pid_t pid, int *status, int options)
函数参数:
1. pid pid是一个整数,具体的数值含义为:
pid>0 回收PID等于参数pid的子进程
pid==-1 回收任何一个子进程。此时同wait()
pid==0 回收其组ID等于调用进程的组ID的任一子进程
pid<-1 回收其组ID等于pid的绝对值的任一子进程
2. status 保存子进程结束时的状态(由exit();返回的值)。使用地址传递,父进程获得该变量。若无需获得状态,则参数设置为NULL
3. options
0:同wait(),此时父进程会阻塞等待子进程退出
WNOHANG:若指定的进程未结束,则立即返回0(不会等待子进程结束)
返回值:
>0 已经结束运行的子进程号
0 使用WNOHANG选项且子进程未退出
-1 错误
wait与waitpid的区别:
当进程结束时,该进程会向它的父进程报告。wait()函数用于使父进程阻塞,直到父进程接收到一个它的子进程已经结束的信号为止。如果该进程没有子进程或所有子进程都已结束,则wait()函数会立即返回-1。
waitpid()函数的功能与wait()函数一样,不过waitpid()函数有若干选项,所以功能也比wait()函数更加强大。实际上,wait()函数只是waitpid()函数的一个特例而已,Linux内核总是调用waitpid()函数完成相应的功能。
阻塞与非阻塞:
阻塞:进程或线程执行到这函数时必须等待某个事件发生,如果事件没有发生,进程或线程就被阻塞,函数不能立刻返回
非阻塞:进程或线程执行函数时不必非要等待事件的发生,一旦执行肯定返回,以返回值的不同来反映函数的执行情况。如果事情发生则与阻塞的方式相同,若事情没有发生,则返回一个值,告知事情未发生,而进程或线程继续执行,所以效率较高
练习1:使用wait()函数,让父进程在子进程结束后再运行
#include
#include
#include
int main()
{
pid_t pid = fork();
if(pid==-1)
{
perror("can not fork");
return -1;
}
else if(pid==0)//子进程
{
printf("This is Child process\n");
printf("Child process ID is %d\n",getpid());
printf("Child process will exit\n");
}
else//父进程
{
pid = wait(NULL);//等待子进程结束
printf("This is Parent process\n");
printf("Child process %d is over\n",pid);
}
return 0;
}
练习2:使用waitpid()函数,让父进程回收子进程。参数使用WNOHANG使父进程不会阻塞,若子进程暂时未退出,则父进程在1s后再次尝试回收子进程
#include
#include
#include
#include
int main()
{
pid_t pid;
pid = fork();
if(pid<0)
{
perror("cannot fork");
return -1;
}
else if(pid==0)//子进程
{
printf("This is Child process\n");
sleep(5);//模拟子进程运行5s
exit(0);//子进程正常退出
}
else//父进程
{
int ret;
do//循环直至子进程退出为止
{
ret = waitpid(pid,NULL,WNOHANG);//回收子进程,使用WNOHANG选项参数
if(ret==0)
{
printf("The Child process is running, can't be exited\n");
sleep(1);//1秒后再次尝试
}
}while(ret==0);
if(pid==ret)//如果检测到子进程退出
{
printf("Child process exited\n");
}
else
{
printf("Some error occured\n");
}
}
return 0;
}
七、进程程序替换(exec函数族)
所需头文件:#include
函数原型:
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[])
execl(完整的路径名,列表……);
execlp(文件名,列表……);
execle(完整的路径,列表……,环境变量的向量表)
execv(完整的路径名,向量表);
execvp(文件名,向量表);
execve(完整的路径,向量表,环境变量的向量表)
函数参数:
path:文件路径,使用该参数需要提供完整的文件路径
file:文件名,使用该参数无需提供完整的文件路径,终端会自动根据$PATH的值查找文件路径
arg:以逐个列举方式传递参数
argv:以指针数组方式传递参数
envp:环境变量数组
返回值: -1(通常情况下无返回值,当函数调用出错才有返回值-1)
这6个函数的函数功能类似,但是在使用语法规则上有细微区别。我们可以看出,其实exec函数族的函数都是exec+后缀来命名的,具体的区别如下:
区别1:参数传递方式(函数名含有l还是v)
exec函数族的函数传参方式有两种:逐个列举或指针数组。
若函数名内含有字母'l'(表示单词list),则表示该函数是以逐个列举的方式传参,每个成员使用逗号分隔,其类型为const char *arg,成员参数列表使用NULL结尾
若函数名内含有字母'v'(表示单词vector),则表示该函数是以指针数组的方式传参,其类型为char *const argv[],命令参数列表使用NULL结尾
区别2:查找可执行文件方式(函数名是否有p)
我们可以看到这几个函数的形参有些为path,而有些为file。其中:
若函数名内没有字母'p',则形参为path,表示我们在调用该函数时需要提供可执行程序的完整路径信息
若函数名内含有字母'p',则形参为file,表示我们在调用该函数时只需给出文件名,系统会自动按照环境变量$PATH的内容来寻找可执行程序
区别3:是否指定环境变量(函数名是否有e)
exec可以使用默认的环境变量,也可以给函数传入具体的环境变量。其中:
若函数名内没有字母'e',则使用系统当前环境变量
若函数名内含有字母'e'(表示单词environment),则可以通过形参envp[]传入当前进程使用的环境变量
exec函数族简单命名规则如下:
l 接收以逗号为分隔的参数列表,列表以NULL作为结束标志
v 接收一个以NULL结尾的字符串数组的指针
p 提供文件的完整的路径信息 或 通过$PATH查找文件
e 使用系统当前环境变量 或 通过envp[]传递新的环境变量
这6个exec函数族的函数,execve()函数属于系统调用函数,其余5个函数属于库函数。
练习1:使用execl()函数
#include
#include
#include
int main()
{
pid_t pid = fork();
if(pid < 0 )
{
perror("cannot fork");
return 0;
}
else if(pid==0)//子进程
{
printf("i am child \n");
if(execl("/bin/ps","ps","-ef",NULL)<0)//子进程执行ps -ef,注意参数的写法,且需要使用NULL结尾
{
perror("cannot exec ps");
}
}
}
else//父进程
{
sleep(1);//父进程延时1s,让子进程先运行
printf("i am father \n");
}
return 0;
}
在调用exec函数族的函数时,一定要加上错误判断语句。当exec函数族函数执行失败时,返回值为-1,并且报告给内核错误码,我们可以通过perror将这个错误码的对应错误信息输出。常见的exec函数族函数执行失败的原因有:
1.找不到文件或路径
2.参数列表arg、数组argv和环境变量数组列表envp未使用NULL指定结尾
3.该文件没有可执行权限
练习2:使用execlp()
#include
#include
#include
int main()
{
pid_t pid;
pid = fork();
if(pid==-1)
{
perror("cannot fork");
return -1;
}
else if(pid==0)//子进程
{
printf("This is Child process\n");
if(execlp("ps","ps","-ef",NULL)<0)//第一个参数只需要写ps即可,系统会根据环境变量自行寻找ps程序的位置
{
perror("cannot exec ps");
}
}
else//父进程
{
printf("This is Parent process\n");
sleep(1);//父进程延时1s,让子进程先运行
}
return 0;
}
练习3:使用execvp()函数
#include
#include
#include
int main()
{
pid_t pid;
char *arg[]={"ps","-ef",NULL};//设定参数向量表,注意使用NULL结尾
pid = fork();
if(pid==-1)
{
perror("cannot fork");
return -1;
}
else if(pid==0)//子进程
{
printf("This is Child process\n");
if(execvp("ps",arg)<0)//注意该函数的参数与execlp()函数的区别
{
perror("cannot exec ps");
}
}
else//父进程
{
printf("This is Parent process\n");
sleep(1);//父进程延时1s,让子进程先运行
}
return 0;
}
练习4:使用execle()函数将一个新的环境变量添加到子进程中,并使用env命令查看
#include
#include
#include
int main()
{
pid_t pid;
char *envp[]={"PATH=/tmp","USER=liyuge",NULL};//设定新的环境变量,注意使用NULL结尾
pid = fork();
if(pid==-1)
{
perror("cannot fork");
return -1;
}
else if(pid==0)//子进程
{
printf("This is Child process\n");
if(execle("/usr/bin/env","env",NULL,envp)<0)
{
perror("cannot exec env");
}
}
else//父进程
{
printf("This is Parent process\n");
sleep(1);//父进程延时1s,让子进程先运行
}
return 0;
}
练习5:使用execve()函数
#include
#include
#include
int main()
{
pid_t pid;
char *arg[]={"env",NULL};//设定参数向量表,注意使用NULL结尾
char *envp[]={"PATH=/tmp","USER=liyuge",NULL};//设定新的环境变量,注意使用NULL结尾
pid = fork();
if(pid==-1)
{
perror("cannot fork");
return -1;
}
else if(pid==0)//子进程
{
printf("This is Child process\n");
if(execve("/usr/bin/env",arg,envp)<0)
{
perror("cannot exec env");
}
}
else//父进程
{
printf("This is Parent process\n");
sleep(1);//父进程延时1s,让子进程先运行
}
return 0;
}
练习6:使用execl函数族函数,在子进程内执行自己编译的可执行程序a.out文件
#include
#include
#include
int main()
{
pid_t pid;
pid = fork();
if(pid==-1)
{
perror("cannot fork");
return -1;
}
else if(pid==0)//子进程
{
printf("This is Child process\n");
if(execl("/home/linux/a.out","./a.out",NULL)<0)//使用execl()函数
{
perror("cannot exec a.out");
}
}
else//父进程
{
printf("This is Parent process\n");
sleep(1);//父进程延时1s,让子进程先运行
}
return 0;
}
作者:海的早晨