进程是一个正在运行的程序。进程控制块(PCB,Process Control Block)是进程存在的唯一标识符,用来描述进程的属性。进程状态:就绪、执行和阻塞。
并行和并发的区别:并行是指在同一时刻,能够同时执行多个进程,单核CPU每个时刻只能执行一个进程,要想实现并行需要多核CPU;并发是指在某一时间段,能够同时执行多个进程,单核CPU每个时刻只能执行一个进程,多个进程需要通过进程切换(调度算法),并发执行。
在Linux下printf函数并不会直接将数据打印到屏幕上,而是先放在缓冲区,满足以下情况之一,才会打印到屏幕上。
三种退出方式的区别
return //语言应用,关键字,结束当前函数调用,会刷新缓冲区
exit //库函数,进程退出,内部调用了_exit,会刷新缓冲区
_exit //系统调用,内核级别,结束进程,但不会刷新缓冲区
int main(int argc, char* argv[], char* envp[])
argc 记录参数个数
argv 记录参数内容
envp 记录环境变量,继承当前终端
函数声明
pid_t fork(void); //pid_t 本质上是 int 类型
fork函数会生成一个新的进程,调用fork函数的进程为父进程,新生成的进程为子进程,父子进程并发执行。在父进程中fork函数返回子进程的pid, 在子进程中返回0,失败返回-1。子进程是父进程的一个副本,二者如果不发生写入的话几乎一模一样。
从结果可以看出,父进程pid=222131,子进程pid=222132,父子进程中打印的变量n的地址是相同的,这是在进行进程复制的时候,将父进程的堆栈段复制到子进程的堆栈段,所以两个变量n的逻辑地址是相同的,但由于两个进程的实际存储空间不同,所以两个变量n的实际存储地址是不同的,也就是两个变量n具有相同的逻辑地址,具有不同的物理地址。从上图中也可以看出父子进程确实是并发执行。
传统的 fork() 系统调用直接把父进程的所有资源复制给子进程,这是一种简单却低效的方法。目前 Linux 的 fork() 使用的是写时拷贝(页)技术。写时拷贝是一种可以推迟甚至免除拷贝数据的技术。内核此时并不复制整个进程地址空间,而是让父子进程共享同一个拷贝。只有在需要写入的时候,数据才会被复制,从而使各个进程拥有自己的拷贝。 资源的复制只有在需要写入(父子进程任意一个写入)的时候才进行,在此之前,只是以只读方式共享。只有进程空间的各段内容发生改变时,才会将父进程的内容复制一份给子进程。 若没有写入,两个进程用的是相同的物理空间(内存),子进程的代码段、数据段、堆栈段都是指向父进程的物理空间,也就是说,两者的虚拟空间不同,但物理空间是相同的的。当父子进程中有更改相应段的行为发生时,再给子进程分配相应的物理空间。未发生写入时如图
习题1
#include
#include
int main(){
printf("A\n");
fork();
return 0;
}
//输出1个A
习题2
#include
#include
int main(){
fork();
printf("A\n");
return 0;
}
//输出2个A
习题3
#include
#include
int main(){
printf("A");
fork();
return 0;
}
//输出2个A
习题4
#include
#include
int main(){
for(int i = 0; i < 2; i++){
fork();
printf("A\n");
}
return 0;
}
//第一次for循环:父进程产生子进程1,父进程和子进程1分别输出一个A
//第二次for循环:父进程产生子进程2,子进程1产生子进程3,父进程、子进程1子进程2、子进程3分别输出一个A
//一共输出6个A
习题5
#include
#include
int main(){
for(int i = 0; i < 2; i++){
fork();
printf("A");
}
return 0;
}
//第一次for循环:父进程产生子进程1,然后父进程将一个A放入自己的输出缓冲区,子进程1将一个A放入自己的输出缓冲区
//第二次for循环:父进程产生子进程2,子进程1产生子进程3,子进程2复制父进程的输出缓冲区,子进程3复制子进程1的输出缓冲区
//此时,父进程输出缓冲区一个A,子进程1输出缓冲区一个A,子进程2输出缓冲区一个A,子进程3输出缓冲区一个A
//然后,每个进程再往自己的输出缓冲区中加入一个A
//一共输出8个A
习题6
#include
#include
int main(){
fork() || fork();
printf("A\n");
return 0;
}
//父进程执行前面的fork产生子进程1,父进程不执行后面的fork
//子进程1执行后面的fork产生子进程2
//一共3个进程,总共输出3个A
子进程先于父进程结束,而父进程没有回收子进程,释放子进程占用的资源,此时子进程将成为一个僵尸进程。如果父进程先结束,子进程成为孤儿进程,此时由 init 接管,当子进程结束后由 init 回收其占用的相关资源。 Linux 中,一个进程结束了,若它的父进程没有等待它(调用 wait/waitpid),那么它就会成为僵尸进程。若该进程的父进程先结束,那么该进程就不会变成僵尸进程。因为每个进程结束时,系统会扫描当前系统中所运行的所有进程,看是否还存在该进程的子进程未结束,如果存在,那么就由 init 接管成为该子进程的父进程。
僵尸进程的危害就是占用进程号。每个进程退出的时候,内核释放该进程所有资源,包括打开的文件、占用的内存等。但仍然会为其保留一定的信息(包括进程号,退出状态,运行时间等),直到父进程通过 wait 或 waitpid 来获取时才释放。这就导致了如果父进程不调用 wait 或 waitpid,那么保留的那段信息就不会被释放,其进程号就会一直被占用,但系统能使用的进程号是有限的,如果产生大量的僵尸进程,将会因为没有可用的进程号导致系统不能产生新的进程。
产生僵尸进程示例
//process.c
#include
#include
#include
#include
#include
int main(int argc, char* argv[], char* envp[]){
char* s = NULL;
int n = 0;
pid_t pid = fork();
assert(pid != -1);
if(pid == 0){
s = "child";
n = 3;
}
else{
s = "parent";
n = 8;
}
for(int i = 0; i < n; i++){
printf("pid=%d, ppid=%d, s=%s\n", getpid(), getppid(), s);
sleep(1);
}
exit(0);
}
让程序在后台运行,并不断敲入 ps 命令查看进程状态
从上图中可以看出,子进程 pid=239273,父进程 pid=239272,当子进程输出三次运行结束,而此时父进程未结束,子进程并没有消失,可以看到子进程有
在父进程中调用 wait 或 waitpid 函数,以解决僵尸进程的问题。
//process.c
#include
#include
#include
#include
#include
#include
int main(){
char *s = NULL;
int n = 0;
pid_t pid = fork();
assert(pid != -1);
if(pid == 0){
s = "child";
n = 3;
}
else{
s = "parent";
n = 8;
int val = 0;
int id = wait(&val);
if(WIFEXITED(val)){
printf("id=%d, exit_val=%d\n", id, WEXITSTATUS(val));
}
}
for(int i = 0; i < n; i++){
printf("pid=%d, ppid=%d, s=%s\n", getpid(), getppid(), s);
sleep(1);
}
exit(3);
}
//wait函数成功返回子进程的pid,失败返回-1,val是传出的参数
//WIFEXITED(status)用来指出子进程是否为正常退出,如果正常退出会返回一个非零值
//WEXITSTATUS(status),当WIFEXITED(status)返回非零值时,可以用WEXITSTATUS(status)来提取子进程的返回值,如果子进程用exit(3)退出,则WEXITSTATUS(status)就会返回3
//如果进程不是正常退出,也就是WIFEXITED(status)返回0,WEXITSTATUS(status)的返回值就毫无意义
从上图可以看出,父进程挂起,子进程运行直至结束,父进程获取子进程的退出码,子进程消失,没有变成僵尸进程,最后父进程结束。
最后看一下孤儿进程,孤儿进程的产生是因为父进程先于子进程结束,此时子进程将由 init 进程接管,子进程结束后会由 init 回收。
//process.c
#include
#include
#include
#include
#include
int main(){
char* s = NULL;
int n = 0;
pid_t pid = fork();
assert(pid != -1);
if(pid == 0){
s = "child";
n = 8;
}
else{
s = "parent";
n = 3;
}
for(int i = 0; i < n; i++){
printf("pid=%d, ppid=%d, s=%s\n", getpid(), getppid(), s);
sleep(1);
}
exit(0);
}
从上图可以看出,当父进程结束后,子进程的父进程变成了 1528,按理来说 init 进程是内核启动的第一个用户级进程,既然孤儿进程由 init 进程接管,理应父进程的 pid=1。这是因为图形化界面是一个伪终端,shell 其实是 init 进程的一个子进程,所以孤儿进程其实是被 shell 收养,如果切换到字符化界面运行程序,孤儿进程的父进程 pid 将会变成1。通过命令 ps -ef 查看 pid=1528 的父进程的 pid=1