系统相关
首页 > 系统相关> > 6.进程概述

6.进程概述

作者:互联网

进程

进程就是“运行中的程序”。程序是存储于外存上的静态实体,如果从来没有运行,程序没有任何意义;程序(实际上是部分程序)被放到内存运行,这时它成为了一个活动实体,成为进程。操作系统会为每一个进程分配一个数据结构,称之为进程控制块(PCB),用于进程相关信息的存储,以及用于进程调度。在操作系统出现之前,程序直接运行于裸机之上。那时硬件资源的利用率不高,程序编写要事无巨细亲历亲为,更别提对硬件资源的保护和安全性。操作系统为程序的运行提供了大量便利,可以说,操作系统就是为了运行程序而存在,即为了进程而存在。因此,进程是操作系统中首屈一指的概念。

ps命令

ps(process status/snapshot)命令用于显示系统中进程的当前状态的快照。

当然,此时只显示前台的进程。如果想查看全部进程,需要使用-e选项。

此时显示的进程过多,可能需要用more分页。

-l选项可以查看更多细节,也常常同-e选项合用。

image-20211204161441034

分别介绍一下每一列的含义:

以上的用法是System V的形式。另外,ps命令还支持BSD系统的命令形式,其中最重要的命令形式就是

ps aux

运行效果如下

image-20211204161617182

top命令

除了ps,top命令也是最常使用的监视进程的命令,它更像windows中的任务管理器。在终端输入

top
image-20211204204853186

/proc文件系统

Linux和大部分的Unix系统都支持/proc文件系统。这是个伪文件系统,其中的文件实际上是内核内存及其数据结构的视图(view)。

ls /proc
image-20211204205041656

/proc下的以数字编号的目录,都是以对应进程的PID命名。这样,每个进程的信息就通过目录分隔开来。

/proc文件系统中包含的每个进程的信息可以使用ps等命令进行查看。由于大部分文件都是文本文件,所以也可以直接用文本编辑器打开。

vi /proc/3798/status

可以查看进程3798的状态。

此外,也可以通过

vi /proc/cpuinfo

查看CPU的信息

image-20211204205339778

strace命令

使用strace命令可以实时显示一个进程所有的系统调用。这使得一个进程的行为一览无余。也可以通过-p选项strace一个已经运行的进程。

image-20211204205843086

strace命令的很多选项都非常有用。比如,-f选项用于跟踪被fork出的进程,可用于监视守护进程(daemon)比如httpd-e file选项只显示文件操作。

进程创建

进程从哪里来?

“进程从哪里来?”这一问题与以下问题相似:“小猫从哪里来?小狗从哪里来?”猫是猫妈妈生的;狗是狗妈妈生的;进程是进程它妈生的——它叫parent process。如果存在进程间的这种创建与被创建的关系,分别称之为parent process(父进程)child process(子进程)

fork()

一个进程可以通过系统调用fork()创建子进程。fork的意思是分叉,一个变成两个。

例:使用fork()创建子进程

#include <stdio.h>

main(){
	printf("I am %d\n", getpid());
	fork();
	printf("I am %d\n", getpid());
}

getpid()可以得到该进程自己的pid。该程序运行结果为:

image-20211204210218352

很显然,程序最开始执行时,只有一个进程3450。而在fork()之后,除了最开始的3450之外,还出现了一个新的进程3451。

在传统的Unix模型中,当一个进程调用fork()后,会创建一个新的进程,这个新的进程称为原来进程的子进程,此时原来的进程称为父进程。子进程会复制父进程的一切,包括代码段、变量、打开文件的文件描述符等等——当然,由于Linux采用写时拷贝(copy on write),所以子进程并不是真的把父进程完全拷贝了。但对于程序员来说,逻辑上可以认为是子进程是父进程的完全复制,甚至是IP寄存器,即无论父子进程,都会从fork()的下一条指令开始执行。父子进程唯一不同的地方,就是fork()的返回值——父进程的fork()返回的是子进程的pid;而子进程未曾调用过fork,形式上得到的返回值为0。因此,我们可以通过fork()的返回值来判断当前进程是父进程还是子进程,尽管它们拥有相同的代码。

例:通过fork()的返回值来区分父子进程。

#include <stdio.h>
#include <stdlib.h>

main(){
	int rv;
	printf("I am parent, I'm %d\n", getpid());
	if((rv=fork())==-1){
		perror("cannot fork");
		exit(1);
	}
	if(0==rv){/*child*/
		printf("I am child, I'm %d\n", getpid());
	}else{/*parent*/
		printf("I am parent, I'm %d, my child is %d\n", getpid(),rv);

	}
}

运行结果:

image-20211204210539105

多进程编程与之前的经验的两点显著不同:

其一,父进程和子进程的代码都写到了同一个程序里。但一般都是父进程运行父进程的代码,子进程运行子进程的代码,互不干扰。这有点像合同,甲乙双方签订同一份合同,合同里分别包含甲和乙的权责,甲和乙分别履行自己的权责,而不会履行对方的权责。

其二,当子进程被创建后,父子进程彼此独立,同时运行,共同竞争操作系统资源。此时,如没有特殊的编程限定,父子进程执行的先后顺序取决于操作系统的进程调度。因而,多进程并发执行时,其运行结果对于程序员来说,似乎是随机的(但事实上对于一个给定的系统,调度策略是固定的,运行结果往往是一定的)。这就导致了多进程编程的不确定性,诸如进程同步等问题都可能接踵而来。

进程如何运行一个程序?

exec函数族是一系列以exec开头的库函数,exec代表execute a file——执行一个可执行文件。比较常用的是execvpexeclp

例:使用execlp运行ls /。

#include <stdio.h>
#include <unistd.h>

main(){
    printf("about to ls\n");
    execlp("ls","ls","/",NULL);
    printf("OK\n");
}

运行结果:

image-20211204211051703

通过execlp执行了ls /这一程序,结果也无误。但问题是我们注意到程序中最后一行printf("OK\n");并没有被执行。

原因很简单,当我们试图运行ls程序时,这时候我们需要生成一个进程包括进程所拥有的内存等资源去加载ls程序的指令,但很显然exec函数并没有创建新进程的能力,而不得不将自己代码替换为ls的代码。因此,当ls /开始执行时,最后的printf("OK\n");和其他的代码都已不复存在了。

解决方案是,在exec之前先fork,让子进程去exec。这样我们才能保留父进程的代码。

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

main(){
    int rv;
    printf("About to ls\n");
    if((rv=fork())==-1){
        perror("cannot fork");
        exit(1);
    }
    if(0==rv){/*child*/
        execlp("ls","ls","/",NULL);
    }else{/*parent*/
        printf("OK\n");
    }
}

运行结果:

image-20211204211404603

我们注意到,尽管“OK”已经被打印出来了,但我的期望是先“ls”,再打印“OK”。这是我们需要父子进程间的协作。wait()系统调用可以让父进程一直等待,直到子进程运行结束。

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

main(){
    int rv;
    printf("About to ls\n");
    if((rv=fork())==-1){
        perror("cannot fork");
        exit(1);
    }
    if(0==rv){/*child*/
        execlp("ls","ls","/",NULL);
    }else{/*parent*/
        wait(NULL);
        printf("OK\n");
    }
}

运行结果:

image-20211204211554021

wait()

wait()系统调用除了等待子进程结束之外,它还能释放子进程资源——主要是指子进程的退出状态。C99规定主函数main的返回值必须是int类型,就是说main函数一定要return一个整数。这个整数就是进程的退出状态,是一个8 bit的无符号整数,其取值范围是0~255。也可以通过exit()函数将这个退出状态值返回。当一个进程运行结束后,其退出状态会一直被保留,此时的进程成为了一种称为僵尸进程(zombie)的状态,直到它的父进程用wait()系统调用查看该进程或父进程运行结束。另外,子进程也可能被信号杀死,父进程也可以通过wait()得知是哪个信号杀死子进程的。

当子进程退出时(或被信号杀死)父进程之所以能够立即知道,是因为在子进程退出时,父进程会接收到SIGCHLD信号。

先看一下man 2 wait

image-20211204211958128
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

main(){
    int rv;
    int status;
    if((rv=fork())==-1){
        perror("cannot fork");
        exit(1);
    }
    if(0==rv){/*child*/
        sleep(10);
        exit(10);
    }else{/*parent*/
        wait(&status);
        printf("%d\t%d\n", status>>8, status&0x7F); 
    }
}

运行结果:

第一次运行:我们用信号9杀死子进程,父进程能够打印出该信号数,由于子进程没有运行结束就被杀死,它的exit value是0。

image-20211205084425179

第二次运行:我们没有杀死子进程,等子进程通过exit(10)运行结束,父进程可以得到其exit value。

image-20211205084513468

僵尸进程

首先编译运行以下代码:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

main(){
    int rv;
    int status;
    if((rv=fork())==-1){
        perror("cannot fork");
        exit(1);
    }
    if(0==rv){/*child*/
        exit(10);
    }else{/*parent*/
        sleep(100);
    }
}

运行结果:

image-20211205084742600

子进程运行结束后,大部分资源都会被释放,但exit value会保存下来,此时子进程成为了一个僵尸进程(zombie),直到它的父进程用wait()系统调用查看该进程或父进程运行结束才能消失。在进行多进程编程时,类似于httpd(Apache HTTP Server)这种程序,如果不对僵尸进程进行处理,很可能就会出现大量的僵尸进程。

由于当子进程退出时(或被信号杀死),父进程会接收到SIGCHLD信号。当我们设置父进程忽略该信号时,则系统不会保留僵尸进程,则这时不再会出现僵尸进程了。

例:通过忽略SIGCHLD消除僵尸进程

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>

main(){
    int rv;
    int status;
    if((rv=fork())==-1){
        perror("cannot fork");
        exit(1);
    }
    if(0==rv){/*child*/
        exit(10);
    }else{/*parent*/
        signal(SIGCHLD, SIG_IGN);
        sleep(100);
    }
}

运行结果:

image-20211205085129112

此时子进程直接退出,而不再作为僵尸进程存在了。

孤儿进程

编译运行下面的程序:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

main(){
    int rv;
    int status;
    if((rv=fork())==-1){
        perror("cannot fork");
        exit(1);
    }
    if(0==rv){/*child*/
        sleep(100);
    }else{/*parent*/
        exit(0);
    }
}

运行结果:

image-20211205085640130

任何一个进程都必须有它的父进程。示例程序父进程先于子进程结束。此时进程12954成为了一个孤儿进程,此时1号进程(即init进程)会接收孤儿进程,并负责wait()这些孤儿进程。

标签:fork,rv,概述,ls,进程,include,运行
来源: https://blog.csdn.net/AIbeichen/article/details/122749649