Linux进阶-加深进程印象

目录

进程

进程状态转换

进程状态

启动新进程

system()函数

system.c文件

Makefile文件

执行过程

fork()函数

函数原型

fork.c文件

Makefile文件

执行过程

exec系列函数

函数原型

execl.c文件

Makrfile文件

执行过程

终止进程

exit()函数和_exit()函数 

头文件和函数原型

等待进程

wait()函数

头文件和函数原型

wait.c文件

Makefile文件

执行过程

waitpid()函数

头文件和函数原型


进程

进程状态转换

一般来说,一个进程的开始都是从其父进程调用fork()函数开始,所以在系统一上电运行时,init进程就开始工作在系统运行过程中,会不断启动新的进程(要么由init进程启动,要么由被init进程启动的其他进程所启动)init进程的PCB是从内核的启动镜像文件中直接加载的,系统中的所有其他进程都是init进程的后代

一个进程被启动后都是处于可运行状态(但此时进程并未占用CPU运行),处于该状态的进程可以是正在进程等待队列中排队(就绪态),也可以占用CPU正在运行(运行态)。

系统产生进程调度时,处于就绪态的进程可以占用CPU的使用权,处于运行态。但每个进程运行时间是有限的(时间片),当进程的时间片已经耗光,如果进程还没有结束运行,那么会被系统重新放入等待队列中等待,处于就绪态,等待下一次进程的调度。另外,正处于运行态的进程即使时间片没有耗光也可能被别的更高优先级的进程抢占被迫重新回到等待队列中等待

处于运行态的进程可能会因为等待某些事件、信号或资源而进入可中断睡眠态(比如进程要读取一个管道文件数据而管道为空时,或进程要获得一个锁资源而当前锁不可获取时,甚至是进程自己调用sleep()函数来强制将自己进入睡眠等)。

可中断睡眠态:可以被中断的,能响应信号的睡眠状态。在特定条件发生后,进程状态就会转变为“就绪态”(比如其他进程向管道文件写入数据,或锁资源可以被获取了,或睡眠时间到达等)。

处于运行态的进程也可能会进入不可中断睡眠态,即进程不能响应信号。但这种状态非常短暂,我们几乎无法通过ps命令将其显示,一般处于这种状态的进程都是在等待输入或输出(I/O)完成,在等待完成后自动进入就绪态。

当进程收到SIGSTOP或SIGTSTP中的其中一个信号时,进程状态会被置为暂停态不再参与调度,但系统资源不会被释放直到收到SIGCONT信号后被重新置为就绪态。

当进程被追踪时(常见是使用调试器调试应用程序时)收到任何信号状态都会被置为TASK_TRACED状态,不再参与调度,但系统资源不会被释放直到收到SIGCONT信号后被重新置为就绪态。

进程在完成任务后会退出,那么此时进程状态为退出态(属于正常退出,如main()函数return,或调用exit()函数,或线程调用pthread_exit()函数)。

不正常退出时,那么此时进程状态为僵尸进程(如进程收到kill信号)。其实不管怎么死,内核都会调用do_exit()函数来使进程状态变为僵尸进程。

僵尸进程的僵尸指的是进程的进程控制块PCB。为什么一个进程死掉之后还要把PCB留下呢?因为进程在退出时,系统会将其退出信息都保存在PCB中(比如死亡原因),得以让父进程去排查(父进程之所以要启动该进程,很大原因是要让进程去干某一件事情,当该进程死亡,父进程当然要知道那一件事情办得怎样)

父进程去处理僵尸进程时,会将这个僵尸进程的状态设置为EXIT_DEAD,即退出态,系统才能去回收僵尸进程的内存空间,否则系统将存在越来越多的僵尸进程,最后导致系统内存不足而崩溃。

当父进程由于太忙而没能及时去处理僵尸进程时,可以考虑使用信号异步通知机制(让一个孩子在变成僵尸时给其父进程发一个信号,父进程接收到这个信号后再对其进行处理)。

当父进程先一步于子进程退出时,子进程将变成孤儿进程(没有父进程),孤儿进程将被祖先进程(init)收养。所以当孤儿进程退出时,init进程将回收资源。

进程状态

执行ps -ux可查出进程的状态。

状态说明
R

可运行状态。表示进程在运行队列中,处于正在运行或即将运行的状态。

只有在该状态才可能在CPU上运行,同一时刻可能有多个进程处于可运行状态

S可中断睡眠态。处于这个状态的进程可能因为等待某种事件的发生而被挂起,比如进程在等待信息
D不可中断睡眠态。通常是在等待输入或输出(I/O)完成,处于这种状态的进程不能响应异步信号
T停止态。通常是被Shell的工作信号控制,或因为处于调试器的控制下进程被追踪
Z退出态。进程成为僵尸进程
X退出态。进程即将被回收
s进程是会话其首进程
l进程是多线程的
+进程属于前台进程组
<高优先级任务

启动新进程

system()函数

简单,但效率低下而且具有不容忽视的完全风险。

system.c文件
#include <sys/types.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>

int main(void)
{
        pid_t result;
        result = system("ls -l");
        return result;
}

Makefile文件
ARCH?=x86
ifeq ($(ARCH), x86)
        CC = gcc
else
        CC = arm-linux-guneabihf-gcc
endif

TARGET=system
BUILD_DIR=build
SRC_DIR=module
INC_DIR=include
CFLAGS = $(patsubst %,-I %,$(INC_DIR))
INCLUDES = $(foreach dir, $(INC_DIR), $(wildcard $(dir)/*.h))

SOURCES = $(foreach dir, $(SRC_DIR), $(wildcard $(dir)/*.c))
OBJS = $(patsubst %.c, $(BUILD_DIR)/%.o, $(notdir $(SOURCES)))
VPATH = $(SRC_DIR)

$(BUILD_DIR)/$(TARGET):$(OBJS)
        $(CC) $(^) -o $(@)
$(BUILD_DIR)/%.o:%.c $(INCLUDE) | create_build
        $(CC) -c $< -o $@ $(CFLAGS)

.PHONY:clean create_build
clean:
        rm -r $(BUILD_DIR)
create_build:
        mkdir -p $(BUILD_DIR)

执行过程

fork()函数

复杂,但提供更好地弹性、效率和安全性。

fork()函数用于从一个已存在的进程(父进程)中启动一个新进程(子进程)。父进程的fork()调用返回的是新子进程的PID新子进程的fork()调用返回的是0

使用fork()函数的本质是将父进程的内容复制一份,但是有一些具体区别。

子进程与父进程一致的内容有:

进程的地址空间

进程上下文、代码段

进程堆、栈空间,内存信息

进程的环境变量

标志IO的缓冲区

打开的文件描述符

信号响应函数

当前工作路径

子进程独有的内容有:

进程号PID。

记录锁。父进程对某文件加了把锁,子进程不会继承这把锁。

挂起的信号。这些信号是已经响应但尚未处理的信号(悬挂的信号),子进程也不会继承这些信号。

因为子进程几乎是父进程的完全复制,所以父子进程会运行同一程序,但资源和时间都会消耗很大。

当发出fork()系统调用时,内核原样复制父进程的整个地址空间并把复制的那一份分配给子进程。这很耗时,因为需要做一些事情:

        为子进程的页表分配页面

        为子进程的页分配页面

        初始化子进程的页表

        把父进程的页复制到子进程相应的页中

创建一个地址空间的方法涉及许多内存访问,消耗许多CPU周期,并且完全破坏了高速缓存的内容,因此直接复制物理内存对系统的开销会产生很大的影响,更重要的是在大多数情况下,这样直接拷贝通常是毫无意义的,因为许多子进程通过装入一个新的程序开始它们的执行,这样就完全丢弃了所继承的地址空间。因此在Linux中引入一种写时复制技术(COW)

Linux系统中的进程都是使用虚拟内存地址,虚拟地址和真实物理地址之间是有一个对应关系的,每个进程都有自己的虚拟地址空间,而操作虚拟地址明显比直接操作物理内存更加便捷快捷,所以写时复制技术是一种可以推迟甚至避免复制数据的技术。内核此时并不复制整个进程的地址空间,而是让父子进程共享同一个地址空间(页面)。

写时复制的思路在于:父进程和子进程共享页面而不是复制页面。共享页面不能被修改,无论父进程和子进程何时试图向一个共享的页面写入内容时,都会产生一个错误,这时内核就把这个页复制到一个新的页面中并标记为可写。原来的页面仍然是写保护的,当还有进程试图写入时,内核会检查这个试图写入的进程是否是这个页面的唯一宿主,如果是则把这个页面标记为对这个进程是可写的。

总的来说,写时复制技术只会用在需要写入时才会复制地址空间,从而使各个进程进行拥有各自的地址空间在此之前父进程和子进程都是以只读方式共享页面,这种技术使地址空间上的页的拷贝被推迟到实际发生写入的时候。而在绝大多数时候共享页面根本不会写写入,例如在调用fork()函数后立即执行exec()函数,地址空间就无需被复制了,这样子fork()的实际开销就是复制父进程的页表和给子进程创建一个进程描述符。

函数原型
pid_t fork(void);

在fork()函数启动新进程后,子进程和父进程开始并发执行(谁先执行取决于内核调度算法决定)。

fork()函数如果启动新进程成功,会对父子进程各返回一次,对父进程返回子进程的PID,对子进程返回0;

fork()函数如果启动新进程失败,将返回-1。失败的原因通常是因为父进程所拥有的子进程数目超过了规定的限制(CFILD_MAX),此时errno变量将被设为EAGAIN。如果是因为进程表里没有足够的空间用于创建新的表单或虚拟内存不足,errno变量将被设为ENOMEM。

fork.c文件
#include <sys/types.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>

int main(void)
{
        pid_t result;
        result = fork();
        if(result == -1){
                printf("fork error!\n");
        }else if(result == 0){
                printf("the result value:%d, child process pid:%d\n", result, getpid());
        }else{
                printf("the result value:%d, father process pid:%d\n", result, getpid());
        }

        return result;
}

Makefile文件

照旧

执行过程

exec系列函数

事实上,使用fork()函数启动一个子进程是并没有太大作用的,因为子进程和父进程是一样的(子进程能干的活父进程也能干),因此就想让子进程做不一样的事情,于是诞生了exec系列函数,主要用于替换进程的执行程序

exec系列函数可以根据指定的文件名或目录名找到可执行文件(二进制文件,或可执行脚本文件),并用它来取代原调用进程的数据段、代码段和堆栈段,在执行完后原调用进程的内容除了PID外,其他全部被新程序的内容替换

简单来说,就是覆盖进程。如A进程通过exec系列函数启动B进程,此时B进程会替换A进程,A进程的内容空间、数据段、代码段等内容都将被进程B占用,然后进程A将不复存在。

函数原型
int execl(const char *path, const char *arg, ...);    // NULL结束
int execle(const char *path, const char *arg, ..., char *const envp[]);

int execlp(const char *file, const char *arg, ...);   // NULL结束
int execvp(const char *file, char *const argv[]);

int execv(const char *path, char *const argv[]);
int execve(const char *path, char *const argv[], char *const envp[]);

execl.c文件
#include <sys/types.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>

int main(void)
{
        int err;

        err = execl("/bin/ls", "ls", "-al", NULL);
        if(err < 0){
                printf("execl fail\n");
        }

        return 0;
}

Makrfile文件

照旧

执行过程

调用execl函数,这个函数在/bin/ls目录中搜索程序ls,然后它将会替换execl.c本身的进程。

当调用exec系列函数后,当前进程将不会再继续执行。一般情况下,exec系列函数是不会返回的,除非发生了错误。出现错误时,exec系列函数将返回-1,并且会设置错误变量errno。

终止进程

正常终止

        从main函数返回

        调用exit()函数终止

        调用_exit()函数终止

异常终止

        调用abort()函数异常终止

        由系统信号终止

exit()函数和_exit()函数 

在linux系统中,exit()函数定义在stdlib.h中,而_exit()定义在unistd.h中。当程序执行到exit()或_exit()函数时,进程会无条件地停止剩下的所有操作,清除包括PCB在内的各种数据结构,并终止当前进程的允许。

_exit()函数:简单,直接通过系统调用使进程终止运行,在终止进程时会清除这个进程使用的内存空间,并销毁它在内核中的各种数据结构。

exit()函数:在终止进程前会检查文件的打开情况,把文件缓冲区的内容写回文件。调用后会变成僵尸进程。

僵尸进程放弃了几乎所有的内存空间,没有任何可执行代码,也不能被调度,仅仅在进程列表中保留一个位置,记载该进程的退出状态等信息供其他进程收集。 

缓存IO操作:在Linux的标准函数库中使用,其特征就是对应每一个打开的文件,在内存中都有一片缓冲区。每次读文件时,会连续读出若干条记录,这样在下次读文件时就可以直接从内存的缓冲区中读取;同样,每次写文件的时候也仅仅是写入内存中的缓冲区,等满足了一定的条件(如达到一定数量或遇到特定字符等),再将缓冲区的内容一次性写入文件。

头文件和函数原型
#include <unistd.h>
void _exit(int status);

#include <stdlib.h>
void exit(int status);
// status表示进程终止时的状态码,0表示正常终止,其他非0值表示异常终止(一般使用-1或1)
// 标准C里使用EXIT_SUCCESS和EXIT_FAILURE宏表示正常终止和异常终止

等待进程

当父进程希望知道子进程何时结束或子进程结束的状态,甚至是等待子进程结束,可以调用wait()或者waitpid()函数让父进程等待子进程结束。

当调用exit()函数时,该进程变成僵尸进程,等待父进程回收该进程,因此调用wait()或waitpid()函数回收该进程,释放僵尸进程占用的内存空间,并且了解一下进程终止的状态信息。

wait()函数
头文件和函数原型
#include <sys/wait.h>
pid_t wait(int *wstatus);

wait()函数被调用时,系统将暂停父进程的执行,直到有信号到来或子进程结束。

如果在调用wait()函数时子进程已经结束,则会立即返回子进程结束状态值。子进程的结束状态信息会由参数wstatus返回,同时该函数会返回子进程的PID(通常是已经结束运行的子进程的PID)。如果不在意子进程的结束状态信息,则参数wstatus可以设为NULL。

wait()要与fork()配套出现,如果在使用fork()之前调用wait(),wait()的返回值则为-1,正常情况下wait()的返回值为子进程的PID。

wstatus参数可使用宏定义判断子进程退出状态:

        WIFEXITED:如果子进程正常结束,返回一个非零值

        WEXITSTATUS:如果WIFEXITED非零,返回子进程退出码

        WIFSIGNALED:子进程因为捕获信号而终止,返回非零值

        WTERMSIG:如果WIFSIGNALED非零,返回信号代码

        WIFSTOPPED:如果子进程被暂停,返回一个非零值

        WSTOPSIG:如果WIFSTOPPED非零,返回信号代码

wait.c文件
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>

int main(void)
{
        pid_t pid, child_pid;
        int status;

        pid = fork();
        if(pid < 0){
                printf("fork fail\n");
        }else if(pid == 0){
                printf("the child pid:%d\n", getpid());
                sleep(3);
                exit(0);
        }else{
                child_pid = wait(&status);
                if(child_pid == pid){
                        printf("the father process----the child pid:%d\n", child_pid);
                        printf("child exit status:%d\n", status);
                }else{
                        printf("some error occured\n");
                }

                exit(0);
        }

        return 0;
}

Makefile文件

照旧

执行过程

waitpid()函数
头文件和函数原型
#include <sys/wait.h>
pid_t waitpid(pid_t pid, int *wstatus, int options);
/* 
pid:要等待的子进程ID。
    < -1:等待进程组号为pid绝对值的任何子进程
    = -1:等待任何子进程,此时的waitpid()函数等同于wait()函数
    =  0:等待进程组号和目前进程相同的任何子进程,即等待任何与调用waitpid()函数且在同一个进程组的进程
    >  0:等待指定进程号为pid的子进程
wstatus:与wait()函数一样
options:提供了一些额外的选项来控制waitpid()函数的行为。如果不想用的话可以设为0
    WNOHANG:如果pid指定的子进程没有终止运行,则waitpid()函数立即返回0,而不是阻塞在这个函数上等待;如果子进程已经终止运行,则立即返回该子进程的进程号和状态信息
    WUNTRACED:如果子进程进入了暂停状态(可能子进程正处于被追踪等情况),则立马返回
    WCONTINUED:如果子进程恢复通过SIGCONT信号运行,也会立即返回(不常用)
*/

当waitpid(子进程pid, status, 0)时等同于wait(status)。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值