linux进程(不含进程间通信)扫盲

参考清华大学<<Linux编程>>的笔记,如有错误请帮忙指出

5.1 逻辑控制流与并发

逻辑控制流:一个核心抽象为一个PC,PC随着时间流逝而指向不同的指令的过程就是逻辑控制流.
并发:同一时间段内有多个流运行
并行:同一时间段内有多个流运行在多个核心上

5.2 进程的基本概念

宏观上看,进程就是进行中的程序的过程

5.2.1 进程概念'结构'描述

Linux将进程抽象为PCB,其管理进程的程序代码数据集,及许多属性,属性可分为:

  1. 进程描述信息
  2. 进程控制信息
  3. 进程资源信息
  4. CPU现场信息

5.2.2 进程的基本状态与状态转换

5种基本状态:

  1. 创建状态:准备必要的非CPU资源
  2. 就绪状态:已经准备好所有必要的非CPU资源
  3. 运行状态:具备就绪状态的所有条件,且已获得CPU
  4. 阻塞状态:等待额外的非CPU资源,这类资源成为了必要的非CPU资源的子集,此时线程主动放弃CPU
  5. 终止状态:线程终止后进入终止状态,不会立即清理

5.2.5 操作进程的工具

  1. ps
    1. ps -ef 查看所有进程信息,可配合| grep过滤
    2. ps l 查看当前用户的进程信息
    3. ps -u 查看当前用户的进程资源消耗
  2. kill
    1. 建议cli:kill -l 查看可用的信号
    2. kill -9 强制关闭进程
    3. kill -15 请求程序正常终止
    4. kill -SIGSTOP pid 挂起进程
    5. kill -SIGCONT pid 恢复进程

5.2.6 编程读取进程属性

概念:

  • pid:进程id
  • ppid:parent pid,父进程id
  • UID:用户id,分为
    • 创建者UID
    • 授权启动者EUID
  • GID:组id,分为
    • 创建者所属的组的GID
    • 授权启动者所属的组的EGID
      示例:
#include <sys/types.h>
#include <unistd.h>
pid_t getpid(void);
pid_t getppid(void);

uid_t getuid(void);
uid_t geteuid(void);
gid_t getgid(void);
uid_t getegid(void);

5.3 进程控制

5.3.1 创建进程

linux系统启动后,创建第一个进程init,其后的所有进程都是不断fork产生的

#include <sys/types.h>
#include <unistd.h>
pid_t fork(void);
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>
int main(){
    int x = 1;
    pid_t pid;

    pid = fork();
    if(pid==0);

}

结合上述代码剖析子进程创建过程`

  1. 父进程在执行fork前,准备好了数据集(父)
  2. 执行fork时,将本线程状态压入栈
  3. 执行fork中,系统复制PCB(父)作为子进程PCB并分配给子进程独立的PID,然后复制数据集(父)于子进程内存空间,然后创建数据集(子)
  4. 在fork执行后,因为PCB(子)完全复制了PCB(父),且PCB中有各种程序现场信息(包括压入栈的信息,如PC),所以下一步弹出栈到寄存器后,子进程的下一条指令与父进程相同,都是将fork的返回值进行返回,此时,linux规定:子进程的PID对自己来说是0,对父进程来说是上一步中独立的PID.然后继续执行代码...

编写多进程并发程序

  1. 画出程序框架
  2. 通过pid==0判断是否是子进程,进而执行对应task

5.3.2 多进程并发特征与执行流程分析

为控制多个进程中语句的执行顺序为非随机,需要使用linux操作系统提供的机制

5.3.3 进程终止与进程回收

1.进程终止
正常终止vs异常终止
exit(code)是正常终止
abort(code)是程序检出错误后主动异常终止
检出子进程的返回码:父进程执行waitpid()得到子进程终止状态,下面会详细说明如何使用
2.进程僵尸问题
僵尸进程:已经执行完毕(释放了大多数资源)但是仍有自己的PCB
清理工作:waitpid函数会读取子进程的PCB中的信息(退出状态,其他信息等),然后彻底清理子进程
3.回收进程(清理工作)
对于拥有子进程的父进程,使用waitpid函数来管理回收子进程

#include<sys/type.h>
#include<sys/wait.h>
pid_t waitpid(pid_t pid, int *status, int options);

pid_t wait(int* status);

waitpid(pid_t pid, int *status, int options)
返回值:正常情况是某个子进程pid,错误时返回-1(因无子进程而错误,会设置errno[一个宏]=ECHILD,因信号中断而错误,会设置errno=EINTR).
参数:
int* options;

  • 0:默认值,表示waitpid挂起调用进程的执行
    int* pid:
  • pid>0 : 等待一个指定的pid
  • pid == -1 : 等待所有的子进程
    • 此处有一个等待集合的概念,指的是要等待的进程有哪些,目的是方便管理子进程,假设A创建了A1,A2;A2又创建了B1,B2{此时,Ax构成了进程组,Bx构成了另一个进程组,还有一个由所有Ax和Bx共同构成的大进程组},那么A进程中waitpid的形参pid == -1就可以等待所有Ax和Bx,也能通过设置进程组的方式,传参给pid形参,来管理指定进程组

int* status:
status存储了子进程的返回的状态信息,使用特定的宏可以查询status
WIFEXITED(status):查询正常终止(exit或return)则返回真
WEXITSTATUS(status):获取正常终止时的返回的状态信息
WIFSIGALED(status):查询信号终止则返回真
WTERMSIG(status):获取导致子进程终止的信号编号

5.3.4 进程休眠

#include<unistd.h>
//返回值是0(正常返回)或剩余的秒数(被信号中断之后就会这样)
unsigned int sleep(unsigned int sec);
//微秒级别,秒,分秒,毫秒,微秒,纳秒
void usleep(unsigned int usec);
//让进程休眠until接到信号
int pause(void);

5.3.5 加载并运行程序

exec函数族执行过程:将系统分配的资源和数据集和程序的代码返还,但是保留PCB和pid,然后再为指定的程序分配资源,然后加载程序代码和数据集等

#include<unistd.h>
int execve(const char *fname, const char *argv[], const char *envp[]);
int execvp(const char *fname, const char *argv[]);
int execlp(const char *fle, const char *arg,...);

envp是环境变量的二级指针,一级指针指向一个含有真实字符串的数组,可在c程序中使用以下接口set,get环境变量

#include <stdlib.h>
char* getenv(const char* name);

int setenv(const char* name, const char* newvalue, int overwrite);

void unsetenv(const char *name);

5.3.7 fork和exec和dup

...

非本地跳转

常规的函数调用栈结构,使得我们在深层函数中出现错误需要调用最外层的处理机制时,要一层层解开调用栈,这显然复杂.非本地跳转的核心思想是使用setjmp函数在env 缓冲区(在 C 语言中,env 缓冲区通常指的是用于存储环境变量的内存区域。)中存储当前调用环境(包括PC,栈指针,通用目的寄存器),然后在需要的地方使用longjmp函数将最近一次setjmp保存的'调用环境'还原.
函数签名

#include <setjmp.h>
int setjmp(jmp_buf env);
int sigsetjmp(sigjmp_buf env, int savesigs);

int longjmp(jmp_buf env, int retval);
int siglongjmp(sigjmp_buf env, int savesigs);
  • setjmp的执行会保存调用环境到env,然后返回0;
    1. longjmp的执行会将env还原到调用环境
    1. 并会触发最后初始化同一env的setjmp函数的函数调用返回部分,这个调用返回会携带longjmp中设置的retval.
  • 上述第2部分的调用是必须的,仅仅还原现场信息只能调整pc,但是我们还需要把retval返回给接口的用户,不然让用户自己在内存中找retval的位置可太危险了.

5.4 信号机制

向进程传入信号,kill指令就可以.
在CLI中键入 man 7 signal可以查看linux系统支持的信号
进程受到信号后就需要处理之,默认的处理方式一般有2种:忽略和终止
名词解释:

  • 捕获信号:进程接收到信号后,如果该信号在进程中注册了对应编号的信号,则会捕获之,然后调用程序员自己安装在程序中的对应了该信号的处理函数
  • 忽略信号:进程不予处理
  • 终止信号:进入终止态(还记得吗,进程终止后仍有自己的pcb,资源被没有被100%回收)

5.4.2 信号的有关术语

传送一个信号到目的进程有两个步骤组成:

  • 发送信号: 内核update进程里的某个状态,发送信号的原因有2个
      1. 内核内部检测到了系统事件(事件是一种由内核记录并由内核传播的数据结构)
      1. 进程之间通过kill函数传递信号
  • 接收信号: 即响应信号.进程接收到信号之后可以表现为以下三种行为:捕获并处理,忽略和终止.
  • 待处理信号: 发出但是没被接收的信号.每一种类型信号只存在一个信号实体,多余的信号实体会被丢弃.

5.4.3 发送信号的方法

进程组的概念中实现了进程发信号的相关机制

1.进程组:
进程组是n个进程的集合,通常处理一组作业.一个进程只属于一个进程组,这不难理解,因为一个进程组绑定一组作业,本进程组的进程一般无权干涉其他作业组,只需要处理自己的作业组.
接口:

#include<unistd.h>
pid_t getpgrp(void);//当前进程的进程组id
int setpgid(pid_t pid, pid_t pgid);

setpgid是设置pgid的接口将pid进程的进程组设为pgid.逻辑上是先有进程再有进程组.当pid0时,指的是调用setpgid的(当前)线程;当pgid0时,指的是以pid所指代的进程为父进程的同时新开一个进程组.
2./bin/killCLI命令发送信号
3.键盘发送信号(如ctrl+c)
4.kill函数和raise函数发信号
kill向其他线程(组)发信号.raise向进程自己发信号

#include<sys/types.h>
#include<signal.h>
int kill(pid_t pid, int sig);
int raise(int sig);

上述,若pid>0,则是向pid进程发信号,若pid<0,则是向abs(pid)进程组发信号
5.alarm函数发信号
unsigned int alarm(unsigned int seconds);
向内核设置内核在sec秒后,向调用进程发送SIGALRM信号.相当于时钟定时.这个虚拟的闹钟每次至多设置一次有效定时,因为设置之后都会取消之前的定时.
若sec为0,则清除任何待处理的时钟,返回先前设置的闹钟的剩余时间.

5.4.4 接收信号的过程

当从内核态返回到进程的用户态时,内核会检查进程的待处理信号集合,通常会选择其中最小的信号号码,然后将该信号传输给该进程,进而该进程会做出某种行为A(如进入信号处理函数)(,如果在信号处理函数中再次陷入内核态,那么从内核态返回到用户态时不再执行传输待处理信号给进程的行为,因为此时进程的状态属于信号尚未处理完成的状态,如果贸然再次传递信号,就可能导致原先的信号处理过程丢失这是不允许的),A行为完成后,执行进程的下一条指令.
我们知道,进程在接收到信号后有2个默认行为:忽略或终止,还有其他信号,能够让进程执行不同行为,一般默认行为都能更改,除了SIGSTOP和SIGKILL.
通过以下接口能设置自定义处理的信号和对应的处理器句柄

#include<signal.h>
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);

signal函数中会使用回调函数用法,为signum调用形参handler,同时:

  • 若handler是SIG_IGN,则忽略之
  • 若handler是SIG_DFL,则重置之
    序号 信号名称 信号说明 默认处理方式
    1 SIGHUP 终端控制进程终止或者控制终端关闭 终止进程
    2 SIGINT interrupt 信号,通常由 Ctrl+C 产生 终止进程
    3 SIGQUIT 退出信号,通常由 Ctrl+\ 产生 生成核心转储并终止进程
    4 SIGILL 非法指令信号,进程执行了非法的指令 生成核心转储并终止进程
    5 SIGTRAP 跟踪/断点异常信号 生成核心转储并终止进程
    6 SIGABRT 进程调用 abort() 函数产生的信号 生成核心转储并终止进程
    7 SIGEMT 协处理器使用异常信号 终止进程
    8 SIGFPE 算术异常信号,如被零除或溢出 生成核心转储并终止进程
    9 SIGKILL 终止进程信号,此信号不能被忽略或阻塞 终止进程
    10 SIGBUS 总线错误信号,如访问未对齐的内存地址 生成核心转储并终止进程
    11 SIGSEGV 段错误信号,进程访问非法内存地址 生成核心转储并终止进程
    12 SIGSYS 无效的系统调用 生成核心转储并终止进程
    13 SIGPIPE 写一个没有读端的管道或socket 终止进程
    14 SIGALRM 定时器超时信号 终止进程
    15 SIGTERM 进程终止信号 终止进程
    16 SIGURG 紧急条件信号,通常用于套接字 忽略
    17 SIGSTOP 停止进程信号,此信号不能被忽略或阻塞 停止进程
    18 SIGTSTP 停止或暂停进程信号,通常由用户输入Ctrl+Z产生 停止进程
    19 SIGCONT 继续执行停止的进程信号 忽略
    20 SIGCHLD 子进程状态改变信号 忽略
    21 SIGTTIN 后台进程读终端信号 停止进程
    22 SIGTTOU 后台进程写终端信号 停止进程
    23 SIGPOLL 轮询式 I/O 信号 终止进程
    24 SIGXCPU 超过CPU时间限制信号 生成核心转储并终止进程
    25 SIGXFSZ 超过文件大小限制信号 终止进程
    26 SIGVTALRM 虚拟时钟信号 终止进程
    27 SIGPROF Profileing 时钟信号 终止进程
    28 SIGWINCH 窗口大小改变信号 忽略
    29 SIGINFO status request from keyboard 忽略
    30 SIGUSR1 用户自定义信号1 终止进程
    31 SIGUSR2 用户自定义信号2 终止进程
    32 SIGSYS 无效的系统调用 生成核心转储并终止进程

5.4.6 可移植信号处理

linux系统会重启被中断的系统调用,所谓系统调用就是内核态才能执行的指令
不同系统对于同一个信号的处理有不同语义,一个较好的方式是使用posix定义的统一的信号处理接口

#include <signal.h>
int sigaction(int signum, struct sigaction *act, struct sigaction *oldact);

但是该接口不常用,<<Linux编程>>中包装了该接口为以下接口,它满足

  • 只有当前被处理的信号类型被阻塞
  • 信号不会排队等待
  • 尽量重启被中断的系统调用
handler_t* Signal(int signum,handler_t* handler)
  struct sigaction action,old_action;

  action.sa_handler = handler;
  sigemptyset(&action.sa_mask);
  action.sa_flags=SA_RESTART;

  if(sigaction(signum,&action,&old_action)<0)
    perror("Signal error");

  return(old_action.sa_handler);

5.4.7 信号处理引起的竞争问题

这属于并发问题,通常是逻辑执行顺序与指令时间执行顺序不一致导致的冲突,这会发生在信号上,毕竟信号是突发的.一种解决策略是使用信号掩码技术在需要逻辑控制的地方阻塞相关信号,这种策略在汇编语言中也经常使用.
进程概念中有一个blocked位向量,表示阻塞信号集合,sigmask的接口操作这个阻塞信号集合.
以下是sigmask的接口介绍

#include<signal.h>
int sigprocmask(int how, const sigset_t *set, sigset_t* oldset);

how包括以下选项

  • SIG_BLOCK:将set+到block向量中
  • SIG_UNBLOCK:将set从block向量中移除
  • SIG_SETMASK:将block设置为mask
    oldset是保存以前的block位向量的值
    还有其他函数,功能如其名
int sigemptyset(sigset_t* set);
int sigfillset(sigset_t* set);
int sigaddset(sigset_t* set, int signum);
int sigdelset(sigset_t* set, int signum);
int sigismember(sigset_t* set, int signum);

5.5 守护进程

一些基础功能的提供者,不过这些功能是特供给本进程组的.类似于一个驻场角色.
创建守护进程的步骤如下:

  1. fork创建新进程
  2. 脱离terminal,session,进程组
  3. 关闭打开的文件描述符
  4. 改变当前工作目录
  5. 处理文件描述符012
#include

5.6 进程,系统调用与内核

进程是为了实现任务而分配的资源,其对系统资源的访问是受限的,在进程当中的操作被成为用户态
系统调用是内核为进程使用内核权限而抽象的接口
内核管理计算机的所有资源,拥有最高的权限,在内核当中的操作被称为用户态

待续...

posted @   WangChangAn  阅读(5)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 单线程的Redis速度为什么快?
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 展开说说关于C#中ORM框架的用法!
点击右上角即可分享
微信分享提示