进程概念
1.程序和进程的区别
程序是保存在硬盘、光盘等介质中的可执行代码和数据,是硬盘(外设)上的一个普通文件。
进程是在CPU及内存中运行的程序代码(就是被加载到内存里的程序),是一个程序的运行中的描述–pcb,通过这个描述操作系统可以实现对一个程序的运行调度管理
2.描述进程—PCB
Linux下的PCB是:task_struct
task_struct内容分类:
- 标示符: 描述本进程的唯一标示符,用来区别其他进程。
- 状态: 不同的状态干不同的事情。任务状态,退出代码,退出信号等。
- 优先级: 决定获取CPU资源的优先权(相对于其他进程的优先级(让程序运行更加良好))。
- 程序计数器: 保存程序在CPU上即将执行的下一个指令的地址
- 内存指针: 指向要调度运行程序代码以及数据在内存中的位置(包括程序代码和进程相关数据的指针,还有和其他进程共享的内存块的指针 )
- 上下文数据: 保存CPU上正在处理的数据
- I/O状态信息: 包括显示的I/O请求,分配给进程的I/O设备和被进程使用的文件列表。
- 记账信息: 可能包括处理器时间总和,使用的时钟数总和,时间限制,记账号等。
3.组织进程
所有运行在系统里的进程都以task_struct链表的形式存在内核里。
4.查看进程
可以通过/proc系统文件查看
同样可以使用top和ps这些用工具来获取
- ps aux以列表的形式显示出进程信息
- ps aux | grep 用于查看某一个进程的信息
- top 查看进程动态信息
进程控制
1.进程创建(fork函数)
【 进程状态(进程状态查看、僵尸进程、孤儿进程)、进程的优先级、环境变量、进程地址空间】
fork函数
是通过系统调用创建进程的(创建一个子进程),其
函数原型是pid_t fork(void)
注意:
fork()有两个返回值,若调用成功,给父进程(原来的进程)返回子进程的pid,给子进程(新建的进程)返回0;若调用失败,则父进程返回-1
- 父子进程代码共享,数据各自开辟空间,私有一份(采用写时拷贝),因为进程运行时具有独立性
#include<stdio.h>
#include<unistd.h>
#include<sys/types.h>
int main()
{
int ret=fork();
if(ret<0)//调用失败,返回-1
{
perror("fork");
return -1;
}
else if(ret==0)//下面是同时执行的
{//child子进程
printf("I am child: %d!,ret:%d\n",getpid(),ret);//子进程返回0
}
else
{//father父进程
printf("I am father: %d!,ret:%d\n",getpid(),ret);//父进程返回子进程的pid
}
sleep(1);
return 0;
}
Linux下运行结果如下:
问题:
1.子进程返回0的原因是:
一个进程只有一个父进程,所以子进程可以调用getppid,从而获得父进程的ID号
- 父进程返回子进程的pid, 子进程返回0就像一个父亲有多个孩子,而一个孩子只有一个父亲
- fork之后,父子进程哪个先运行?
不一定,fork之后,说明父子进程创建成功了,系统里多了进程,但是系统里是有很多进程的,先运行哪个是由操作系统的调度器决定.
-父子进程都是从fork函数之后的代码开始执行
例如
if(fork()) {} else {} printf(“123\n”);
打印结果两个进程都会有123这句
2.fork调用失败的原因:
系统中有太多的进程;实际用户的进程数超过了限制
3.为什么fork之后有两个返回值?
这是因为父进程可能有很多子进程,所以必须通过这个返回的子进程ID来跟子进程,而子进程只有一个父进程,它的ID可以通过getppid取得
fork常规用法
- 一个父进程希望复制自己,使父子进程同时执行不同的代码段。eg:父进程等待客户端请求,生成子进程来处理请求
- 一个进程要执行一个不同的程序。eg:子进程从fork返回后,调用exec函数
写时拷贝
意思就是,通常,父子代码共享,父子再不写入时,数据也是共享的,当任意一方试图写入,便以写时拷贝的方式各自一份副本。这样一来,不仅节省了时间,也节省了资源
2.进程终止
进程终止具体分三种:
1.代码运行完毕,结果正确(正常退出)
2.代码运行完,结果不正确(正常退出)
3 代码异常终止(异常退出)
正常终止可以有不同的退出方式:
1.从main函数中返回
,main函数中的return值是当前进程退出时的退出码,我们可以通过echo $? 查看上一个进程退出时的退出码
2.调用 exit 或 _exit函数
,这两个都是终止进程的,但还有点区别,exit在终止前是要刷新缓冲区的,而 _exit可直接终止进程
3. return退出
:执行return n等同于执行exit(n),因为调用main函数运行时,函数会将main的返回值当做exit的参数
通过下面的代码比较下exit()与_exit():
exit函数:
#include<stdio.h>
#include<unistd.h>
void exit(int ststus);
int main()
{
printf("hello!"); //printf函数有输出缓冲区
sleep(3);//睡眠3秒再打印
exit(0);
}
printf函数有输出缓冲区,因为exit会在终止前刷新缓冲区,从而打印 hello!
运行结果如下:
_exit函数:
#include<stdio.h>
#include<unistd.h>
void _exit(int ststus);
int main()
{
printf("hello!");
sleep(3);
_exit(0);
}
exit函数直接终止,所以运行结果如下:
异常终止:在等待过程中,wait或waitpid方法中的参数status收到了信号
3.进程等待
之前我们了解过僵尸进程,知道了它是什么状态。也知道如果不及时回收,就会导致内存泄漏的危害。所以,只要让子进程退出后,父进程接收子进程的退出信息就可以避免,即父进程通过进程等待的方式,回收子进程的资源,获取子进程的信息。(僵尸进程如果不太清楚,可以点上面链接了解哈)
所以,也为了防止内存泄漏,进程等待就非常有必要的!
父进程获取子进程的信息就要通过进程的退出结果来判断了。
进程等待的方法
在了解方法之前,我们需要知道获取子进程的一个status参数
- wait 和waitpid,都有一个status参数,该参数是一个输出型参数,由操作系统填充
- 如果传递NULL,表示不关心子进程的退出状态信息
wait方法
#include<sys/types.h>
#include<sys/wait.h>
pid_t wait(int* ststus)
模拟实现下:
1 #include<stdio.h>
2 #include<stdlib.h>
3 #include<sys/wait.h>
4 #include<unistd.h>
5 int main()
6 {
7 pid_t id = fork();
8 if(id == 0)
9 {
10 printf("child ...\n");
11 sleep(1);
12 }
13 else
14 {
15 printf("father.....\n");
16 pid_t ret =wait(NULL);//不关心子进程的退出信息,调用wait,等子进程,等待成功,返回所等待子进程的pid
17 if(ret>0)
18 {
19 printf("wait success...: %d",ret);
20 }
21 }
22 }
waitpid方法
pid_t waitpid(pid_t pid, int *status, int options);
返回值:
- 当正常返回时,waitpid返回收集到的子进程的进程ID;
- 如果设置了选项WNOHANG,而调用中waitpid发现没有已退出的子进程可收集,则返回0;
- 如果调用中出错,则返回-1,这时errno会被设置成相应的值以指示错误所在;
参数:
pid:
- pid=-1,等待任一个子进程。与wait等效
- pid>0 等待其进程ID与pid相等的子进程
status:(通过宏提取信息)
- WIFEXITED(status): 若为正常终止子进程返回的状态,则为真。(是通过检测退出码中的特定比特位来查看进程是否正常退出)
- WEXITSTATUS(status): 若WIFEXITED非零,提取子进程退出码。(查看进程的退出码)
options:(等待方式默认是阻塞方式(默认0))
- WNOHANG:(代表非阻塞方式)若pid指定的子进程没有结束,则waitpid()函数返回0,不予以等待。若正常结束,则返回进程的ID。
4.进程程序替换
替换原理
用fork创建子进程后执行的是和父进程相同的程序(但有可能执行不同的代码分支),子进程往往要调用一种exec函数以执行另一个程序。当进程调用exec函数时,该进程的地址空间代码和数据完全被新程序替换,从新程序的启动例程开始执行。调用exec并不创建新进程,所以调用exec前后该进程的id并未改变。
替换函数
这六种替换函数以exec开头,统称exec函数:(执行什么,怎么执行)
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 execp(const char *path,char *const argv[]);
int execve(const char *path,char *const argv[],char *const envp[]);
这些函数如果调用成功则加载新的程序,从启动代码开始执行,不再返回。
调用出错,返回-1(exec函数只有出错的返回值,没有成功的返回值
)
l: 参数列表
v:参数用数组
p:自动搜索环境变量PATH
e:自己维护环境变量
例:
int main()
{
char *const argv[]={"ps","-ef",NULL };
char *const envp[]={"PATH=/bin:/user/bin","TERM= console",NULL};
execl("bin/ps","ps","-ef",NULL);
execlp("ps","ps","-ef",NULL);
execle("ps","ps","-ef",NULL,envp);
execv("bin/ps",argv);
execvp("ps",argv);
execve("ps",argv,envp);
return 0;
}