我终于又想起了我的博客密码(其实是之前一直没更新,也因为考试,也因为懒)这篇来总结一下最近看的关于进程的一些东西吧。
1. 概念
程序的一个执行实例,正在执行的程序等。即一个程序加载到内存中就变成了进程。
进程除了包含可执行代码比如代码段,还包含进程的一些活动信息和数据,比如用来存放函数变量、局部变量以及返回值的用户栈,存放进城相关数据的数据段,用于内核中进程间切换的内核段,以及用于动态内存分配的堆等信息。
进程是操作系统分配内存、CPU时间片等资源的基本单位。
CPU通过在进程间的快速切换,实现多进程并发执行,让每个进程都感觉自己在独占CPU,以此实现多线程并发执行。
2. 进程描述
进程信息被放在一个叫做进程控制块的数据结构中,进程属性的集合。称为PCB,Linux下的PCB为task struct。
PCB中主要描述如下几类信息:
- 标识符:描述本进程的唯一标识符,用来区别其他进程
- 状态:任务状态,退出代码,退出信号
- 程序计数器;程序中即将被执行的下一条指令的地址
- 内初指针:包括程序代码和进程相关数据的指针,还有和其他进程共享的内存块的指针
- 上下文数据:进程执行时处理器的寄存器中的数据
- 记账信息:可能包括处理器时间总和,使用的时钟总和,时间限制记账号等
- 其他信息
3.进程标识
进程创建时会分配唯一的号码标识,这个号码就是进程标识符PID(process ID)。进程信息可以通过 /proc 路径查看
- such as 查看为1的进程信息,即查看 /proc/1
那么怎么得到进程的PID呢?
答案就是:
- getpid()返回调用它的进程的PID
- getppid()返回调用它的进程的父进程的PID
#include <sys/types.h>
#include <unistd.h>
pid_t getpid(void);
pid_t getppid(void);
4.进程家族关系
-
Linux启动时会有一个init_task进程,它是所有进程的鼻祖。称为0号进程或者idle进程。当系统没有建成需要调度时,就会去调度idle进程。
-
系统块初始化完成时会创建一个init进程,就是常说的1号进程。它是所有进程的祖先,从这个进程开始所有进程都参与了调度。
-
如果A进程创建了B进程,那么A称为父进程,进程B称为子进程。如果B又创建了C,那么A与C就是祖孙关系进程。
-
如果A创建了B1,B2,B3……Bn,那么这些B进程间就称为兄弟进程。
5.创建子进程
通过fork()函数可以创建一个子进程。子进程会从父进程那里继承整个进程地址空间,他们共享相同的地址空间。但是子进程会拥有自己独有的PID
#include <unistd.h>
pid_t fork(void);
fork()函数有两次返回,一次是在父进程中,另一次是在子进程中。代码执行到fork()时,子进程就被创建了,这时父进程中的fork()会返回子进程的PID,而子进程中的fork()会返回0。
6.进程状态
- R运行状态(running): 它表明进程要么是在运行中要么在运行队列里。(并不一定在运行)
- S睡眠状态(sleeping):意味着进程在等待事件完成(这里的睡眠有时候也叫做可中断睡眠)。
- D磁盘休眠状态(Disk sleep):有时候也叫不可中断睡眠状态,在这个状态的进程通常会等待IO的结束。
- T停止状态(stopped): 可以通过发送 SIGSTOP 信号给进程来停止进程。这个被暂停的进程可以通过发送 SIGCONT信号让进程继续运行。
- X死亡状态(dead):这个状态只是一个返回状态,不会在任务列表里看到这个状态。
- 僵死状态(Zombies)是一个比较特殊的状态。当子进程退出并且父进程没有读取到子进程退出的返回代码时就会产生僵死(尸)进程。
- 僵死进程会以终止状态保持在进程表中,并且会一直在等待父进程读取退出状态代码。所以,只要子进程退出,父进程还在运行,但父进程没有读取子进程状态,子进程进入Z状态。
- 进程一旦变成僵死状态,kill -9 也无能为力,因为谁也没有办法杀死一个已经死去的进程。
使用下面这个命令来查看进程状态
ps aux / ps axj
- 孤儿进程
如果父进程先于子进程退出,则子进程将变为孤儿进程,此时Linux内核将让1号进程init领养它,此时init成为了他的父进程。
7.写时拷贝技术
写时拷贝技术是指在父进程创建子进程时,不需要复制进程地址空间的内容给子进程,只需要复制父进程的进程地址空间的页表给子进程,父子进程共享进程地址空间。都以只读的方式读取数据段,当任何一方修改物理空间中的内容时,再将共享的地址空间复制一份出来。各自拥有自己的副本。
由此fork()函数创建进程的开销变得更小,不需要复制父进程的所有资源。
8.进程终止
进程终止有被动和主动两种方式:
- 主动终止
从main函数中返回,链接程序会自动添加对exit()系统调用。调用return运行时,系统会将return返回的值作为exit函数的参数。
主动调用exit()系统调用。 - 被动终止
进程收到自己不能处理的信号
进程在内核态执行时产生 了一个异常
进程收到SIGKILL等终止信号(比如ctrl+c)
#include <unistd.h>
void exit(int status);//status定义进程的返回状态,父进程通过wait()函数来获取子进程的status
9.进程等待
父进程通过进程等待的方式,回收子进程资源,获取子进程退出信息(子进程的运行结果,是否正常退出等等)。还记得僵尸进程吗?僵尸的产生就是因为子进程退出了但是父进程没有退出,子进程就变成了僵尸进程。为了不让进程变成僵尸进程,我们需要及时调用wait()函数。
- wait的手册是这样写的:
#include <sys/types.h>
#include <sys/wait.h>
pid_t wait(int *status);
pid_t waitpid(pid_t pid, int *status, int options);
int waitid(idtype_t idtype, id_t id, siginfo_t *infop, int options);
返回值:
- 第一个wait():
成功时返回被等待的进程PID,失败返回-1。
参数status为子进程的返回状态。 - 第二个waitpid()
成功时返回收集到的子进程PID。
调用出错返回-1。
没有可收集的子进程返回0。
参数:
- pid:
pid = -1:等待任一子进程。pid为其他正值时等待该pid代表的子进程。(此时函数相当于wait() - status:
WIFEXITED(status): 若为正常终止子进程返回的状态,则为真。(查看进程是否是正常退出)
WEXITSTATUS(status): 若WIFEXITED非零,提取子进程退出码。(查看进程的退出码) - options:
WNOHANG: 若pid指定的子进程没有结束,则waitpid()函数返回0,不予以等待。若正常结束,则返回该子进程PID。
如果子进程已经退出,调用wait/waitpid时,wait/waitpid会立即返回,并且释放资源,获得子进程退出信息。 如果在任意时刻调用wait/waitpid,子进程存在且正常运行,则进程可能阻塞。(即父进程在wait()处等待子进程执行exit()函数退出) 如果不存在该子进程,则立即出错返回。
10.阻塞等待:
父进程已经调用了wait()函数,但是子进程仍然没有退出。就会导致父进程阻塞等待,父进程中wait()语句后面的代码会被阻塞,直至wait()读取到子进程返回值时,wait()后面的代码才会被执行。
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <string.h>
#include <stdlib.h>
#include <wait.h>
int main (){
pid_t pid;
pid = fork();
if(pid<0){
printf("%s fork error\n",__FUNCTION__);
return 1;
}else if (pid == 0){
printf("child is running,pid is : %d\n",getpid());
sleep(5);//子进程5s后才退出,让父进程阻塞等待5s
exit(257);
}else {
int status = 0;
pid_t ret = waitpid(-1,&status,0);
printf("this is test for wait\n");
if (WIFEXITED(status)&&ret == pid){
//WIFEXITED(status) 若此值为非0 表明进程正常结束
printf("wait child 5s success,child return code is :%d.\n",WEXITSTATUS(status));
//通过WEXITSTATUS(status)获取进程退出状态(exit时参数)
}else{
printf("wait child failed,return .\n");
return 1;
}
}
return 0;
}
如果将waitpid()函数中的参数设置为:
pid_t ret = waitpid(-1,&status,WNOHANG);
则父进程不会等待子进程直接执行wait()下面的代码。
11.进程替换
通过fork()得到的进程都是与父进程相同的进程,子进程中执行的代码中,有的是和父进程相同的。子进程会调用一类exec函数,来用新的程序取代子进程中的程序。
execve()函数负责读取可执行文件,并将其装入子进程的地址空间中开始运行。此时,父进程与子进程开始“恩断义绝”,父进程与子进程的联系就有可能到此为止了。
- 原来子进程中的程序代码、数据、堆栈都会被新的程序所覆盖。新程序不再返回源程序中。
- exec不会创建新的进程,进程的PID不变。
- 原来进程中程序打开的文件描述符都不会消失,会继续继承给新的程序。
- exec成功调用后不会返回,失败则返回-1。
#include <unistd.h>
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[]);
- l (list):表示参数采用列表
- v(vector) : 参数用数组
- p(path) : 有p自动搜索环境变量PATH
- e(env) : 表示自己维护环境变量
实际上,只有execve()函数是系统调用,其他5个函数最终都会调用exec()函数。