- 1. 进程的几个概念
- 2. fork创建子进程
- 3. 地址空间
- 4. 进程的退出
- 4.1 基本概念
- 4.2 父进程等待子进程退出
- 4.2.1 waitpid
- 4.2.2 退出状态status
- 5. 其他
- 5.1 获取环境变量
- 5.2 孤儿进程
-
每个进程执行其逻辑流的一部分,然后在其他进程轮流执行时被抢占(暂时挂起)。进程与其他进程轮流执行的过程称为多任务处理。进程执行其流的一部分的每个时间段称为时间片。因此,多任务也被称为时间切片。任何逻辑流在时间上与另一个流重叠的进程都称为并发进程,这两个进程称为并发运行。
-
操作系统内核使用一种更高级别的异常控制流(称为上下文切换)来实现多任务。在进程执行过程中的某些时刻,内核可以决定抢占当前进程并重新启动先前抢占的进程。这个决定称为调度,由内核中称为调度器的部分来处理。该机制(1)保存了当前进程的上下文,(2)恢复一些以前抢占的进程保存的上下文,(3)将控制权传递给这个新恢复的进程。
-
系统中的每个程序都在某个进程的上下文中运行。上下文由程序正确运行所需的状态组成。该状态包括:
- 存储在内存中的程序代码和数据、用户及内核堆栈
- 通用寄存器、浮点寄存器、程序计数器(指向程序执行的下一条指令地址)、状态寄存器:记录临时数据以供调度时恢复。
- 环境变量、文件描述符集和各种内核数据结构,如描述地址空间的页表、包含当前进程信息的进程表,以及一个文件表,其中包含有关进程已打开的文件的信息。
运行一段c程序:
#include#include int main() { while(1) { printf("hello worldn"); sleep(1); } return 0; }
查看进程信息:
[test@VM-12-4-centos ~]$ ps axj | head -1 && ps axj | grep main|grep -v grep
PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND
1 11255 11255 2381 ? -1 S 1001 0:36 ./main
25802 26785 26785 25802 pts/0 26785 S+ 1001 0:00 ./main
[test@VM-12-4-centos ~]$ ls -al /proc/26785/ # 查看proc下的目录文件,对应的数字是进程的PID
total 0
dr-xr-xr-x 9 test test 0 May 30 01:08 .
dr-xr-xr-x 105 root root 0 Dec 18 12:46 ..
dr-xr-xr-x 2 test test 0 May 30 01:09 attr
-rw-r--r-- 1 test test 0 May 30 01:09 autogroup
-r-------- 1 test test 0 May 30 01:09 auxv
-r--r--r-- 1 test test 0 May 30 01:09 cgroup
--w------- 1 test test 0 May 30 01:09 clear_refs
-r--r--r-- 1 test test 0 May 30 01:08 cmdline # 启动当前进程使用的命令
-rw-r--r-- 1 test test 0 May 30 01:09 comm
-rw-r--r-- 1 test test 0 May 30 01:09 coredump_filter
-r--r--r-- 1 test test 0 May 30 01:09 cpuset
lrwxrwxrwx 1 test test 0 May 30 01:09 cwd -> /home/test #进程所在的目录
-r-------- 1 test test 0 May 30 01:09 environ # 环境变量
lrwxrwxrwx 1 test test 0 May 30 01:08 exe -> /home/test/main # 进程所执行的二进制文件
dr-x------ 2 test test 0 May 30 01:08 fd #文件描述符
dr-x------ 2 test test 0 May 30 01:09 fdinfo
-rw-r--r-- 1 test test 0 May 30 01:09 gid_map
-r-------- 1 test test 0 May 30 01:09 io
-r--r--r-- 1 test test 0 May 30 01:09 limits
-rw-r--r-- 1 test test 0 May 30 01:09 loginuid
dr-x------ 2 test test 0 May 30 01:09 map_files
-r--r--r-- 1 test test 0 May 30 01:09 maps # 动态库在虚拟地址空间中的映射
-rw------- 1 test test 0 May 30 01:09 mem
-r--r--r-- 1 test test 0 May 30 01:09 mountinfo
-r--r--r-- 1 test test 0 May 30 01:09 mounts
-r-------- 1 test test 0 May 30 01:09 mountstats
dr-xr-xr-x 5 test test 0 May 30 01:09 net
dr-x--x--x 2 test test 0 May 30 01:09 ns
-r--r--r-- 1 test test 0 May 30 01:09 numa_maps
-rw-r--r-- 1 test test 0 May 30 01:09 oom_adj
-r--r--r-- 1 test test 0 May 30 01:09 oom_score
-rw-r--r-- 1 test test 0 May 30 01:09 oom_score_adj
-r--r--r-- 1 test test 0 May 30 01:09 pagemap
-r-------- 1 test test 0 May 30 01:09 patch_state
-r--r--r-- 1 test test 0 May 30 01:09 personality
-rw-r--r-- 1 test test 0 May 30 01:09 projid_map
lrwxrwxrwx 1 test test 0 May 30 01:09 root -> /
-rw-r--r-- 1 test test 0 May 30 01:09 sched
-r--r--r-- 1 test test 0 May 30 01:09 schedstat
-r--r--r-- 1 test test 0 May 30 01:09 sessionid
-rw-r--r-- 1 test test 0 May 30 01:09 setgroups
-r--r--r-- 1 test test 0 May 30 01:09 smaps
-r--r--r-- 1 test test 0 May 30 01:09 stack
-r--r--r-- 1 test test 0 May 30 01:08 stat # 记录了当前进程运行的状态:R、S、D、T、t、X、Z
#R(running) :进程处在运行队列,可以被cpu调度
#S(sleeping)D(disk sleep): 进程处在等待队列(挂起阻塞),等待条件满足后回到运行队列(唤醒)
#S状态可中断;
#t 调试时的状态
#D状态不可中断,处于被保护的状态,如给磁盘写入时不需要cpu资源,但是写入完成后进程需要获取是否成功,若此进程被中断则无法获得结果。
#T(stopped):
#Z(zombie):进程死亡前先进入僵尸状态,由操作系统检测死亡原因并写入进程控制块;Z状态长时间存在会造成内存泄漏
#X(dead):进程死亡,操作系统回收进程资源,释放内存空间
-r--r--r-- 1 test test 0 May 30 01:09 statm
-r--r--r-- 1 test test 0 May 30 01:08 status # 记录了umaskstatepidppiduidgid等信息
-r--r--r-- 1 test test 0 May 30 01:09 syscall
dr-xr-xr-x 3 test test 0 May 30 01:09 task
-r--r--r-- 1 test test 0 May 30 01:09 timers
-rw-r--r-- 1 test test 0 May 30 01:09 uid_map
-r--r--r-- 1 test test 0 May 30 01:09 wchan
-
我们可以将进程视为处于以下三种状态之一:
- 运行:进程要么在CPU上执行,要么等待执行,最终将被调度。
int main() { while(1); return 0; }[test@VM-12-4-centos first]$ ps axj | grep main # 此进程无IO操作,无需挂起等待,一直处在运行队列 13095 14424 14424 13095 pts/1 14424 R+ 1001 0:30 ./main
- 暂停挂起:进程的执行已挂起,将不会安排。进程因接收到SIGSTOP、SIGTSTP、SIGTTIN或SIGTTOU信号而停止,并保持停止状态,直到接收到SIGCONT信号,此时将再次运行。(信号是软件中断的一种形式)。当进程有IO操作时,进程可能大部时间处于S状态
int main() { while(1) printf("hello world"); //IO操作 return 0; }[test@VM-12-4-centos first]$ ps axj | grep main 13095 15938 15938 13095 pts/1 15938 S+ 1001 0:00 ./main [test@VM-12-4-centos first]$ ps axj | grep main # 发送19(SIGSTOP)号信号后进入 T状态 13095 16369 16369 13095 pts/1 13095 T 1001 0:00 ./main [test@VM-12-4-centos first]$ ps axj | grep main # 发送18(SIGCONT)号信号后进入 S状态 13095 16369 16369 13095 pts/1 13095 S 1001 0:00 ./main #此S不带+,后台运行,无法通过Ctrl+C中止
- 终止:进程将永久停止。进程因以下三个原因之一而终止:(1)接收到其默认操作是终止进程的信号;(2) 从主程序返回(return)(3)调用退出功能(exit)。父进程在运行,子进程异常退出,若父进程没有回收子进程的资源,则会出现僵尸进程。
- 每个进程都有一个唯一的正(非零)进程ID(PID)。getPID函数返回调用进程的PID。getppid函数返回其父进程(即创建调用进程的进程)的PID
- fork函数只调用了一次,但返回了两次:一次在调用进程(父进程)中,一次在新创建的子进程中。在父进程中,fork返回子进程的PID。在子进程中,fork返回值0(创建失败返回<0)。由于子进程的PID始终为非零,因此返回值提供了一种明确的方式来判断程序是在父进程还是在子进程中执行。
#include#include #include int main() { pid_t id = fork(); if(id == 0) printf("child pid:%d, ppid:%dn",getpid(),getppid()); else printf("father pid:%d, ppid:%dn",getpid(),getppid()); sleep(1); return 0; }
[test@VM-12-4-centos first]$ ./main # 在bash中运行的命令都是bash创建的子进程 father pid:18225, ppid:17072 # father的父进程是bash child pid:18226, ppid:18225 # 可以看到子进程的ppid正是父进程的pid [test@VM-12-4-centos first]$ ps axj | head -1 && ps axj | grep 17072 | grep -v 'grep' PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND 3901 17072 17072 17072 pts/8 18630 Ss 1001 0:00 -bash 17072 18630 18630 17072 pts/8 18630 R+ 1001 0:00 ps axj
-
新创建的子进程与父进程几乎相同,但并不完全相同。子进程获取父进程用户级虚拟地址空间的相同(但单独,进程具有独立性)副本,包括文本(程序)、数据和bss段、堆和用户堆栈。子进程还可以获得父进程打开的任何文件描述符的相同副本,这意味着子进程可以读取和写入调用fork时在父进程中打开的任何文件。
-
子进程写入时,子进程通过写时拷贝实现进程的独立性:不写时共享,写入时系统分配新的物理地址给子进程然后修改子进程的页表。例如子进程返回值与父进程不同时,则会单独创建status的存储内存。
#include#include int Var = 6; int main() { pid_t id = fork(); if(id > 0) { while(1) { Var = 10; printf("Father -> Var:%d, &Var:%pn", Var, &Var); sleep(1); } } else if(id == 0) { while(1) { sleep(1); //fork父子进程执行没有先后顺序,保证father先执行 printf("Child -> Var:%d, &Var:%pn", Var, &Var); } } else { printf("fork errorn"); } return 0; }
以上代码的输出结果为:
[test@VM-12-4-centos Fork]$ ./main Father -> Var:10, &Var:0x60104c Father -> Var:10, &Var:0x60104c Child -> Var:6, &Var:0x60104c
可以看到:父子进程在打印同一个全局变量时,由于子进程拷贝了父进程的虚拟地址空间的内容,因此对于全局变量Var他们有着相同的虚拟地址。但是当子进程修改Var时,操作系统重新在物理内存中分配了一段空间并修改了子进程的页表,将Var的虚拟地址重新映射到这个新的物理内存。所以结果是,他们打印出相同的地址,但是变量值却不同。
3. 地址空间- 地址空间的底部四分之三是为用户程序保留的,包括通常的文本、数据、堆和堆栈段。地址空间的顶部四分之一是为内核保留的,这部分地址空间包含内核所使用的代码、数据和堆栈,当应用程序执行系统调用时,会操作这一部分内存。
- 虚拟地址是地址空间上进行区域划分的起始位置。
- 用户在向操作系统申请空间时,系统会在地址空间上修改区域的起始位置(进而修改了空间),但是不会立即向物理地址中申请。当用户对这个空间作读写操作时,才会把这个空间开辟出来:基于缺页中断的物理内存申请。
- 使用地址空间实现了进程间的隔离,降低进程自身内存非法访问的概率;CPU可以将进程看成统一的4GB空间进行区域划分,可以通过页表连接在物理地址上不连续的内存;方便实现内存管理算法实现内存的最优占用。
-
进程退出的有三种方式:
- return返回: 只有main函数的return才会返回给操作系统;函数调用中的return会返回给main函数。
- exit(status): 退出码会直接返回给操作系统,无论是在函数中还是在main中。
- _exit(status)
int fun()
{
eixt(12);
}
int main()
{
fun();
return 0;
}
注意:return和eixt退出时,都会刷新C语言缓冲区的数据到内核。_exit则是强制终止,不会有刷新缓冲区等后续的操作
-
进程退出会出现三种情况:
- 程序运行完毕(正常终止)、结果正确 —— 一般退出码为0
- 程序运行完毕、结果不正确 —— 退出码非0
- 进程异常终止 —— 异常信号
在C语言库中,可以获取进程的退出码来得知进程退出的原因。
#include#include int main() { for(int i = 0; i < 140; i++) printf("%d : %sn", i, strerror(i)); return 0; }
[test@VM-12-4-centos first]$ ./main # 部分截取 0 : Success 1 : Operation not permitted 2 : No such file or directory 3 : No such process 4 : Interrupted system call 5 : Input/output error 6 : No such device or address 7 : Argument list too long 8 : Exec format error 9 : Bad file descriptor 10 : No child processes [test@VM-12-4-centos first]$ echo $? # 打印前一个进程退出的退出码 04.2 父进程等待子进程退出 4.2.1 waitpid
- 父进程创建子进程后,需要等待子进程的退出,等待有阻塞和非阻塞两种方式。
- 父进程等待子进程退出可以保证父进程在子进程之后退出,父进程可以获取子进程的退出信息;
- 若子进程退出,父进程不等待回收子进程资源,则会产生僵尸进程(无法通过kill -9 杀掉),造成内存泄漏。 —— 先于父进程退出的子进程程序已经不再运行,但是因为需要保存退出原因,因此资源没有完全释放;僵尸进程不会自动退出释放资源,也不会被kill命令再次杀死
若要回收子进程资源,使用函数waitpid()
pid_t waitpid(pid_t pid, int *status, int options);参数说明:
- pid_t pid: pid = -1:等待任意子进程 ; pid > 0:等待进程id与pid相等的子进程
- int *status: 进程返回的信息
- int options: WNOHANG:非阻塞等待;0默认行为阻塞等待
- 返回值:wait成功,返回那个退出子进程的PID;wait失败,返回-1;如果设置了WNOHANG和具体等待的pid进程,但是那个进程的状态还没有改变,则返回0(为0可以继续指向父进程的代码)。
#include#include #include #include int main() { pid_t id = fork(); if(id < 0){ //fork失败 printf("fork errorn"); exit(-1); } else if(id == 0) // 子进程 { printf("Child -> pid:%d, ppid:%dn", getpid(), getppid()); sleep(5); //子进程5秒后退出 exit(66); } //父进程 int count = 0; while(1) { printf("Father -> pid:%d, ppid:%dn", getpid(), getppid()); sleep(1); printf("%dn",count++); if(count > 5) //5秒后让父进程等待子进程退出 { pid_t ret = waitpid(id, NULL, 0); //阻塞等待 } } return 0; }
[test@VM-12-4-centos Zombie]$ ./main # 部分截取 Father -> pid:30814, ppid:3884 Child -> pid:30815, ppid:30814 0 Father -> pid:30814, ppid:3884 1 Father -> pid:30814, ppid:3884 # 运行脚本: while :;do ps axj | head -1 && ps axj | grep main |grep -v grep;echo -e "n"; sleep 1;done PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND 3884 30814 30814 3884 pts/5 30814 S+ 1001 0:00 ./main 30814 30815 30814 3884 pts/5 30814 S+ 1001 0:00 ./main PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND 3884 30814 30814 3884 pts/5 30814 S+ 1001 0:00 ./main 30814 30815 30814 3884 pts/5 30814 Z+ 1001 0:00 [main]4.2.2 退出状态status# 子进程变成了僵尸进程 PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND 3884 30814 30814 3884 pts/5 30814 S+ 1001 0:00 ./main # 子进程资源被回收 dead PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND 3884 30814 30814 3884 pts/5 30814 S+ 1001 0:00 ./main
status:32个bit,只使用了低16位,高位不用。获取退出码的过程由父进程完成。
#include#include #include #include int main() { pid_t id = fork(); int count = 0; int StatusInfo = 0; if(id > 0) { printf("Father -> pid:%d, ppid:%dn", getpid(), getppid()); sleep(1); pid_t ret = waitpid(id, &StatusInfo, 0); //不再执行,阻塞等待子进程结束 //0处设置WNOHANG选项,非阻塞等待 printf("nChild has been retrieved!n"); printf("Exit code of the child process:%dn", (StatusInfo>>8) & 0xFF); printf("Signals received by child processes:%dn", StatusInfo & 0x7F); if(WIFEXITED(StatusInfo)) //使用宏检测低7位,!(status&0x7F);判断进程是否为正常退出。1表示正常 { printf("WIFEXITED exit with:%dn", WEXITSTATUS(StatusInfo)); //获取退出码 } } else if(id == 0) { printf("Child -> pid:%d, ppid:%dn", getpid(), getppid()); sleep(100); //子进程100秒后退出 exit(30); //子进程退出码为30 } else printf("fork errorn"); return 0; }
[test@VM-12-4-centos status]$ ./main Father -> pid:26388, ppid:26121 Child -> pid:26389, ppid:26388 Child has been retrieved! Exit code of the child process:30 Signals received by child processes:0 WIFEXITED exit with:305. 其他 5.1 获取环境变量
- 环境变量具有全局属性,:父进程的环境变量可以被子进程继承。
# printf("Home: %sn",getenv("HOME"));
[test@VM-12-4-centos first]$ ./main
Home: /home/test
5.2 孤儿进程
- 父进程退出,子进程状态从S+ 变成 S。子进程会被1号进程领养,即其父进程(PPID)变为1(操作系统进程).
#include#include #include #include int main() { pid_t id = fork(); if(id == 0) while(1); //子进程死循环 sleep(1); exit(-1);// 父进程1秒后退出 return 0; }
[test@VM-12-4-centos first]$ while :;do ps axj | head -1 && ps axj | grep main |grep -v grep;echo -e "n"; sleep 1;done
PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND
13971 17285 17285 13971 pts/0 17285 S+ 1001 0:00 ./main
17285 17286 17285 13971 pts/0 17285 R+ 1001 0:00 ./main
PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND # 子进程被1号进程回收
1 17286 17285 13971 pts/0 13971 R 1001 0:01 ./main



