0
点赞
收藏
分享

微信扫一扫

Linux学习与记录(2)

静守幸福 2022-03-12 阅读 68

程序与进程

程序是存放在介质上的一个可执行文件。
进程是程序执行的过程,是动态变化的,是管理事务的基本单元。
在计算机中时间中断是多道程序设计的理论基础(时间片轮转法),分时复用CPU资源

并行:同一时刻多条指令再多个处理上同时执行。
并发:同一时刻只能有一条指令执行,但多个进程指令被快速的轮转执行。

MMU:内存管理单元。CPU管理虚拟存储器、物理存储器的控制线路,负责虚拟地址映射为五哦里地址。提供硬件机制的内存访问授权。

PCB:进程控制块。进程运行时,内核为进程的每一个进程分配一个PCB,维护进程相关的信息,是task_struct结构体。

vim /usr/src/linux-headers-5.4.0-100-generic/include/linux/sched.h

在这里插入图片描述
内部成员很多,需要掌握:

进程id。系统中每个进程有唯一的id,在C语言中用pid_t类型表示,其实就是一个非负整数。
进程的状态,有就绪、运行、挂起、停止等状态。
进程切换时需要保存和恢复的一些CPU寄存器。
描述虚拟地址空间的信息。
描述控制终端的信息。
当前工作目录(Current Working Directory)。 umask掩码。
文件描述符表,包含很多指向file结构体的指针。
和信号相关的信息。
用户id和组id。
会话(Session)和进程组。
进程可以使用的资源上限(Resource Limit)。

三态:运行态,就绪态,阻塞态。
五态:新建态,终止态,运行态,就绪态,阻塞态。
在这里插入图片描述
①TASK_RUNNING:进程正在被CPU执行。当一个进程刚被创建时会处于TASK_RUNNABLE,表示己经准备就绪,正等待被调度。
②TASK_INTERRUPTIBLE(可中断):进程正在睡眠(也就是说它被阻塞)等待某些条件的达成。一旦这些条件达成,内核就会把进程状态设置为运行。处于此状态的进程也会因为接收到信号而提前被唤醒。
给一个TASK_INTERRUPTIBLE状态的进程发送SIGKILL信号,这个进程将先被唤醒(进入TASK_RUNNABLE状态),然后再响应SIGKILL信号而退出(变为TASK_ZOMBIE状态),并不会从TASK_INTERRUPTIBLE状态直接退出。
③TASK_UNINTERRUPTIBLE(不可中断):处于等待中的进程,待资源满足时被唤醒,但不可以由其它进程通过信号或中断唤醒。由于不接受外来的任何信号,因此无法用kill杀掉这些处于该状态的进程。而TASK_UNINTERRUPTIBLE状态存在的意义就在于,内核的某些处理流程是不能被打断的。
ps命令基本上不可能捕捉到。
④TASK_ZOMBIE(僵死):表示进程已经结束了,但是其父进程还没有调用wait()或waitpid()来释放进程描述符。为了父进程能够获知它的消息,子进程的进程描述符仍然被保留着。一旦父进程调用了wait(),进程描述符就会被释放。
⑤TASK_STOPPED(停止):进程停止执行。当进程接收到SIGSTOP,SIGTSTP,SIGTTIN,SIGTTOU等信号的时候。此外,在调试期间接收到任何信号,都会使进程进入这种状态。当接收到SIGCONT信号,会重新回到TASK_RUNNABLE。

查看进程

ps aux
ps -ef

在这里插入图片描述
查看当前终端所有进程,可以通过下面指令查看异常

ps -a

查看进程运行,实时刷新

top

在这里插入图片描述
占用终端

sleep 3000

占用后台

sleep 3000 &

在这里插入图片描述
kill语句
-9强制结束

kill -9 [PID]

killall 通过进程名字杀死进程
杀死所有名为sleep的进程

killall -9 sleep

进程号,范围0-32768,类型pid_t(无符号整型)
进程号(PID)
父进程号(PPID)
进程号组(PGID)

getpid函数

#include <sys/types.h>
#include <unistd.h>
//这个函数不会返回失败
pid_t getpid(void);
功能:
    获取本进程号(PID)
参数:
    无
返回值:
    本进程号

getppid

#include <sys/types.h>
#include <unistd.h>
​
pid_t getppid(void);
功能:
    获取调用此函数的进程的父进程号(PPID)
参数:
    无
返回值:
    调用此函数的进程的父进程号(PPID)

在这里插入图片描述
在这里插入图片描述

进程的创建
不断创建会形成进程树结构

#include <sys/types.h>
#include <unistd.h>
​
pid_t fork(void);
功能:
    用于从一个已存在的进程中创建一个新进程,新进程称为子进程,原进程称为父进程。
参数:
    无
返回值:
    成功:子进程中返回 0,父进程中返回子进程 ID。pid_t,为整型。
    失败:返回-1。
    失败的两个主要原因是:
        1)当前的进程数已经达到了系统规定的上限,这时 errno 的值被设置为 EAGAIN。
        2)系统内存不足,这时 errno 的值被设置为 ENOMEM。

父子进程关系
使用fork()函数得到子进程,继承了进程的地址空间:进程上下文,进程堆栈,文件描述符,信号控制设定,进程优先级,进程组号。
子进程独有的:进程号,计时器。
在这里插入图片描述
程序计数器指向fork()之后的语句(PC指针)。
Linux 的 fork() 使用是通过 写时拷贝,读时共享 实现。写时拷贝是一种可以推迟甚至避免拷贝数据的技术。

fork之后父子进程共享文件,fork产生的子进程与父进程相同的文件文件描述符指向相同的文件表,引用计数增加,共享文件文件偏移指针

fork() 函数被调用一次,但返回两次。两次返回的区别是:子进程的返回值是 0,而父进程的返回值则是新子进程的进程 ID。在 fork() 之后是父进程先执行还是子进程先执行是不确定的。这取决于内核所使用的调度算法。
在这里插入图片描述
在进程中使用malloc时,分配一次,但需要释放两次。

判断内存泄漏的工具:

valgrind ./a.out

在这里插入图片描述

退出进程

exit(0);

显示当前用户下所有进程:ps aux
在这里插入图片描述
以比较完整的格式显示所有的进程:ps ajx
在这里插入图片描述
杀死进程:

kill -SIGKILL/(-9) 89899【进程标识号】

查看kill

kill -l(字母)

GDB调试进程
使用gdb调试的时候,gdb只能跟踪一个进程。可以在fork函数调用之前,通过指令设置gdb调试工具跟踪父进程或者是跟踪子进程。默认跟踪父进程。

set follow-fork-mode child 设置gdb在fork之后跟踪子进程。
set follow-fork-mode parent 设置跟踪父进程(默认)。

注意,一定要在fork函数调用之前设置才有效。

进程退出

#include <stdlib.h>
void exit(int status);#include <unistd.h>
void _exit(int status);
功能:
    结束调用此函数的进程。
参数:
    status:返回给父进程的参数(低 8 位有效),至于这个参数是多少根据需要来填写。
返回值:
    无

exit() 和 _exit() 函数功能和用法是一样的,无非时所包含的头文件不一样,还有的区别就是:_exit()属于标准库函数,exit()属于系统调用函数。_exit()会刷新缓冲区域。注意:return也会刷新缓冲区域。_exit()等价于_Exit()。
在这里插入图片描述
等待子进程退出函数
wait和waitpid
在每个进程退出的时候,内核释放该进程所有的资源、包括打开的文件、占用的内存等。但是仍然为其保留一定的信息,这些信息主要主要指进程控制块PCB的信息(包括进程号、退出状态、运行时间等)。

父进程可以通过调用wait或waitpid得到它的退出状态同时彻底清除掉这个进程。
wait() 和 waitpid() 函数的功能一样,区别在于,wait() 函数会阻塞,waitpid() 可以设置不阻塞,waitpid() 还可以指定等待哪个子进程结束。

注意:一次wait或waitpid调用只能清理一个子进程,清理多个子进程应使用循环。

#include <sys/types.h>
#include <sys/wait.h>
​
pid_t wait(int *status);
功能:
    等待任意一个子进程结束,如果任意一个子进程结束了,此函数会回收该子进程的资源。
参数:
    status : 进程退出时的状态信息。
返回值:
    成功:已经结束子进程的进程号
    失败: -1

调用 wait() 函数的进程会挂起(阻塞),直到它的一个子进程退出或收到一个不能被忽视的信号时才被唤醒(相当于继续往下执行)。

若调用进程没有子进程,该函数立即返回;若它的子进程已经结束,该函数同样会立即返回,并且会回收那个早已结束进程的资源。

所以,wait()函数的主要功能为回收已经结束子进程的资源。
在这里插入图片描述
宏函数可分为如下三组:

  1. WIFEXITED(status)
    为非0 → 进程正常结束
    WEXITSTATUS(status)
    如上宏为真,使用此宏 → 获取进程退出状态 (exit的参数)

  2. WIFSIGNALED(status)
    为非0 → 进程异常终止
    WTERMSIG(status)
    如上宏为真,使用此宏 → 取得使进程终止的那个信号的编号。

  3. WIFSTOPPED(status)
    为非0 → 进程处于暂停状态
    WSTOPSIG(status)
    如上宏为真,使用此宏 → 取得使进程暂停的那个信号的编号。
    WIFCONTINUED(status)
    为真 → 进程暂停后已经继续运行

在这里插入图片描述
在这里插入图片描述
当子进程exit(10)之后,父进程ret=wait(&statue),这时候会将10->statue进行赋值,并且返回ret,然后再通过宏函数来判断是否正常退出,并且可以输出10。

在这里插入图片描述
在这里插入图片描述
kill默认是15号信号
在这里插入图片描述
-19 暂停, -18 恢复

#include <sys/types.h>
#include <sys/wait.h>
​
pid_t waitpid(pid_t pid, int *status, int options);
功能:
    等待子进程终止,如果子进程终止了,此函数会回收子进程的资源。
​
参数:
    pid : 参数 pid 的值有以下几种类型:
      pid > 0  等待进程 ID 等于 pid 的子进程。
      pid = 0  等待同一个进程组中的任何子进程,如果子进程已经加入了别的进程组,waitpid 不会等待它。
      pid = -1 等待任一子进程,此时 waitpid 和 wait 作用一样。
      pid < -1 等待指定进程组中的任何子进程,这个进程组的 ID 等于 pid 的绝对值。
​
    status : 进程退出时的状态信息。和 wait() 用法一样。
​
    options : options 提供了一些额外的选项来控制 waitpid()。
            0:同 wait(),阻塞父进程,等待子进程退出。
            WNOHANG:没有任何已经结束的子进程,则立即返回。
            WUNTRACED:如果子进程暂停了则此函数马上返回,并且不予以理会子进程的结束状态。(由于涉及到一些跟踪调试方面的知识,加之极少用到)
                 
返回值:
    waitpid() 的返回值比 wait() 稍微复杂一些,一共有 3 种情况:
        1) 当正常返回的时候,waitpid() 返回收集到的已经回收子进程的进程号;
        2) 如果设置了选项 WNOHANG,而调用中 waitpid() 发现没有已退出的子进程可等待,则返回 0;
        3) 如果调用中出错,则返回-1,这时 errno 会被设置成相应的值以指示错误所在,如:当 pid 所对应的子进程不存在,或此进程存在,但不是调用进程的子进程,waitpid() 就会出错返回,这时 errno 被设置为 ECHILD;
​
ret = waitpid(-1, &statue, 0);// 等价于wait
ret = waitpid(-1, &statue, WNOHANG);// 表示不阻塞

孤儿进程
父进程运行结束,但子进程还在运行(未运行结束)的子进程就称为孤儿进程(Orphan Process)。
每当出现一个孤儿进程的时候,内核就把孤儿进程的父进程设置为 init ,而 init 进程会循环地 wait() 它的已经退出的子进程。这样,当一个孤儿进程凄凉地结束了其生命周期的时候,init 进程就会代表党和政府出面处理它的一切善后工作。
因此孤儿进程并不会有什么危害。

僵尸进程
进程终止,父进程尚未回收,子进程残留资源(PCB)存放于内核中,变成僵尸(Zombie)进程。
这样就会导致一个问题,如果进程不调用wait() 或 waitpid() 的话, 那么保留的那段信息就不会释放,其进程号就会一直被占用,但是系统所能使用的进程号是有限的,如果大量的产生僵尸进程,将因为没有可用的进程号而导致系统不能产生新的进程,此即为僵尸进程的危害,应当避免。

init 6 重启
init 0 关机

查看僵尸进程

ps aux | grep z

进程替换
而在 Linux 平台,我们可以通过 ./ 运行,让一个可执行程序成为一个进程。
exec 函数族,顾名思义,就是一簇函数,在 Linux 中,并不存在 exec() 函数,exec 指的是一组函数,一共有 6 个:

#include <unistd.h>
extern char **environ;
​
int execl(const char *path, const char *arg, .../* (char  *) NULL */);
int execlp(const char *file, const char *arg, ... /* (char  *) NULL */);
int execle(const char *path, const char *arg, .../*, (char *) NULL, char * const envp[] */);
int execv(const char *path, char *const argv[]);
int execvp(const char *file, char *const argv[]);
int execvpe(const char *file, char *const argv[], char *const envp[]);
​
int execve(const char *filename, char *const argv[], char *const envp[]);
char *const argv[] //指针数组

其中只有 execve() 是真正意义上的系统调用,其它都是在此基础上经过包装的库函数。

exec 函数族的作用是根据指定的文件名或目录名找到可执行文件,并用它来取代调用进程的内容,换句话说,就是在调用进程内部执行一个可执行文件。
只做替换!!!
进程调用一种 exec 函数时,该进程完全由新程序替换,而新程序则从其 main 函数开始执行。因为调用 exec 并不创建新进程,所以前后的进程 ID (当然还有父进程号、进程组号、当前工作目录……)并未改变。exec 只是用另一个新程序替换了当前进程的正文、数据、堆和栈段(进程替换)
在这里插入图片描述
在这里插入图片描述
我们发现第二句printf没有输出,这是因为当发生进程替换时,把整个进程空间都进行替换,所以原来进程的所有操作都将失效。进程数据被全部替换之后,又会从新的main()主函数开始执行。除非替换进程执行失败。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

举报

相关推荐

0 条评论