前话
进程:
程序:死的。只占用磁盘空间。 ——剧本。
进程;活的。运行起来的程序。占用内存、cpu等系统资源。 ——戏。
并发:在操作系统中,一个时间段中有多个进程都处于已启动运行到运行完毕之间的状态。但,任意时刻都仍只有一个进程在进行
在同一时刻只能有一条指令执行,但多个进程指令被快速的轮换执行,使得在宏观上具有多个进程同时执行的效果,但在微观上并不是同时执行的,只是把时间分成若干段,使多个进程快速交替的执行。这就好像两个人用同一把铁锨,轮流挖坑,一小时后,两个人各挖一个小一点的坑,要想挖两个大一点得坑,一定会用两个小时
单核CPU的并发:边听歌、边聊天、边看视频,通过在计算机内存中同时存放相互独立的程序,在管理程序控制之下相互穿插的运行,分时复用CPU,不感觉卡顿是因为转换速度太快了
并行(parallel):指在同一时刻,有多条指令在多个处理器上同时执行
![](https://img-blog.csdnimg.cn/img_convert/771149325c752473ba70d7469d41b058.png)
所有程序都要进入内存当中,上图存储介质中由上到下存储量逐渐变大,存储速度逐渐变慢,其中网络的存储空间可认为是无限的
Write这样一个函数,从硬盘一个程序变成进程通过系统调用进入内存,从内存进入缓存,从缓存进入寄存器(cpu内部),最终放到CPU中运算;CPU中有一个预取器对指令进行预取,再通过译码器对指令进行译码(将二进制译码成CPU语言),译码器再将CPU语言交给ALU,ALU计算完后将计算结果放到寄存器堆中。接着再将cpu处理后的数据放回cache,内存最终给用户
MMU:虚拟内存映射单元
虚拟内存和物理内存映射
linux内存管理(详解) - 知乎 (zhihu.com)
![](https://img-blog.csdnimg.cn/img_convert/796eb946d4b526daad19d41a9a86a719.png)
虚拟内存作用:
避免用户直接访问物理内存地址,防止一些破坏性操作,保护操作系统;
每个进程都被分配4GB的虚拟内存,用户程序可使用比实际物理内存更大的地址空间。
当进程需要实际访问内存时候,会由内核的[请求分页机制]产生[缺页异常]调用物理内存页。
MMU作用:
虚拟内存与物理内存的映射
设置修改内存访问级别
借助MMU虚拟内存映射,512M的物理内存运行程序也是4G的地址空间(虚拟地址,不真正存在)
借助MMU做虚拟地址映射:
1、物理内存通过分段和分页机制,将物理内存以page为单位,一个page大小是4KB,物理内存的分配和回收都是基于内存页(page)进行
2、当a.out和b.out中的int a =10在虚拟地址映射到物理地址,存储在不同的物理地址中,大小都是4K
3、当int arr[10000000] 在虚拟地址映射到物理地址,在物理地址中是选取可用地址(内存不连续),在虚拟地址中是连续的
4、a.out和b.out的pcb都在虚拟地址中的内核区域中,其都映射到同一物理内存区域的不同地址中。
此外,CPU存在不同权级,不同的虚拟地址会被分配不同的权级,从而有了之前说的用户访问内核会很慢,其实是CPU的权级在转换。
注:32位操作系统,寄存器大小是4字节
PCB进程控制块
PCB是描述进程的,进程名称、创建时间、进程编号等等
里面包含了:
进程id:系统中每个进程有唯一的id,一个非负整数
ps aux 返回结果里,第二列是进程id
文件描述符表
进程状态: 初始态、就绪态、运行态、挂起态、终止态。
进程切换时需要保存和恢复的一些CPU寄存器
描述虚拟地址空间的信息(比如说这里存着虚拟地址真正对应的物理地址)
描述控制终端信息(是否需要终端)
进程工作目录位置
*umask掩码 (进程的概念)(umask默认002)
信号相关信息资源。
用户id和组id
2的32次方为什么是4GB
32位操作系统寻址空间为什么是4GB
加上单位:
2^32byte(B) = 4Gbyte(B)
1、32指的是CPU的32位地址线,可以访问2^32个不同地址
2、CPU有8位位线,即访问可以获得8位数据(1字节(1B))
即:这里的32位表示 2 ^ 32 个地址,32指的是地址线,所以每一个地址线都有 8位位线,也就是1个地址可以访问 8bit 的数据,2 ^ 32 * 8 bit = 2 ^ 32 Byte = 4GB
2^32B = 2^22KB
2^22KB = 2^12MB
2^12MB = 2^2GB = 4GB
fork、getpid、getppid函数
pid_t fork(void)
创建子进程。父子进程各自返回。父进程返回子进程pid。 子进程返回 0.
pid_t getpid(void);获取当前进程 ID
pid_t getppid(void);获取当前进程的父进程 ID
![](https://img-blog.csdnimg.cn/img_convert/69a9b226f480c41397b75bcc91629c48.png)
打印父子进程
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
int main() {
pid_t pid;
pid = fork();
if (pid < 0) {
perror("fork error");
}
if (pid == 0){
printf("I am child pid = %d my father pid = %d\n",getpid(),getppid());
}
else {
printf("I am father pid = %d my father pid = %d\n",getpid(),getppid());
}
return 0;
}
编译后的结果:
I am father pid = 3179 my father pid = 2773
I am child pid = 3180 my father pid = 3179
循环顺序创建多个子进程
# include<stdio.h>
# include<stdlib.h>
# include<unistd.h>
int main()
{
pid_t pid;
int i;
for(i=0;i<5;i++)
{
pid =fork();
if(pid==0)
{
break;
}
}
if(5==i)//父进程 等待for结束自己台跳出
{
sleep(5);
printf("I am father\n");
}
else//子进程 提前break跳出 打印
{
sleep(i);
printf("I am %d child \n",i+1);
}
return 0;
}
编译后输出:
I am 1 child
I am 2 child
I am 3 child
I am 4 child
I am 5 child
I am father
父子进程
为什么需要创建子进程
每个程序的运行都需要进程,创建子进程可以实现并发
什么时候需要创建子进程
当父进程执行到某个阶段,接收到某个事件后需要创建一个独立的进程协助其完成任务时
子进程对父进程的继承
1、为什么要继承父进程的相关资源
父进程创建子进程是为了其能够协助父进程完成某些操作,因此,父进程必须将其自己的一些资源分享给子进程,以便父子进程共同完成任务。而于此目的不相关的资源,子进程没有必要继承,继承了只会白白浪费内存资源
2、继承的资源
用户号UIDs和用户组号GIDs
环境变量
堆栈
共享内存
打开文件的描述符
执行时关闭(Close-on-exec)标志
信号(Signal)控制设定
nice值,该值表示进程的优先级, 数值越小,优先级越高
进程调度类别(scheduler class)
进程组号
对话期ID(Session ID) (指:进程所属的对话期 (session)ID, 一个对话期包括一个或多个进程组, 更详细说明参见《高级编程》 9.5节
当前工作目录
根目录
文件方式创建屏蔽字
资源限制
控制终端
3、不继承的资源
进程号PID
不同的父进程号(译者注: 即子进程的父进程号与父进程的父进程号不同, 父进程号可由getppid函数得到)
子进程自己的文件描述符和目录流的拷贝
异步输入和输出
文件锁,pending alarms和pending signals
timer_create函数创建的计时器
阻塞信号集初始化为空集
资源使用(resource utilizations)设定为0
在tms结构中的系统时间
子进程不继承父进程的进程正文(text), 数据和其它锁定内存(memory locks) (译者注:锁定内存指被锁定的虚拟内存页,锁定后, 不允许内核将其在必要时换出(page out)
其他需要注意:
1.父进程和子进程拥有独立的地址空间和PID参数
2.经过fork()以后,父进程和子进程拥有相同内容的代码段、数据段和用户堆栈(共享内存)
3.读时共享、写时复制:就像父进程把自己克隆了一遍。事实上,父进程只复制了自己的PCB块。而代码段,数据段和用户堆栈内存空间并没有复制一份,而是与子进程共享。只有当子进程在运行中出现写操作时,才会产生中断,并为子进程分配内存空间。由于父进程的PCB和子进程的一样,所以在PCB中断中所记录的父进程占有的资源,也是与子进程共享使用的。这里的“共享”一词意味着“竞争”
父进程对子进程的回收
1.孤儿进程:父进程先结束的时候,此时子进程变为孤儿进程。系统会把init进程(进程1)变为子进程的父进程
2.僵尸进程:子进程先于父进程结束,父进程还未来得及将其收尸(系统只是回收了这个进程工作时消耗的内存和IO,而并没有回收这个进程本身占用的内存)
3.回收函数
wait:阻塞,子进程结束时,系统向其父进程发送SIGCHILD信号 pid_t wait(int *status);
waitpid:可以回收指定PID的子进程,可以阻塞式或非阻塞式两种工作模式
pid_t waitpid(pid, &status, 0); //0默认表示阻塞
pid_t waitpid(pid,&status,WNOHANG) //表示非阻塞
总结为:
父子进程相同:
刚fork后。 data段、text段、堆、栈、环境变量、全局变量、宿主目录位置、进程工作目录位置、信号处理方式
父子进程不同:
进程id、fork的返回值、各自的父进程、进程创建时间、闹钟、未决信号集
父子进程共享:
读时共享、写时复制。
父子进程之间不共享全局变量
但其共享:
1、文件描述符 2. mmap映射区
注:fork之后父进程先执行还是子进程先执行不确定,取决于内核所使用的调度算法
exec函数族
fork 创建子进程后执行的是和父进程相同的程序(但有可能执行不同的代码分支),子进程往往要调用一种 exec 函数以执行另一个程序。当进程调用一种 exec 函数时,该进程的用户空间代码和数据完全被新程序替换,从新程序的启动例程开始执行。调用 exec 并不创建新进程,所以调用 exec 前后该进程的 id 并未改变。
将当前进程的.text、.data 替换为所要加载的程序的.text、.data,然后让进程从新的.text第一条指令开始执行,但进程 ID 不变,换核不换壳。
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[]);
wait函数
一个进程终止时会关闭所有文件描述符,释放在用户空间分配的内存,但它的PCB还保留着,内核在其中保存了一些信息:如果是正常终止则保存着退出状态,如果是异常终止则保存着导致该进程终止的信号是哪个。这个进程的父进程可以调用wait或者waitpid获取这些信息,然后彻底清除掉这个进程。
wait函数: 回收子进程退出资源, 阻塞回收任意一个。
pid_t wait(int *status)
参数:(传出) 回收进程的状态。
返回值:成功: 回收进程的pid
失败: -1, errno
函数作用1: 阻塞等待子进程退出
函数作用2: 清理子进程残留在内核的 pcb 资源
函数作用3: 通过传出参数,得到子进程结束状态
获取子进程正常终止值:
WIFEXITED(status) --》 为真 --》调用 WEXITSTATUS(status) --》 得到 子进程 退出值。
获取导致子进程异常终止信号:
WIFSIGNALED(status) --》 为真 --》调用 WTERMSIG(status) --》 得到 导致子进程异常终止的信号编号。
wait获取子进程退出值和异常终止信号
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
int main()
{
pid_t pid,wpid;
int status;
pid =fork();
if(pid==0)
{
printf(" i am child,my id is%d\n",getpid());
printf("child die\n");
return 73;
}
else if(pid>0)
{
wpid=wait(NULL);//不关心怎么结束的
wpid = wait(&status);//等待子进程结束
if(wpid==-1)
{
perror("wait error");
exit(1);
}
if(WIFEXITED(status))//判断 子进程正常退出判断
{
printf("child exit with%d\n",WEXITSTATUS(status));
printf("------parent finish\n");
}
if(WIFSIGNALED(status))//判断 子进程异常退出判断
{
printf("child exit with%d\n",WTERMSIG(status));
}
}
else
{
perror("fork");
return 1;
}
}
![](https://img-blog.csdnimg.cn/img_convert/113bd314b52e194cba2f3df43664d91e.png)
waitpid函数
waitpid函数: 指定某一个进程进行回收。可以设置非阻塞。
waitpid(-1, &status, 0) == wait(&status);
pid_t waitpid(pid_t pid, int *status, int options)
参数:
pid:指定回收某一个子进程pid
> 0: 待回收的子进程pid
-1:任意子进程
0:同组的子进程。
status:(传出) 回收进程的状态。
options:WNOHANG 指定回收方式为,非阻塞。
返回值:
> 0 : 表成功回收的子进程 pid
0 : 函数调用时, 参3 指定了WNOHANG, 并且,没有子进程结束。
-1: 失败。errno
回收子进程
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/wait.h>
#include <pthread.h>
int main(int argc, char *argv[])
{
int i;
pid_t pid, wpid, tmpid;
for (i = 0; i < 5; i++) {
pid = fork();
if (pid == 0) { // 循环期间, 子进程不 fork
break;
}
if (i == 2) {
tmpid = pid;
printf("--------pid = %d\n", tmpid);
}
}
if (5 == i) { // 父进程, 从 表达式 2 跳出
sleep(5);
//wait(NULL); // 一次wait/waitpid函数调用,只能回收一个子进程.
//wpid = waitpid(-1, NULL, WNOHANG); //回收任意子进程,没有结束的子进程,父进程直接返回0
//wpid = waitpid(tmpid, NULL, 0); //指定一个进程回收, 阻塞等待
printf("i am parent , before waitpid, pid = %d\n", tmpid);
wpid = waitpid(tmpid, NULL, WNOHANG); //指定一个进程回收, 不阻塞
//wpid = waitpid(tmpid, NULL, 0); //指定一个进程回收, 阻塞回收
if (wpid == -1) {
perror("waitpid error");
exit(1);
}
printf("I'm parent, wait a child finish : %d \n", wpid);
} else { // 子进程, 从 break 跳出
sleep(i);
printf("I'm %dth child, pid= %d\n", i+1, getpid());
}
return 0;
}
![](https://img-blog.csdnimg.cn/img_convert/1c587cd10532c69837545652f5cb24c6.png)
回收多个子进程
不管是wait还是waitpid一次都只能回收一个子进程,要想回收多个,就是在父进程中加入while循环
// 回收多个子进程
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/wait.h>
#include <pthread.h>
int main(int argc, char *argv[])
{
int i;
pid_t pid, wpid;
for (i = 0; i < 5; i++) {
pid = fork();
if (pid == 0) { // 循环期间, 子进程不 fork
break;
}
}
if (5 == i) { // 父进程, 从 表达式 2 跳出
/*
while ((wpid = waitpid(-1, NULL, 0))) { // 使用阻塞方式回收子进程
printf("wait child %d \n", wpid);
}
*/
while ((wpid = waitpid(-1, NULL, WNOHANG)) != -1) { //使用非阻塞方式,回收子进程.
if (wpid > 0) {
printf("wait child %d \n", wpid);
} else if (wpid == 0) {
sleep(1);
continue; //跳出本次循环,进入下一次while循环
}
}
} else { // 子进程, 从 break 跳出
sleep(i);
printf("I'm %dth child, pid= %d\n", i+1, getpid());
}
return 0;
}
![](https://img-blog.csdnimg.cn/img_convert/a7d75c980df09ef796f0fe4a84ec80b5.png)
会话和守护进程
会话
进程组:多个进程
会话:多个进程组的集合
1、当父进程创建子进程的时候,默认子进程与父进程属于同一进程组。进程组ID==第一个进程ID(组长进程)。所以组长进程标识:其进程组ID==其进程ID
2、组长进程可以创建一个进程组,创建该进程组中的进程后然后终止。(只要进程组中有一个进程存在,进程组就存在,与组长进程是否终止无关)
3、进程组生存期:进程组创建到最后一个进程离开(终止或转移到另一个进程组)。
4、一个进程可以为自己或子进程设置进程组ID
可以使用kill -SIGKILL -进程组ID(负的)来将整个进程组内的进程全部杀
创建会话的6点注意事项:
调用进程不能是进程组组长,该进程变成新会话首进程
该进程成为一个新进程组的组长进程
需要root权限(ubuntu不需要)
新会话丢弃原有的控制终端,该会话没有控制终端
该调用进程是组长进程,则出错返回
建立新会话时,先调用fork,父进程终止,子进程调用setsid
1和2表示:调用setsid函数的进程会成为新的会长,同时也是新的组长。即pid、组ID、会话ID三个ID都是同一个
getsid函数:
pid_t getsid(pid_t pid) 获取当前进程的会话id
成功返回调用进程会话ID,失败返回-1,设置error
setsid函数:
pid_t setsid(void) 创建一个会话,并以自己的ID设置进程组ID,同时也是新会话的ID
成功返回调用进程的会话ID,失败返回-1,设置error
![](https://img-blog.csdnimg.cn/img_convert/204837ec1ae2f805bdb6b8bc8926fc58.gif)
守护进程
1、daemon进程。通常运行于操作系统后台,脱离控制终端。一般不与用户直接交互。周期性的等待某个事件发生或周期性执行某一动作。
2、不受用户登录注销影响。通常采用以d结尾的命名方式。(httpd、sshd、vsftpd、nfsd)
3、创建守护进程,最关键的一步是调用setsid函数创建一个新的Session,并成为Session Leader
守护进程创建步骤:
1. fork子进程,让父进程终止。
所有工作在子进程中进行形式上脱离了控制终端
2. 子进程调用 setsid() 创建新会话
3. 通常根据需要,改变工作目录位置 chdir(), 防止目录被卸载。
4. 通常根据需要,重设umask文件权限掩码,影响新文件的创建权限。 022 -- 755 0345 --- 432 r---wx-w- 422
5. 通常根据需要,关闭/重定向 文件描述符
通常关闭的文件描述符的0、1、2;但是有许多程序员是不关闭文件描述符的(而是重定向)
因为一旦关闭0、1、2,当执行一个函数时,返回的就会是0( 返回文件描述符中可用的最小的那个数),与编程习惯不符
6. 守护进程 业务逻辑。while()
创建一个守护进程:
#include<stdio.h>#include<sys/stat.h>#include<fcntl.h>#include<stdlib.h>#include<string.h>#include<unistd.h>#include<errno.h>#include<pthread.h>voidsys_err(constchar *str){
perror(str);
exit(1);
}
intmain(int argc, char *argv[]){
pid_t pid;
int ret, fd;
pid = fork();
if (pid > 0) // 父进程终止 exit(0);
pid = setsid(); //创建新会话 if (pid == -1)
sys_err("setsid error");
ret = chdir("/home/zhcode/Code/code146"); // 改变工作目录位置 if (ret == -1)
sys_err("chdir error");
umask(0022); // 改变文件访问权限掩码 close(STDIN_FILENO); // 关闭文件描述符 0
fd = open("/dev/null", O_RDWR); // fd --> 0 if (fd == -1)
sys_err("open error");
dup2(fd, STDOUT_FILENO); // 重定向 stdout和stderr dup2(fd, STDERR_FILENO);
while (1); // 模拟 守护进程业务. return0;
}
![](https://img-blog.csdnimg.cn/img_convert/71dce8f50b6f2531de7bd9b01d8bb8d8.gif)
编译运行,结果如下:
![](https://img-blog.csdnimg.cn/img_convert/9efd54ccd5e4fe66099a9b198c67e116.png)
![](https://img-blog.csdnimg.cn/img_convert/aab4414a784b72731849e2d235e64acb.gif)
编辑
查看进程列表,如下:
![](https://img-blog.csdnimg.cn/img_convert/7c644b8b3e7f1389b17d50bb056d47f5.png)
![](https://img-blog.csdnimg.cn/img_convert/3fd87c7fc7f5f1c6732051b1101aec6c.gif)
编辑
这个daemon进程就不会受到用户登录注销影响。
要想终止,就必须用kill命令 Kill -9 15203杀死daemon进程
Linux中查看进程命令ps aux,ps -ef,ps -A,ps -a_夏已微凉、的博客-CSDN博客
ps aux 查看进程ID
Linux系统中查看父进程ID,进程ID,进程组ID,会话ID的方法:ps ajx_ctrigger的博客-CSDN博客_进程组id怎么看
ps ajx 查看父进程ID,进程ID,进程组ID,会话ID
ps ajx | grep daemon 在注销当前用户再登录之后快速查找daemon