linux下的进程详解(1)——这可能是最深入浅出的进程学习笔记
sinye56 2024-11-02 13:24 10 浏览 0 评论
进程控制块(PCB)
在Linux中task_struct结构体即是PCB。PCB是进程的唯一标识,PCB由链表实现(为了动态插入和删除)。
进程创建时,为该进程生成一个PCB;进程终止时,回收PCB。
PCB包含信息:1、进程状态(state);2、进程标识信息(uid、gid);3、定时器(time);4、用户可见寄存器、控制状态寄存器、栈指针等(tss)
每个进程都有一个非负的唯一进程ID(PID)。虽然是唯一的,但是PID可以重用,当一个进程终止后,其他进程就可以使用它的PID了。
PID为0的进程为调度进程,该进程是内核的一部分,也称为系统进程;PID为1的进程为init进程,它是一个普通的用户进程,但是以超级用户特权运行;PID为2的进程是页守护进程,负责支持虚拟存储系统的分页操作。
除了PID,每个进程还有一些其他的标识符:
五种进程状态转换如下图所示:
每个进程的task_struct和系统空间堆栈存放位置如下:两个连续的物理页
系统堆栈空间不能动态扩展,在设计内核、驱动程序时要避免函数嵌套太深,同时不宜使用太大太多的局部变量,因为局部变量都是存在堆栈中的。
进程的创建
新进程的创建,首先在内存中为新进程创建一个task_struct结构,然后将父进程的task_struct内容复制其中,再修改部分数据。分配新的内核堆栈、新的PID、再将task_struct 这个node添加到链表中。所谓创建,实际上是“复制”。
子进程刚开始,内核并没有为它分配物理内存,而是以只读的方式共享父进程内存,只有当子进程写时,才复制。即“copy-on-write”。
fork都是由do_fork实现的,do_fork的简化流程如下图:
fork函数
fork函数时调用一次,返回两次。在父进程和子进程中各调用一次。子进程中返回值为0,父进程中返回值为子进程的PID。程序员可以根据返回值的不同让父进程和子进程执行不同的代码。
一个形象的过程:
需要C/C++ Linux服务器架构师学习资料私信“资料”(资料包括C/C++,Linux,golang技术,Nginx,ZeroMQ,MySQL,Redis,fastdfs,MongoDB,ZK,流媒体,CDN,P2P,K8S,Docker,TCP/IP,协程,DPDK,ffmpeg等),免费分享
运行这样一段演示程序:
1 #include <stdio.h>
2 #include <unistd.h>
3 #include <stdlib.h>
4
5 int main()
6 {
7 pid_t pid;
8 char *message;
9 int n = 0;
10 pid = fork();
11 while(1){
12 if(pid < 0){
13 perror("fork failed\n");
14 exit(1);
15 }
16 else if(pid == 0){
17 n--;
18 printf("child's n is:%d\n",n);
19 }
20 else{
21 n++;
22 printf("parent's n is:%d\n",n);
23 }
24 sleep(1);
25 }
26 exit(0);
27 }
运行结果:
可以发现子进程和父进程之间并没有对各自的变量产生影响。
一般来说,fork之后父、子进程执行顺序是不确定的,这取决于内核调度算法。进程之间实现同步需要进行进程通信。
什么时候使用fork呢?
一个父进程希望子进程同时执行不同的代码段,这在网络服务器中常见——父进程等待客户端的服务请求,当请求到达时,父进程调用fork,使子进程处理此请求。
一个进程要执行一个不同的程序,一般fork之后立即调用exec
vfork函数
vfork与fork对比:
相同:
返回值相同
不同:
fork创建子进程,把父进程数据空间、堆和栈复制一份;vfork创建子进程,与父进程内存数据共享;
vfork先保证子进程先执行,当子进程调用exit()或者exec后,父进程才往下执行
为什么需要vfork?
因为用vfork时,一般都是紧接着调用exec,所以不会访问父进程数据空间,也就不需要在把数据复制上花费时间了,因此vfork就是”为了exec而生“的。
运行这样一段演示程序:
1 #include <stdio.h>
2 #include <unistd.h>
3 #include <stdlib.h>
4
5 int main()
6 {
7 pid_t pid;
8 char *message;
9 int n = 0;
10 int i;
11 pid = vfork();
12 for(i = 0; i < 10; i++){
13 if(pid < 0){
14 perror("fork failed\n");
15 exit(1);
16 }
17 else if(pid == 0){
18 n--;
19 printf("child's n is:%d\n",n);
20 if(i == 1)
21 _exit(0);
22 //return 0;
23 //exit(0);
24 }
25 else{
26 n++;
27 printf("parent's n is:%d\n",n);
28 }
29 sleep(1);
30 }
31 exit(0);
32 }
运行结果:
可以发现子进程先被执行,exit后,父进程才被执行,同时子进程改变了父进程中的数据
子进程return 0 会发生什么?
运行结果:
从上面我们知道,结束子进程的调用是exit()而不是return,如果你在vfork中return了,那么,这就意味main()函数return了,注意因为函数栈父子进程共享,所以整个程序的栈就跪了。 如果你在子进程中return,那么基本是下面的过程: 1)子进程的main() 函数 return了,于是程序的函数栈发生了变化。 2)而main()函数return后,通常会调用 exit()或相似的函数(如:_exit(),exitgroup()) 3)这时,父进程收到子进程exit(),开始从vfork返回,但是尼玛,老子的栈都被你子进程给return干废掉了,你让我怎么执行?(注:栈会返回一个诡异一个栈地址,对于某些内核版本的实现,直接报“栈错误”就给跪了,然而,对于某些内核版本的实现,于是有可能会再次调用main(),于是进入了一个无限循环的结果,直到vfork 调用返回 error) 好了,现在再回到 return 和 exit,return会释放局部变量,并弹栈,回到上级函数执行。exit直接退掉。如果你用c++ 你就知道,return会调用局部对象的析构函数,exit不会。(注:exit不是系统调用,是glibc对系统调用 _exit()或_exitgroup()的封装) 可见,子进程调用exit() 没有修改函数栈,所以,父进程得以顺利执行
execve
可执行文件装入内核的linux_binprm结构体。
进程调用exec时,该进程执行的程序完全被替换,新的程序从main函数开始执行。因为调用exec并不创建新进程,只是替换了当前进程的代码区、数据区、堆和栈。
六种不同的exec函数:
当指定filename作为参数时:
如果filename中包含/,则将其视为路径名。
否则,就按系统的PATH环境变量,在它所指定的各个目录中搜索可执行文件。
*出于安全方面的考虑,有些人要求在搜索路径中不要包括当前目录。
在这6个函数中,只有execve是内核的系统调用。另外5个只是库函数,他们最终都要调用该系统调用,如下图所示:
execve的实现由do_execve完成,简化的实现过程如下图:
关于这些函数的区别,需要时可以查看《APUE》关于exec函数部分的内容。
运行这样一段演示程序:
1 #include <errno.h>
2 #include <stdio.h>
3 #include <stdlib.h>
4
5 char command[256];
6 void main()
7 {
8 int rtn; /*child process return value*/
9 while(1) {
10 printf( ">" );
11 fgets( command, 256, stdin );
12 command[strlen(command)-1] = 0;
13 if ( fork() == 0 ) {
14 execlp( command, NULL );
15 perror( command );
16 exit( errno );
17 }
18 else {
19 wait ( &rtn );
20 printf( " child process return %d\n", rtn );
21 }
22 }
23 }
a.out 是一个打印hello world的可执行文件。
运行结果:
进程终止
正常终止(5种)
从main返回,等效于调用exit
调用exit
exit 首先调用各终止处理程序,然后按需多次调用fclose,关闭所有的打开流。
调用_exit或者_Exit
最后一个线程从其启动例程返回
最后一线程调用pthread_exit
异常终止(3种)
调用abort
接到一个信号并终止
最后一个线程对取消请求作出响应
wait和waitpid函数
wait用于使父进程阻塞,等待子进程退出;waitpid有若干选项,如可以提供一个非阻塞版本的wait,也能实现和wait相同的功能,实际上,linux中wait的实现也是通过调用waitpid实现的。
waitpid返回值:正常返回子进程号;使用WNOHANG且没有子进程退出返回0;调用出错返回-1;
运行如下演示程序
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <sys/types.h>
4 #include <sys/wait.h>
5
6 int main()
7 {
8 pid_t pid0,pid1;
9 pid0 = fork();
10 if(pid0 < 0){
11 perror("fork");
12 exit(1);
13 }
14 else if(pid0 == 0){
15 sleep(5);
16 exit(0);//child
17 }
18 else{
19 do{
20 pid1 = waitpid(pid0,NULL,WNOHANG);
21 if(pid1 == 0){
22 printf("the child process has not exited.\n");
23 sleep(1);
24 }
25 }while(pid1 == 0);
26 if(pid1 == pid0){
27 printf("get child pid:%d",pid1);
28 exit(0);
29 }
30 else{
31 exit(1);
32 }
33 }
34 return 0;
35 }
36
37
38
39 当把第三个参数WNOHANG改为0时,就不会有上面五个显示语句了,说明父进程阻塞了。
40
41
42
43 a.out 的代码如下:
44
45
46 #include <stdio.h>
47 void main()
48
49 {
50 printf("hello WYJ\n");
51 }
52
53
54
55 process.c的代码如下:
56
57 #include <stdio.h>
58 #include <sys/types.h>
59 #include <unistd.h>
60 #include <stdlib.h>
61 #include <sys/times.h>
62 #include <sys/wait.h>
63
64 int main()
65 {
66 pid_t pid_1,pid_2,pid_wait;
67 pid_1 = fork();
68 pid_2 = fork();
69 if(pid_1 < 0){
70 perror("fork1 failed\n");
71 exit(1);
72 }else if(pid_1 == 0 && pid_2 != 0){//do not allow child 2 to excute this process.
73 if(execlp("./a.out", NULL) < 0){
74 perror("exec failed\n");
75 }//child;
76 exit(0);
77 }
78 if(pid_2 < 0){
79 perror("fork2 failded\n");
80 exit(1);
81 }else if(pid_2 == 0){
82 sleep(10);
83 }
84 if(pid_2 > 0){//parent
85 do{
86 pid_wait = waitpid(pid_2, NULL, WNOHANG);//no hang
87 sleep(2);
88 printf("child 2 has not exited\n");
89 }while(pid_wait == 0);
90 if(pid_wait == pid_2){
91 printf("child 2 has exited\n");
92 exit(0);
93 }else{
94 // printf("pid_2:%d\n",pid_2);
95 perror("waitpid error\n");
96 exit(1);
97
98 }
99 }
100 exit(0);
101 }
运行结果:
编写一个多进程程序:该实验有 3 个进程,其中一个为父进程,其余两个是该父进程创建的子进程,其中一个子进程运行“ls -l”指令,另一个子进程在暂停 5s 之后异常退出,父进程并不阻塞自己,并等待子进程的退出信息,待收集到该信息,父进程就返回。
1 #include<stdio.h>
2 #include<string.h>
3 #include<fcntl.h>
4 #include<unistd.h>
5 #include<sys/types.h>
6 #include<sys/wait.h>
7 int main()
8 {
9 pid_t child1,child2,child;
10 if((child1 = fork()) < 0){
11 perror("failed in fork 1");
12 exit(1);
13 }
14 if((child2 = fork()) < 0){
15 perror("failed in fork 2");
16 exit(1);
17 }
18 if(child1 == 0){
19 //run ls -l
20 if(child2 == 0){
21 printf("in grandson\n");
22 }
23 else if(execlp("ls", "ls", "-l", NULL) < 0){
24 perror("child1 execlp");
25 }
26 }
27 else if(child2 == 0){
28 sleep(5);
29 exit(0);
30 }
31 else{
32 do{
33 sleep(1);
34 printf("child2 not exits\n");
35 child = waitpid(child2, NULL, WNOHANG);
36 }while(child == 0);
37 if(child == child2){
38 printf("get child2\n");
39 }
40 else{
41 printf("Error occured\n");
42 }
43 }
44 }
运行结果:
init进程成为所有僵尸进程(孤儿进程)的父进程
僵尸进程
在进程调用了exit之后,该进程并非马上就消失掉,而是留下了一个成为僵尸进程的数据结构,记载该进程的退出状态等信息供其他进程收集,除此之外,僵尸进程不再占有任何内存空间。
子进程结束之后为什么会进入僵尸状态? 因为父进程可能会取得子进程的退出状态信息。
如何查看僵尸进程?
linux中命令ps,标记为Z的进程就是僵尸进程。
执行下面一段程序:
1 #include <sys/types.h>
2 #include <unistd.h>
3 int main()
4 {
5 pid_t pid;
6 pid = fork();
7 if(pid < 0){
8 printf("error occurred\n");
9 }else if(pid == 0){
10 exit(0);
11 }else{
12 sleep(60);
13 wait(NULL);
14 }
15 }
运行结果:
ps -ef|grep defunc可以找出僵尸进程
ps -l 可以得到更详细的进程信息
运行结果显示:
运行两次之后发现有两个Z进程,然后等待一分钟后,Z进程被父进程回收。
其中S表示状态:
O:进程正在处理器运行
S:休眠状态
R:等待运行
I:空闲状态
Z:僵尸状态
T:跟踪状态
B:进程正在等待更多的内存分页
C:cpu利用率的估算值
收集僵尸进程的信息,并终结这些僵尸进程,需要我们在父进程中使用waitpid和wait,这两个函数能够手机僵尸进程留下的信息并使进程彻底消失。
守护进程Daemon
是linux的后台服务进程。它是一个生存周期较长的进程,没有控制终端,输出无处显示。用户层守护进程的父进程是init进程。
守护进程创建步骤:
1、创建子进程,父进程退出,子进程被init自动收养;fork exit
2、调用setsid创建新会话,成为新会话的首进程,成为新进程组的组长进程,摆脱父进程继承过来的会话、进程组等;setsid
3、改变当前目录为根目录,保证工作的文件目录不被删除;chdir(“/”)
4、重设文件权限掩码,给子进程更大的权限;umask(0)
5、关闭不用的文件描述符,因为会消耗资源;close
一个守护进程的实例:每隔10s写入一个“tick”
1 #include<stdio.h>
2 #include<string.h>
3 #include<fcntl.h>
4 #include<unistd.h>
5 #include<sys/types.h>
6 #define MAXFILE 65535
7
8 int main()
9 {
10 int fd,len,i;
11 pid_t pid;
12 char *buf = "tick\n";
13 len = strlen(buf);
14 if((pid = fork()) < 0){
15 perror("fork failed");
16 exit(1);
17 }
18 else if(pid > 0){
19 exit(0);
20 }
21 setsid();
22 if(chdir("/") < 0){
23 perror("chdir failed");
24 exit(1);
25 }
26 umask(0);
27 for(i = 0; i < MAXFILE; i++){
28 close(i);
29 }
30 while(1){
31 if((fd = open("/tmp/dameon.log", O_CREAT | O_WRONLY | O_APPEND, 0600)) < 0){
32 perror("open log failed");
33 exit(1);
34 }
35 write(fd, buf, len+1);
36 close(fd);
37 sleep(10);
38 }
39 }
运行结果:
1 #include<stdio.h>
2 #include<string.h>
3 #include<fcntl.h>
4 #include<unistd.h>
5 #include<sys/types.h>
6 #include<syslog.h>
7 #define MAXFILE 65535
8
9 int main()
10 {
11 int fd,len,i;
12 pid_t pid,child;
13 char *buf = "tick\n";
14 len = strlen(buf);
15 if((pid = fork()) < 0){
16 perror("fork failed");
17 exit(1);
18 }
19 else if(pid > 0){
20 exit(0);
21 }
22 openlog("Jack", LOG_PID, LOG_DAEMON);
23 if(setsid() < 0){
24 syslog(LOG_ERR, "%s\n", "setsid");
25 exit(1);
26 }
27
28 if(chdir("/") < 0){
29 syslog(LOG_ERR, "%s\n", "chdir");
30 exit(1);
31 }
32 umask(0);
33 for(i = 0; i < MAXFILE; i++){
34 close(i);
35 }
36 if((child = fork()) < 0){
37 syslog(LOG_ERR, "%s\n", "fork");
38 exit(1);
39 }
40 if(child == 0){
41 //printf("in child\n");//can not use terminal from now on.
42 syslog(LOG_INFO, "in child");
43 sleep(10);
44 exit(0);
45 }
46 else{
47 waitpid(child, NULL, 0);
48 //printf("child exits\n");//can not use terminal from now on.
49 syslog(LOG_INFO, "child exits");
50 closelog();
51 while(1){
52 sleep(10);
53 }
54 }
55
56 }
真正编写调试的时候会发现需要杀死守护进程。
如何杀死守护进程?
ps -aux 找到对应PID
kill -9 PID
相关推荐
- Linux在线安装JDK1.8
-
首先在服务器pingwww.baidu.com查看是否可以连网然后就可以在线下载一、下载安装JDK1.81、在下载安装的同时做好一些准备工作...
- Linux安装JDK,超详细
-
1、了解RPMRPM是Red-HatPackageManager(RPM软件包管理器)的缩写,这一文件格式名称虽然打上了RedHat的标志,但是其原始设计理念是开放式的,现在包括OpenLinux...
- Linux安装jdk1.8(超级详细)
-
前言最近刚购买了一台阿里云的服务器准备要搭建一个网站,正好将网站的一个完整搭建过程分享给大家!#一、下载jdk1.8首先我们需要去下载linux版本的jdk1.8安装包,我们有两种方式去下载安装...
- Linux系统安装JDK教程
-
下载jdk-8u151-linux-x64.tar.gz下载地址:https://www.oracle.com/technetwork/java/javase/downloads/index.ht...
- 干货|JDK下载安装与环境变量配置图文教程「超详细」
-
1.JDK介绍1.1什么是JDK?SUN公司提供了一套Java开发环境,简称JDK(JavaDevelopmentKit),它是整个Java的核心,其中包括Java编译器、Java运行工具、Jav...
- Linux下安装jdk1.8
-
一、安装环境操作系统:CentOSLinuxrelease7.6.1810(Core)JDK版本:1.8二、安装步骤1.下载安装包...
- Linux上安装JDK
-
以CentOS为例。检查是否已安装过jdk。yumlist--installed|grepjdk或者...
- Linux系统的一些常用目录以及介绍
-
根目录(/):“/”目录也称为根目录,位于Linux文件系统目录结构的顶层。在很多系统中,“/”目录是系统中的唯一分区。如果还有其他分区,必须挂载到“/”目录下某个位置。整个目录结构呈树形结构,因此也...
- Linux系统目录结构
-
一、系统目录结构几乎所有的计算机操作系统都是使用目录结构组织文件。具体来说就是在一个目录中存放子目录和文件,而在子目录中又会进一步存放子目录和文件,以此类推形成一个树状的文件结构,由于其结构很像一棵树...
- Linux文件查找
-
在Linux下通常find不很常用的,因为速度慢(find是直接查找硬盘),通常我们都是先使用whereis或者是locate来检查,如果真的找不到了,才以find来搜寻。为什么...
- 嵌入式linux基本操作之查找文件
-
对于很多初学者来说都习惯用windows操作系统,对于这个系统来说查找一个文件简直不在话下。而学习嵌入式开发行业之后,发现所用到的是嵌入式Linux操作系统,本想着跟windows类似,结果在操作的时...
- linux系统查看软件安装目录的方法
-
linux系统下怎么查看软件安装的目录?方法1:whereis软件名以查询nginx为例子...
- Linux下如何对目录中的文件进行统计
-
统计目录中的文件数量...
- Linux常见文件目录管理命令
-
touch用于创建空白文件touch文件名称mkdir用于创建空白目录还可以通过参数-p创建递归的目录...
- Linux常用查找文件方法总结
-
一、前言Linux系统提供了多种查找文件的命令,而且每种查找命令都具有其独特的优势,下面详细总结一下常用的几个Linux查找命令。二、which命令查找类型:二进制文件;...
你 发表评论:
欢迎- 一周热门
- 最近发表
- 标签列表
-
- oracle忘记用户名密码 (59)
- oracle11gr2安装教程 (55)
- mybatis调用oracle存储过程 (67)
- oracle spool的用法 (57)
- oracle asm 磁盘管理 (67)
- 前端 设计模式 (64)
- 前端面试vue (56)
- linux格式化 (55)
- linux图形界面 (62)
- linux文件压缩 (75)
- Linux设置权限 (53)
- linux服务器配置 (62)
- mysql安装linux (71)
- linux启动命令 (59)
- 查看linux磁盘 (72)
- linux用户组 (74)
- linux多线程 (70)
- linux设备驱动 (53)
- linux自启动 (59)
- linux网络命令 (55)
- linux传文件 (60)
- linux打包文件 (58)
- linux查看数据库 (61)
- linux获取ip (64)
- 关闭防火墙linux (53)