一、多进程引入
1.1 引入目的
程序员写程序时,一个程序可能由多个任务组成,如果使用的是单进程,或单任务,那么当该任务执行阻塞时,其他任务就无法执行,必须等到该任务解除阻塞后,才能去执行其他任务。
多进程或多线程,可以解决同一个程序中多个任务并发执行的情况
1.2 进程的概念
1> 进程是程序的一次执行过程
2> 进程是程序资源分配的基本单位,系统会给每个进程分配4G的虚拟内存,分为0--3G的用户空间和3--4G的内核空间
多个进程共享内核空间,用户空间相互独立
3> 进程是一个动态的过程,有生命周期的概念,分为创建态、就绪态、运行态、阻塞态、死亡态
4> 进程在内核空间中存储在一个名为task_struct的结构体中(PCB)
进程描述符:task_struct包含了描述一个进程所需的所有信息。
进程状态:包括运行、就绪、阻塞等状态。
进程标识符:如进程ID(PID)。
进程调度信息:如优先级、调度策略等。
内存管理信息:如虚拟地址空间、页表等。
文件系统信息:如打开的文件、文件系统根目录等。
信号处理:包括待处理信号和信号处理函数。
进程间通信:如消息队列、共享内存等IPC机制相关信息。
时间和定时器:如进程创建时间、CPU使用时间等。
线程信息:在Linux中,线程被视为轻量级进程,也用task_struct表示。
5> 单核cpu处理多任务是,一般使用的是时间片轮询机制
6> 进程与程序的区别:程序是静态的,是存储在磁盘上的二进制代码
进程是动态的,是有生命周期的
7> 进程的组成:进程控制块(PCB)、数据段、程序段
1.3 进程的种类
进程一共分为三大类:交互进程、批处理进程、守护进程
1> 交互进程:他是由shell控制,用于直接跟用户进行交互的进程。例如:vim编辑器、文本编辑器
2> 批处理进程:本质上维护了一个队列,被放入队列中的进程会统一被调度。例如gcc编译器的一步到位的编译
3> 守护进程:脱了了终端而存在的进程,随着系统的启动而开始,随着系统的结束而终止。例如:服务进程
1.4 进程号的概念
每个进程在系统中都有一个唯一的标识位,用一个整数表示,这就是该进程的进程号(PID)
1> PID(process ID):当前进程的进程号
2> PPID(parent process ID):当前进程的父进程的进程号
每个进程都是由其父进程进行拷贝赋值出来的。
可以进入 /proc目录中的每个数字都是现在正在执行的一个进程
1.5 特殊的进程
1> 0号进程:也成为 idel 进程,他是操作系统启动后执行的第一个进程,这个进程也叫空闲进程,当没有其他进程执行时,系统会默认执行该进程。1号进程和2号进程都是由0号进程创建出来的。
2> 1号进程:也称init进程,该进程由0号进程产生,主要完成系统创建时一些软件硬件的初始化工作。当其他进程的父进程死亡后,会托管其子进程
3> 2号进程:也称kthreadd,该进程由0号进程产生,也成为调度进程,当某个就绪进程时间片轮到时,该进程负责进程的调度工作
4> 孤儿进程:当前进程的父进程死亡后,但是当前进程还没有结束,那么当前进程称为孤儿进程,孤儿进程会由1号进程收养
5> 僵尸进程:当前进程已经死亡,但是其父进程没有为其收尸,那么该进程为僵尸进程
1.6 进程的相关指令
1> 查看进程信息的命令:
ps ,跟不同的选项,执行不同的状态
ps -ef:显示进程之间的关系
UID PID PPID C STIME TTY TIME CMD
root 1 0 0 七月25 ? 00:00:20 /sbin/init splash
root 2 0 0 七月25 ? 00:00:00 [kthreadd]
root 3 2 0 七月25 ? 00:00:00 [rcu_gp]
root 4 2 0 七月25 ? 00:00:00 [rcu_par_gp]
root 6 2 0 七月25 ? 00:00:00 [kworker/0:0H-kb]
root 9 2 0 七月25 ? 00:00:00 [mm_percpu_wq]
root 10 2 0 七月25 ? 00:00:06 [ksoftirqd/0]
root 11 2 0 七月25 ? 00:01:11 [rcu_sched]
UID:用户ID
PID:当前进程的id号
PPID:当前进程的父进程PID号
C:不用管
STIME、TIME:创建时间
TTY:如果该值为问号,说明不依赖于终端而存在的进程
CMD:进程名称
ps -ajx:可以显示进程的状态
PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND
0 1 1 1 ? -1 Ss 0 0:20 /sbin/init splash
0 2 0 0 ? -1 S 0 0:00 [kthreadd]
2 3 0 0 ? -1 I< 0 0:00 [rcu_gp]
2 4 0 0 ? -1 I< 0 0:00 [rcu_par_gp]
2 6 0 0 ? -1 I< 0 0:00 [kworker/0:0H-kb]
PGID:组id号
SID:会话组id
STAT:当前进程的状态
ps -aux:可以查看进程资源使用情况
USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND
root 1 0.0 0.2 160076 4676 ? Ss 七月25 0:20 /sbin/init splash
root 2 0.0 0.0 0 0 ? S 七月25 0:00 [kthreadd]
root 3 0.0 0.0 0 0 ? I< 七月25 0:00 [rcu_gp]
root 4 0.0 0.0 0 0 ? I< 七月25 0:00 [rcu_par_gp]
root 6 0.0 0.0 0 0 ? I< 七月25 0:00 [kworker/0:0H-kb]
%CPU:cpu的占用率
%MEM :内存的占用率
2> top或htop
可以动态展示进程的占用情况
top - 11:51:17 up 5 days, 13:55, 1 user, load average: 0.01, 0.02, 0.00
任务: 327 total, 3 running, 260 sleeping, 0 stopped, 0 zombie
%Cpu(s): 3.5 us, 1.8 sy, 0.0 ni, 94.7 id, 0.0 wa, 0.0 hi, 0.0 si, 0.0 st
KiB Mem : 2005964 total, 67820 free, 1168168 used, 769976 buff/cache
KiB Swap: 969960 total, 88160 free, 881800 used. 654624 avail Mem
进程 USER PR NI VIRT RES SHR � %CPU %MEM TIME+ COMMAND
1872 ubuntu 20 0 640144 59872 5056 S 4.0 3.0 12:54.43 Xorg
2057 ubuntu 20 0 3057424 149072 31884 S 3.0 7.4 27:58.69 gnome-shell
89961 ubuntu 20 0 949248 69268 23020 S 2.7 3.5 3:17.13 terminator
2193 ubuntu 20 0 809364 5568 2120 S 0.3 0.3 2:22.89 gsd-color
111256 ubuntu 20 0 51380 4456 3616 R 0.3 0.2 0:00.21 top
1 root 20 0 160076 4676 2652 S 0.0 0.2 0:20.90 systemd
2 root 20 0 0 0 0 S 0.0 0.0 0:00.02 kthreadd
3> pstree:展示进程树,可以显示进程的父子关系
4> 查看给定进程的进程号:pidof 进程名
4> kill : 向进程发送信号,发信号的格式
kill -信号名(号) 进程号
能够发送的信号号有:可以通过指令 kill -l查看
1) SIGHUP 2) SIGINT 3) SIGQUIT 4) SIGILL 5) SIGTRAP
6) SIGABRT 7) SIGBUS 8) SIGFPE 9) SIGKILL 10) SIGUSR1
11) SIGSEGV 12) SIGUSR2 13) SIGPIPE 14) SIGALRM 15) SIGTERM
16) SIGSTKFLT 17) SIGCHLD 18) SIGCONT 19) SIGSTOP 20) SIGTSTP
21) SIGTTIN 22) SIGTTOU 23) SIGURG 24) SIGXCPU 25) SIGXFSZ
26) SIGVTALRM 27) SIGPROF 28) SIGWINCH 29) SIGIO 30) SIGPWR
31) SIGSYS 34) SIGRTMIN 35) SIGRTMIN+1 36) SIGRTMIN+2 37) SIGRTMIN+3
38) SIGRTMIN+4 39) SIGRTMIN+5 40) SIGRTMIN+6 41) SIGRTMIN+7 42) SIGRTMIN+8
43) SIGRTMIN+9 44) SIGRTMIN+10 45) SIGRTMIN+11 46) SIGRTMIN+12 47) SIGRTMIN+13
48) SIGRTMIN+14 49) SIGRTMIN+15 50) SIGRTMAX-14 51) SIGRTMAX-13 52) SIGRTMAX-12
53) SIGRTMAX-11 54) SIGRTMAX-10 55) SIGRTMAX-9 56) SIGRTMAX-8 57) SIGRTMAX-7
58) SIGRTMAX-6 59) SIGRTMAX-5 60) SIGRTMAX-4 61) SIGRTMAX-3 62) SIGRTMAX-2
63) SIGRTMAX-1 64) SIGRTMAX
1、能够发射的信号一共有62个
2、其中前32个属于稳定信号
3、SIGHUP:当进程所在的终端关闭时,该终端会向运行在该终端上的所有进程发送该信号,让其结束进程
4、SIGINT:当终端键入 ctrl+c时,终端向指定进程发送的就是该信号,表示中断进程
5、SIGQUIT:当终端键入 ctrl + \ 时,表示终端要终止进程的结束
6、SIGILL:当进程自己发生非法操作时,内核空间会发送该信号
7、SIGKILL:表示杀死进程
8、SIGUSR1、SIGUSR2:没有特殊的含义,留给程序员使用
9、SIGSEGV:表示指针访问越界时的段错误
10、SIGPIPE:当操作管道文件时,如果管道的读端被关闭,写端继续写的话,就会出现管道破裂
11、SIGALRM:当启动一个定时器,并且定时器超时时,会发送该信号
12、SIGCHLD:当一个进程的子进程退出时,会向该进程的父进程发送该信号,表示让其为子进程收尸
13、SIGCONT:该信号表示让暂停的进程继续执行
14、SIGSTOP、SIGTSTP:表示让一个进程暂停,当用户从键盘上键入 ctrl +z时,就会发送该信号
1.7 进程的状态
1> 主要状态一共有五个:创建态、就绪态、运行态、阻塞态、死亡态
2> 程序中的进程的状态显示:可以通过指令 man ps查看进程的状态
进程的状态由两部分组成:主状态和附加态
主状态:
D 不可中断的休眠态 (usually IO)
R 运行态 (on run queue)
S 可中断的休眠态 (waiting for an event to complete)
T 暂停态,会给出作业号进行控制
t 程序调试时的暂停态
W 已经弃用
X 死亡态 (should never be seen)
Z 僵尸态
附加态:
< 高优先级的进程 (not nice to other users)
N 低优先级的进程 (nice to other users)
L 锁到内存中的进程 (for real-time and custom IO)
s 会话组组长,默认为当前终端
l 包含多线程的进程 (using CLONE_THREAD, like NPTL pthreads do)
+ 表示是前台运行的进程
3> 进程状态切换的实例
二、多进程编程
2.1 fork进程的创建函数
1> 在进程的创建过程,其实就是通过拷贝父进程的task_struct结构体而来的,子进程中保存了大部分的父进程的遗传基因,只需要修改少部分的内容,如子进程的进程号,子进程的父进程号。。。
2> 子进程和父进程的资源用户空间是完全独立的,创建出子进程后,父子进程的资源相互独立,互不影响
3> 进程的创建,通过fork函数完成
4> 当子进程创建后,会跟父进程一起执行fork后面的语句
#include <sys/types.h>
#include <unistd.h>
pid_t fork(void);
功能:拷贝父进程得到子进程
参数:无
返回值:成功时,父进程中返回子进程的pid号,子进程中返回0,失败返回-1并置位错误码
例1:先不关注返回值
注意:创建出的子进程跟父进程没有先后执行的顺序,遵循时间片轮询机制
例2:关注返回值
#include<myhead.h>
int main(int argc, const char *argv[])
{
pid_t pid = -1; //定义变量存储创建的进程的id号
pid = fork(); //创建一个子进程
//对pid进行判断
if(pid > 0)
{
while(1)
{
printf("我是父进程\n");
sleep(1);
}
}else if(pid == 0)
{
while(1)
{
printf("我是子进程\n");
sleep(1);
}
}
sleep(1);
return 0;
}
2.2 进程号的获取
#include <sys/types.h>
#include <unistd.h>
pid_t getpid(void);
功能:获取当前进程的进程号
参数:无
返回值:当前进程的pid
pid_t getppid(void);
功能:获取当前进程的父进程的pid号
参数:无
返回值:当前进程父进程的pid
2.3 进程的退出(exit、_exit)
#include <stdlib.h>
void exit(int status);
功能:退出当前进程,并刷新当前进程打开的标准IO文件指针的缓冲区
参数:退出时的状态
EXIT_SUCCESS:成功退出
EXIT_FAILURE:失败退出
返回值:无
#include <unistd.h>
void _exit(int status);
功能:退出当前进程,但是不刷新标准IO的缓冲区
参数:退出时的状态
EXIT_SUCCESS:成功退出
EXIT_FAILURE:失败退出
返回值:无
2.4 进程资源回收(wait、waitpid)
#include <sys/types.h>
#include <sys/wait.h>
pid_t wait(int *wstatus);
功能:阻塞回收子进程的资源,如果没有进程退出,那么就阻塞等待
参数:子进程退出时的状态,一般不接收,直接填NULL即可
返回值:成功返回回收的子进程的pid,失败返回-1并置位错误码
pid_t waitpid(pid_t pid, int *wstatus, int options);
功能:既可以阻塞也可以非阻塞形式回收僵尸进程
参数1:要回收的进程号
>0:表示回收具体的某个进程
=0:能够回收当前进程所在的进程组中的任意一个子进程
=-1:回收任意一个子进程
<-1: 回收别的进程组(进程组id为给定的pid的绝对值)中的任意一个子进程
参数2:子进程退出时的状态,一般不接收,直接填NULL即可
参数3:表示是否阻塞回收僵尸进程
0:表示阻塞
WNOHANG:表示非阻塞
返回值:
>0: 如果成功回收了子进程资源,那么会返回该子进程的pid
=0: 表示以非阻塞的形式回收资源,但是没有回收到子进程
=-1:失败返回-1并置位错误码
#include<myhead.h>
int main(int argc, const char *argv[])
{
//定义进程号
pid_t pid = fork(); //创建一个子进程
//判断父子进程
if(pid > 0)
{
printf("我是父进程:pid = %d, 当前进程的id为:%d,当前进程的父进程的id为:%d\n",\
pid, getpid(), getppid());
}else if(pid == 0)
{
printf("我是子进程:pid = %d,当前进程的pid为:%d, 当前进程的父进程pid为:%d\n",\
pid, getpid(), getppid());
sleep(3); //休眠3秒
//退出子进程
//exit(EXIT_SUCCESS); //exit(0);
_exit(EXIT_SUCCESS); //exit(0);
}else
{
perror("fork error");
return -1;
}
//回收子进程资源
//wait(NULL);
//waitpid(-1, NULL, 0); //阻塞回收
sleep(5);
waitpid(-1, NULL, WNOHANG); //非阻塞回收
printf("回收子进程资源结束\n");
while(1);
return 0;
}
2.5 创建三个进程
可以让父进程创建一个子进程,再由父进程或者子进程创建一个子进程
#include<myhead.h>
int main(int argc, const char *argv[])
{
//定义变量接受进程号
pid_t pid = fork(); //创建出两个进程
if(pid > 0)
{
//这是父进程
pid_t pid1 = fork();
if(pid1 > 0)
{
printf("我是父进程\n");
}else if(pid1 == 0)
{
printf("我是小儿子\n");
//退出进程
sleep(8);
printf("你们太溺爱我了\n");
exit(EXIT_SUCCESS);
}else
{
perror("fork error");
return -1;
}
}else if(pid == 0)
{
printf("我是大儿子\n");
//退出进程
sleep(5);
printf("老大走伤悲\n");
exit(EXIT_SUCCESS);
}else
{
perror("fork error");
return -1;
}
//阻塞回收子进程资源
wait(NULL);
wait(NULL);
printf("活着\n");
sleep(3);
return 0;
}
2.6 写时拷贝技术
2.7 特殊进程的验证
1> 僵尸进程验证
子进程已经退出,但是父进程没有为其收尸
#include<myhead.h>
int main(int argc, const char *argv[])
{
pid_t pid = fork(); //创建出子进程
if(pid < 0)
{
perror("fork error");
return -1;
}else if(pid == 0)
{
printf("我是子进程\n");
exit(EXIT_SUCCESS); //退出子进程
}
//父进程内容
printf("我是父进程\n");
sleep(5);
return 0;
}
2> 孤儿进程验证
#include<myhead.h>
int main(int argc, const char *argv[])
{
pid_t pid = fork(); //创建出子进程
if(pid < 0)
{
perror("fork error");
return -1;
}else if(pid > 0)
{
printf("我是父进程\n");
exit(EXIT_SUCCESS); //退出子进程
}
//父进程内容
printf("我是子进程\n");
sleep(5);
return 0;
}
3> 创建守护进程
守护进程相当于一个服务,不依赖于任何终端而存在,随着系统的启动而启动,终止而结束
守护进程制作流程:
1、创建子进程,让其成为孤儿进程:fork exit
2、将自己设置成会话组组长,不依赖终端:setsid
#include <unistd.h>
pid_t setsid(void);
功能:设置当前进程的id号为会话组组长
参数:无
返回值:新的会话组id号,失败返回-1并置位错误码
3、更改操作操作目录为根目录: chdir
#include <unistd.h>
int chdir(const char *path);
功能:设置当前进程操作的目录
参数:要操作的目录
返回值:成功返回0,失败返回-1并置位错误码
4、设置文件操作权限:umask
5、将标准输入、输出、出错文件描述符重定向到指定文件中:dup2
#include<myhead.h>
int main(int argc, const char *argv[])
{
//1、产生孤儿进程
pid_t pid = fork();
if(pid < 0)
{
perror("fork error");
return -1;
}else if(pid > 0)
{
//父进程,直接退出
exit(EXIT_SUCCESS);
}
//2、将自己设置成会话组组长
setsid();
//3、更改操作目录为根目录
chdir("/");
//4、获得创建文件的最大权限
umask(0);
//5、将标准输入、输出、出错重定向到文件中
int fd = open("./log.txt", O_WRONLY|O_APPEND|O_CREAT, 0664);
if(-1 == fd)
{
perror("open error");
return -1;
}
//重定向
dup2(fd, STDIN_FILENO);
dup2(fd, STDOUT_FILENO);
dup2(fd, STDERR_FILENO);
//循环实现操作
while(1)
{
printf("hello a\n");
fflush(stdout);
sleep(1);
}
//关闭文件
close(fd);
return 0;
}