一、进程
1、基本概念
狭义定义:进程是正在运行的程序的实例(an instance of a computer program that is being executed)。
广义定义:进程是一个具有一定独立功能的程序关于某个数据集合的一次运行活动。它是操作系统动态执行的基本单元,在传统的操作系统中,进程既是基本的分配单元,也是基本的执行单元。
2、进程–PCB
(1)每个进程在内核中都有一个进程控制块(PCB)来维护进程相关的信息,其作用是使一个在多道程序环境下不能独立运行的程序成为一个能独立运行的基本单位或其他进程并发执行的进程。
(2)PCB是系统感知进程存在的唯一标识。
(3)Linux内核的进程控制块是task_struct结构体。task_struct是Linux内核的一种数据结构。它会被装载到RAM里并且包含着进程的信息。它定义在linux-2.6.38.8、include/linux/sched.h文件中。task_steuct结构体包含了以下内容:
标⽰符: 描述本进程的唯⼀一标示符,⽤用来区别其他进程。
状态: 任务状态,退出代码,退出信号等。
优先级: 相对于其他进程的优先级。
程序计数器: 程序中即将被执⾏行的下⼀一条指令的地址。
内存指针: 包括程序代码和进程相关数据的指针,还有和其他进程共享的内存块的指针
上下⽂文数据: 进程执⾏行时处理器的寄存器中的数据[休学例⼦子,要加图CPU,寄存器]。
I/O状态信息: 包括显⽰示的I/O请求,分配给进程的I/O设备和被进程使⽤用的⽂文件列表。
记账信息: 可能包括处理器时间总和,使⽤用的时钟数总和,时间限制,记账号等。
其他信息
二、进程的创建
1、fork函数
在Linux中fork函数是非常重要的函数,它从已存在进程中创建一个新的进程;新创建的进程被称为子进程,原来的进程就是父进程。
返回值:子进程中返回0,父进程返回子进程id,出错返回-1;
当进程调用fork,控制块转移到内核中的fork代码后,内核需要做以下工作:
(1)分配新的内存块和内核数据结构给⼦子进程
(2)将⽗父进程部分数据结构内容拷⻉贝⾄至⼦子进程
(3)添加⼦子进程到系统进程列表当中
(4)fork返回,开始调度器调度
fork函数—创建进程实例代码:
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
int main(void)
{
pid_t pid;
printf("Before:pid is %d\n",getpid());//创建子进程之前的进程id
if((pid=fork())==-1)
{
perror("fork()");
exit(EXIT_FAILURE);
}
//创建成功之后子进程和父进程id
printf("After:pid is %d,fork return %d\n",getpid(),pid);
sleep(1);
return 0;
}
运行结果:
fork之前,父进程独立执行;fork之后,父子执行流进程分别执行。
2、vfork函数
查看函数原型:
函数解释:
(1)vfork函数用于创建一个子进程,而子进程和父进程共享地址空间,fork的子进程具有独立的地址空间
(2)vfork保证子进程先运行,在它调用exec或(exit)之后父进程才可能被调度运行
vfork函数–使用程序
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
int glob=100;
int main()
{
pid_t pid;
if((pid=vfork())==-1)
{
perror("fork");
exit(EXIT_FAILURE);
}
if(pid==0)
{//child
sleep(5);
glob=200;
printf("child glob %d\n",glob);
exit(0);
}
else
{
//parent
printf("parent glob %d\n",glob);
}
return 0;
}
运行结果:
通过运行结果可以发现子进程直接改了父进程的值,这是因为子进程在父进程的地址空间内运行。
三、进程终止
1、进程终止场景
(1)代码运行完毕,结果正确
(2)代码运行完毕,结果不正确
(3)代码异常终止
2、进程常见退出方法
(1)正常终止
从main函数返回
调用exit
_exit
(2)异常退出:
Ctrl+c,信号终止
3、exit函数
#include<unistd.h>
void exit(int status);
exit函数最后也会调用exit,但在调用exit之前,还做了其他工作:
(1)执行用户通过atexit或on_exit定义的清理函数
(2)关闭所有打开的流,所有的缓存数据均被写入
(3)调用_exit
4、_exit函数
函数原型:
#include<unistd.h>
void _exit(int status);
**参数:**status 定义了进程的终止状态,父进程通过wait来获取该值
虽然status是int,但是仅有低8位可以被⽗父进程所⽤用。所以_exit(-1)时,在终端执⾏行$?发现返回值是255。
5、实例
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
int main()
{
printf("hello\n");
//return 0;
_exit(0);
}
return n 退出相当于执行exit(n),因为调用main的运行时函数会将main返回值当做exit的参数。
四、进程等待
1、为什么需要进程等待?
(1)子进程退出,父进程如果不管不顾,就可能造成‘僵尸进程’的问题,进⽽而造成内存泄漏。
(2)另外,进程一旦变成僵尸状态,那就⼑枪不⼊入,“杀⼈不眨眼”的kill -9 也无能为⼒力,因为谁也没有办法杀死一个已经死去的进程。
(3)最后,父进程派给子进程的任务完成的如何,我们需要知道。如,子进程运行完成,结果对还是不对,或者是否正常退出。
(4)父进程通过进程等待的⽅式,回收子进程资源,获取子进程退出信息
2、进程等待的方法
(1)wait函数
函数原型:
#include<sys/types.h>
#include<syd/wait.h>
pid_t wait(int *status);
参数:输出型参数,获取子进程退出状态,不关心则可以设置为NULL
返回值:成功返回被等待进程pid,失败返回-1。
(2)waitpid函数
函数原型:
pid_t waitpid(pid_t pid,int *status,int options)
参数:
pid:
Pid=-1,等待任⼀一个⼦子进程。与wait等效。
Pid>0.等待其进程ID与pid相等的⼦子进程。
status:
WIFEXITED(status): 若为正常终⽌止⼦子进程返回的状态,则为真。(查看进程是否是正常退出)
WEXITSTATUS(status): 若WIFEXITED⾮非零,提取⼦子进程退出码。(查看进程的退出码)
options:
WNOHANG: 若pid指定的⼦子进程没有结束,则waitpid()函数返回0,不予以等待。若正常结束,则
返回该⼦子进程的ID。
返回值:
当正常返回的时候waitpid返回收集到的⼦子进程的进程ID;
如果设置了选项WNOHANG,⽽而调⽤用中waitpid发现没有已退出的⼦子进程可收集,则返回0;
如果调⽤用中出错,则返回-1,这时errno会被设置成相应的值以指⽰示错误所在;
(3)具体代码实现:
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
int main()
{
pid_t pid;
if((pid==fork())==-1)
perror("fork()"),exit(1);
if(pid==0)
{
sleep(20);
exit(10);
}
else
{
int stu;
int ret=wait(&stu);
if(ret>0&&(stu&0x7F)==0)
{
printf("child exit code;%d\n",(stu>>8)&0xFF);
}
else if(ret>0)
{
printf("signal code: %d\n",stu&0x7F);
}
}
}
运行结果:
五、进程替换
1、exec函数族
exec函数族提供了一个在进程中启动另一个程序执行的方法。它可以根据指定的文件名或目录名找到可执行文件,并用它来取代原调用进程的数据段、代码段和堆栈段,执行完之后,原调用进程的内容除了进程另外,其他全部被新的进程替换了。另外,这里的可执行文件即可以是二进制文件,也可以是Linux下任何可执行的脚本文件。
在Linux中使用exec函数族组要有两种情况:
(1)当进程认为自己不能再为系统和用户做出任何贡献时,就可以调用exec函数族中的任意一个函数让自己重生。
(2)如果一个进程想执行另一个程序,那么它就可以调用fork()函数新建一个进程,然后调用exec函数族中的任意一个函数,这样看起来就像通过执行应用程序而产生了一个新进程(这种情况非常普遍)。
2、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[]);
函数说明:
(1)这些函数如果调⽤用成功则加载新的程序从启动代码开始执⾏行,不再返回。
(2)如果调⽤用出错则返回-1
(3)所以exec函数只有出错的返回值,⽽而没有成功的返回值。
其函数差别如下:
3、应用举例
用例代码如下:
#includ<stdio.h>
#include<unistd.h>
#include<stdlib.h>
int main()
{
printf("father pid is %d\n",getpid());
int ret=0;
if((ret=execlp("ps","ps","-ef",NULL))<0)
printf("execlp error\n");
printf("child pid is %d.ret=%d\n",getpid(),ret);
return 0;
}
从这里我们也能看出程序中执行execlp(“ps”,”ps”,”-ef”,NULL);语句,其实就是执行ps -ef命令。同时我们也能看出新进程取代了原有进程,新进程的进程PID为原进程的进程PID:3352。同样我们不能看到printf(“child process pid is %d,ret = %d\n”,getpid(),ret);的打印信息,因为原进程的内容被新进程完全去掉了,即不往下继续执行了。这里execlp()函数参数列表中为什么最后一个要为NULL,是因为从第二个ps开始表示命令参数,而这些参数最后需要用NULL作为结尾标识符。
那么我们该如何让原进程继续往下执行呢?此时我们就需要调用fork()函数创建一个子进程,在子进程中代用execlp()函数来执行其他程序。修改tihuan.c文件,修改后内容如下:
#includ<stdio.h>
#include<unistd.h>
#include<stdlib.h>
int main()
{
printf("father pid is %d\n",getpid());
int ret=0;
if(0==fork())
{
if((ret=execlp("ps","ps","-ef",NULL))<0)
printf("execlp error\n");
}
printf("father pid is %d.ret=%d\n",getpid(),ret);
return 0;
}
运行结果:
从这里可以看出原进程(父进程)没有被”ps -ef“进程取代,同样这里可以看出父进程先执行完。