目录
1.什么是进程
- 进程是程序的一次执行过程。
- 程序:是静态的,它是存储在外存上的可执行二进制文件。
- 进程:动态的概念,它是程序的一次执行过程,包括了进程的创建,调度、消亡,是存在于内存中的。
- 进程是独立的,可以被CPU调度的任务。
- Linux中的调度机制:时间片轮询机制。
- 操作系统会给每一个进程分配时间片,当时间片结束后,cpu资源会切走,当前进程等待下一次调度
- 进程在被调度的时候,系统会分配和释放各种资源。(CPU资源,内存资源,进程调度块(PCB))
2.进程的5态图
- 创建态
- 就绪态
- 运行态
- 阻塞态
- 终止态
进程运行过程中有5种种状态,运行态只是进程运行过程中的一种状态。
3.进程的内存分布(重点!!)
1.内存分布
- 在Linux操作系统中,每个进程都会被分配4G的内存空间。(虚拟内存)
- 其中0-3G是用户空间代码使用,每个进程相互独立
- 其中3-4G是内核空间,所有进程共享。
每个进程的用户空间都有自己独立的区:
1> 静态存储区 2> 堆区 3> 栈区
2.虚拟内存和物理内存
虚拟内存和物理内存之间的关系--->映射关系
- 物理内存:硬件上(内存条上)真正存在的存储空间。
- 虚拟内存:程序启动后,会分配4G的虚拟地址空间,用户只能访问到虚拟地址空间。当要使用的时候,由虚拟地址映射到物理地址上。
- 32位操作系统,指针变量的大小为4bytes,取值范围为[0, 2^32-1],所以寻址范围为4G。
- 64位操作系统,指针变量的大小为8bytes,只取了前48bit存储地址,所以寻址范围是256TB;
- 虚拟内存的目的:动态分配内存。
4.进程是资源分配的最小单位
以进程为单位分配和释放各种资源。
- 内存资源的申请和释放
- 文件描述符表:每个进程都有自己的1024个文件描述符。
- 时间片
- 管理自己的虚拟地址空间,使用的时候映射到物理地址上
5.进程标识
操作系统会给每一个进程分配一个编号,这个编号就是进程号
- 主要进程标识
- 进程号:PID (process id)
- 父进程号:PPID (parent process id)
- 进程组号:PGID (process group id) 若干个进程的集合,称之为进程组,默认情况下新创建的进程会继承父进程的组ID
- 会话组号:SID (session id) 若干个进程组的集合,称之为会话组,默认情况下新创建的进程会继承父进程的会话ID
- 操作系统刚启动的时候,会启动三个进程。三个特殊的进程号:
- 0 idle进程 操作系统引导程序,创建1号,2号进程。
- 1 init进程 初始化内核的各个模块,当内核启动完成后,用于收养孤儿进程(没有父进程的进程)
- 2 kthreadd进程 用于进程间调度。
6.进程相关的shell指令
1.ps -aux
USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND
功能:显示进程占计算机资源的百分比
2.ps -ajx
PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND
父进程 进程号 进程组 会话组
功能:显示进程之间的关系
3.pidof
功能:根据进程名字获取PID号;
格式:
pidof a.out
4.pstree
功能:显示进程关系树
5.kill
kill -9 pid 根据pid号杀死进程
killall -9 进程名字 根据进程名字杀死进程
进程的STAT:
D 不能被中断的阻塞状态
R 运行状态
S 可以被中断的阻塞状态
T 被信号控制的挂起状态
t 进程被调试的挂起状态
X 死亡态,死亡是一瞬间的,该状态永远看不到
Z 僵尸状态,当前进程退出后,父进程没有为其收尸。
< 高优先级的
N 低优先级的
L 有些页被锁进内存
s 会话组组长
l 多线程
+ 运行在前端
进程相关的函数
1.fork
功能:创建一个子进程;
原型:
#include <sys/types.h>
#include <unistd.h>
pid_t fork(void);
参数:
返回值:
成功, >0 , 在父进程中,返回创建的子进程的pid号;
=0, 在子进程中,返回0;
失败,返回-1,更新errno,且没有子进程被创建
注意:
- fork函数创建的子进程,会克隆父进程用户空间的所有资源,以及PC寄存器的值。所以子进程不会运行执行过的fork函数以及fork函数以上的代码。
(ps :PC寄存器中存储着下一次该运行到哪一行代码)
- 父子进程的虚拟地址空间不是同一块,但是由于子进程是从父进程拷贝过来的,所以fork完毕的一瞬间,父子进程的用户空间长得完全一致。
- 父子进程映射的物理地址不是同一块空间。
- 写时拷贝:当父子进程均不修改其中的内容的时候,此时映射的物理地址是同一块空间。若某个进程要修改其中的内容的时候,此时才会真正的申请一块新的物理地址空间给子进程的虚拟地址映射。
- 父进程会拷贝文件描述符表给子进程.
练习
文件IO函数实现,拷贝文件。子进程先拷贝后半部分,父进程再拷贝前半部分。允许使用sleep函数。
#include <stdio.h>
#include <head.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <fcntl.h>
#include <unistd.h>
int main(int argc, const char *argv[])
{
int fd_r = open("./1.png", O_RDONLY);
if(fd_r < 0)
{
ERR_MSG("open");
return -1;
}
int fd_w = open("copy.png", O_WRONLY|O_CREAT|O_TRUNC, 0644);
if(fd_w < 0)
{
ERR_MSG("open");
return -1;
}
//计算文件大小
off_t size = lseek(fd_r, 0, SEEK_END);
pid_t cpid = fork();
if(cpid > 0)
{
sleep(4);
//父进程拷贝前半部分
//将偏移量修改到0
lseek(fd_r, 0, SEEK_SET);
lseek(fd_w, 0, SEEK_SET);
char c = 0;
for(int i=0; i<size/2; i++)
{
read(fd_r, &c, 1);
write(fd_w, &c, 1);
}
printf("前半部分拷贝完毕\n");
}
else if(0 == cpid)
{
//子进程拷贝后半部分
//将偏移量修改到size/2
lseek(fd_r, size/2, SEEK_SET);
lseek(fd_w, size/2, SEEK_SET);
char c = 0;
for(int i=size/2; i<size; i++)
{
read(fd_r, &c, 1);
write(fd_w, &c, 1);
}
printf("后半部分拷贝完毕\n");
}
else
{
ERR_MSG("fork");
return -1;
}
close(fd_r);
close(fd_w);
return 0;
}
2.getpid / getppid
功能:获取进程号 、 获取父进程号;
原型:
#include <sys/types.h>
#include <unistd.h>
pid_t getpid(void);
pid_t getppid(void);
返回值:
获取进程号 、 获取父进程号;
3._exit / exit
1._exit
功能:结束进程,销毁其在内存中的资源,且直接摧毁缓冲区,不会刷新缓冲区!!!
原型:
#include <unistd.h>
void _exit(int status);
参数:
int status:可以传递进程退出状态值给其父进程,父进程可以通过wait/ waitpid函数接收。可以传递任意整型;
2.exit
功能:结束进程,销毁其在内存中的资源,会刷新缓冲区!!!
原型:
#include <stdlib.h>
void exit(int status);
参数:
int status:可以传递进程退出状态值给其父进程,父进程可以通过wait/ waitpid函数接收。可以传递任意整型;
4.wait / waitpid
i. wait
功能:1.阻塞函数,阻塞等待任意子进程退出。2.回收退出的子进程的资源,(回收僵尸进程); 3.接收子进程退出状态
原型:
#include <sys/types.h>
#include <sys/wait.h>
pid_t wait(int *wstatus);
参数:
int *wstatus:接收子进程传递回来的退出状态值,若不想接收,则填NULL;
返回值:
>0, 成功返回退出的子进程的PID号;
=-1,函数运行失败,更新errno;
若没有子进程,则wait函数运行失败;
父进程只能回收子进程,无法回收孙子进程。
若子进程退出,父进程没有回收子进程的资源,此时子进程会变成僵尸进程。
任务:若没有子进程,问wait函数还阻塞吗,wait函数是否能运行成功?
答:没有子进程,则wait函数运行失败;
ii. 子进程退出状态
wait(int* wstatus)中,int* wstatus指向的int类型参数,只有[8bit, 15bit]用于存储子进程传递的退出状态值。范围为[0, 255]。
所以子进程只能传递256种状态。
1)从wstatus中提取子进程退出状态值
int wstatus = -1;
pid_t wpid = wait(&wstatus);
printf("wpid = %d wstatus=%d\n", wpid, wstatus>>8);
WEXITSTATUS(wstatus): 提取wstauts中子进程传递的退出状态; ---》(((status) & 0xff00) >> 8)
2)判断子进程是否正常退出
WIFEXITED(wstatus)
若正常退出,则返回真。
正常退出: exit _exit 主函数调用return退出。
iii. waitpid
功能:阻塞等待指定子进程退出;
原型:
#include <sys/types.h>
#include <sys/wait.h>
pid_t waitpid(pid_t pid, int *wstatus, int options);
参数:
pid_t pid:
< -1 阻塞等待指定进程组下的任意一个子进程退出;
-1 阻塞等待当前进程下的任意一个子进程退出; 与wait函数的功能基本一致;
0 阻塞等待当前进程组下的任意一个子进程退出;
> 0 阻塞等待指定的子进程退出;子进程的pid号 == pid参数;
int *wstatus:接收子进程传递回来的退出状态值,若不想接收,则填NULL;
int options:
0:阻塞方式运行,当指定的子进程没有退出的时候,该函数阻塞,直到指定子进程退出,解除阻塞;
WNOHANG:非阻塞方式运行,当指定的子进程没有退出,该函数不阻塞,立即返回;
返回值:
成功,>0, 成功回收到的子进程的pid号;
=0, 函数运行成功,但是此时子进程没有退出。
函数运行失败,返回-1,更新errno;
1. 若指定的子进程不存在(没有子进程)的时候,函数运行失败;
2. 子进程无法回收父进程的资源,
3. 同级之间无法相互回收资源。
Linux中的特殊进程
1.孤儿进程
父进程退出,子进程不退出,此时子进程被1号(init)进程收养,变成孤儿进程。
孤儿进程会脱离终端控制,且运行在后端,不能用ctrl+c杀死后端进程,但是可以被kill -9杀死
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
int main(int argc, const char *argv[])
{
//父进程退出 子进程不退出
pid_t cpid = fork();
if(cpid > 0)
{
}
else if(0 == cpid)
{
while(1)
{
printf("this is child %d %d\n", getppid(), getpid());
sleep(1);
}
}
else
{
perror("fork");
return -1;
}
return 0;
}
2.僵尸进程
子进程退出,父进程不退出去,且父进程没有给子进程收尸,此时子进程就变成僵尸进程。
注意:
- 僵尸进程只能被回收,不能被杀死。
- 僵尸进程有危害:占用进程号,占用部分内存空间,占用物理空间,占用进程调度块(PCB)等等...
- 回收僵尸进程的方式:
- 退出父进程后,子进程的资源由内核自动回收。
- wait / waitpid函数回收。缺点:阻塞函数,父进程无法做自己的事情。非阻塞形式,有可能收不到。
- 结合信号的方式回收僵尸进程:当子进程退出后,通知父进程收尸。
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
int main(int argc, const char *argv[])
{
//子进程退出,父进程不退出
pid_t cpid = fork();
if(0 == cpid)
{
}
else if(cpid > 0)
{
while(1)
{
printf("this is parent %d %d\n", getpid(), cpid);
sleep(1);
}
}
else
{
perror("fork");
return -1;
}
return 0;
}
3.守护进程
又称之为幽灵进程。
- 守护进程脱离于终端,且运行在后端。
- 守护进程在执行过程中不会将信息显示在任何终端上,避免影响前端任务执行。且不会被任何终端产生的终端信息所打断。
- 守护进程目的:需要周期性执行某个任务或者周期性等待处理某些事情的时候,为了避免影响前端执行或者被前端信息打断的时候,可以使用守护进程。
守护进程的创建:
- 创建孤儿进程:所有工作都在子进程中执行,从形式上脱离终端控制。
- fork(), 退出父进程
- 创建新的会话组:使子进程完全独立出来,防止兄弟进程对其有影响
- setsid() 函数
功能:创建一个新的进程组和会话组,成为该进程组和会话组组长;
原型:
#include <sys/types.h>
#include <unistd.h>
pid_t setsid(void);
3.修改当前孤儿进程的运行目录为不可卸载的文件系统:例如根目录,/tmp
防止运行目录被删除后,导致进程崩溃
chdir函数
功能:修改运行目录;
#include <unistd.h>
int chdir(const char *path);
chdir("/");
注意:从当前位置往后,运行在指定的目录下;
- 重设文件权限掩码:umask(0), 一般清零;
- 关闭所有文件描述符,从父进程继承过来的文件描述符不会用到,浪费资源。
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
int main(int argc, const char *argv[])
{
//创建孤儿进程
pid_t cpid = fork();
if(0 == cpid)
{
//创建新的会话
pid_t sid = setsid();
printf("sid = %d\n", sid);
//修改运行目录为不可卸载的文件目录下
chdir("/");
//清空文件权限掩码
umask(0);
//关闭所有文件描述符
for(int i = 0; i<getdtablesize(); i++)
close(i);
while(1)
{
//守护进程运行的周期性代码
sleep(1);
}
}
return 0;
}