Linux多进程开发I

1. 程序 vs 进程
  • 程序时包含一系列信息的文件,这些文件描时了如何在运行时创建一个进程。(二进制格式标识、机器语言指令、程序入口地址、数据、符号表和重定位表、共享库和动态链接信息)
  • 进程是正在运行的程序的实例,是一个具有一定独立功能的程序关于某个数据集合的一次运行活动,是操作系统动态执行的基本单元,资源分配的基本单元。
2. 单道程序 vs 多道程序
  • 单道程序:在计算机内存中只允许一个程序运行。
  • 多道程序:在计算机内存中同时存放几道相互独立的程序,在管理程序控制下,相互穿插运行,这些程序共享计算机系统资源。宏观上而言,这些程序看起来像是同时运行的,微观上任意时刻CPU上只有一个程序在运行。(CPU执行指令是纳秒级的,人眼反应速度是毫秒级的)
3. 时间片(处理器片),操作系统分配给每个正在运行的进程微观上的一段CPU时间。由操作系统内核的调度程序分配给每个进程。 4. Linux进程调度的算法有哪些? 5. 并行 vs 并发
  • 并行:在同一时刻,有多条指令在同个处理器上同时执行。
  • 并发:在同一时刻,一个处理器上只能有一条指令执行,但多个进程被快速轮换执行,使得在宏观上具有多个进程同时执行的效果。但在微观上,只是把时间分成若干段,使多个进程快速交替执行。
Linux多进程开发I 6. 同一时刻多个用户访问服务器,怎么处理?并发问题。 7. ulimit -a 内核中的资源上限 8.进程的状态。
  • 运行态:进程占有处理器正在运行。
  • 就绪态:进程具备运行条件,已分配到除CPU之外的所有资源,等待系统分配处理器以后即可运行(被调度运行)。
  • 阻塞态:进程不具备运行条件,正在等待某个事件的完成。
  • 新建态:进程刚被创建,尚未分配资源和进入就绪队列。
  • 终止态:进程完成任务,或出现无法克服的错误而异常终止,或被其他有终止权的进程终止。进入终止态的进程不再执行,但依然保留在操作系统中,等其他进程未完成对终止态进程的信息抽取后,操作系统将该进程删除。
Linux多进程开发I 9. 进程相关命令。
# 查看进程
ps -aux/ajx
-a  显示所有进程
-u  显示详细信息
-x  显示没有控制终端的进程
-j  列出与作业控制相关的信息
 
# 实时显示进程动态
top -d 更新时间
在top命令执行后可以按键对显示的结果排序:
M 内存
P CPU
T 进程运行时长
U 用户名
K 杀死指定PID进程
 
# 杀死进程
kill 进程pid
kill -9 进程pid       # 强制杀死进程
kill -l              # 列出所有信号
killall 进程name      # 根据进程名杀死进程

  

10. 进程由进程标识号(PID)表示。任何继承(除init进程)都是由另一个进程创建,该进程称为被创建进程的父进程,对应的进程号为父进程号(PPID)。 11. 进程组是一个或多个进程的集合。它们之间相互关联,进程组可以接收同一个终端的各种信号,关联的进程有一个进程组号(PGID)。 12. 创建进程。
  • 进程树结构模型。一个进程可以创建新的进程,新进程即为子进程,该子进程也可以创建新的子进程。
  • fork()之后,除了内核区的进程pid不一样之外,子进程的虚拟地址空间与父进程的虚拟地址空间内容完全一样。
  • 子进程只会从fork()函数的指令开始执行。后续可以根据fork()函数的返回值判断在父进程和子进程执行不同的代码。
#include <sys/types>
#include <unistd.h>
pid_t fork(void);
    - 作用:用于创建子进程
    - 返回值:fork()返回值会返回两次。一次是在父进程中,一次是在子进程中。
             在父进程中返回创建的子进程的ID,在子进程中返回0。
             在父进程中返回-1表示创建子进程失败,并设置errno。
?如何区分父进程和子进程:通过fork()的返回值。
 
pid_t getpid(void);
pid_t getppid(void);

  

13. fork()原理。
  • 实际上,Linux的fork()函数使用是通过写时拷贝(copy-on-write)实现。写时拷贝是一种可以推迟甚至避免拷贝数据的技术。此时并不复制整个进程的地址空间,而是让父子进程共享同一个地址空间。只在需要写入时复制地址空间,从而使各个进程拥有各自的地址空间。即,资源的复制是在需要写入时进行的,在此之前是以只读方式共享。fork之后父子进程共享文件,fork产生的子进程与父进程相同的文件的文件描述符指向相同的文件表,引用计数增加,共享文件偏移指针。
14.父子进程之间的关系。
  • 区别:1.fork()函数的返回值不同,父进程中>0返回子进程的ID,子进程中返回0;2.内核的PCB中的一些数据不同,当前进程的ID,PID,当前进程的父进程的ID,PPID,信号集;
  • 共同点:1.子进程刚被创建出来,还没有执行任何写数据操作时,用户区的数据和文件描述符表是一样的;
  • 父子进程对变量是否共享?子进程刚创建的时候是共享的,如果某些变量的值被修改了则不会继续共享该变量了。(写时拷贝)
15. GDB多进程调试。GDB默认只能跟踪一个进程,可以在fork函数调用之前通过指令设置跟踪父进程还是子进程。
# 设置跟踪父进程还是子进程
set fllow-fork-mode [parent/child]
 
# 设置调试模式
# on表示调试当前进程时,其他进程继续运行,off表示调试当前程序时其他进程被GDB挂起
set detach-on-fork [on/off]
 
# 查看调试的进程
info inferiors
 
# 切换当前调试的进程
inferior id
 
# 使进程脱离GDB调试
detach inferiors id

  

16.exec函数族。
  • 根据指定的文件名找到可执行文件,并用它来取代调用进程的内容。即,在调用进程内部执行一个可执行文件。
  • 找到的可执行文件的用户区替换原进程的用户区(代码段、数据段、堆栈等),内核区的信息(进程ID等)并没有变,并重新从新的可执行文件的main函数处开始执行。且原进程在调用exec()函授后的代码都不会被执行了。
  • 所有一般都会使用fork创建一个子进程,然后在子进程中调用exec()函数,使原进程不受影响。
Linux多进程开发I
#include <unistd.h>
int execl(const char *path, const char *arg, ...);    
    - path: 需要指定的执行的文件的路径或名称
    - arg: 执行可执行文件所需要的参数列表,第一个参数为执行程序的名称,第二个参数为程序执行所需要的参数列表,参数最后以NULL结束
    - 返回值:只有调用失败才有返回值,返回-1并设置errno
 
int execlp(const char *file, const char *arg, ...);
    - 作用:会到环境变量中查找指定的可执行文件,如果找到就执行,找不到就执行不成功
    - file:需要执行的可执行文件的文件名
    - exec("ps", "aux", NULL)
 
int execle(const char *path, const char *arg, ...);
    - 指定环境变量数组,e.g.: char *envp[] = {"/home/aaa", "/home/bbb", "/bin/ps"};
    - exec("/bin/ps", "aux", NULL, envp);
 
int execv(const char *path, char *const argv[]);
    - 将可执行文件的参数保存在数组中,e.g.: char *argv[] = {"ps", "aux", "NULL"};
    - exec("/bin/ps", 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[]);
 
l(list)            参数地址列表,空指针结尾
v(vector)          存有个参数地址的指针数组的地址
p(path)            按path环境变量指定的目录搜索可执行文件
e(environment)     存有环境变量字符串地址的指针数组的地址
 
18.进程控制。
子进程退出可以释放用户区的数据,但是其内核区的数据需要其父进程回收释放。
# 进程退出
#include <stdlib.h>
void exit(int status);
    - 会调用退出处理函数,并刷新IO缓冲并关闭文件描述符后再调用Unix系统函数_exit();
    - status: 进程退出时的一个状态信息。父进程回收子进程资源的时候可以获取到。 
 
#include <unistd.h>
void _exit(int status);
    - 不会刷新缓冲区,被C库函数exit()调用。

  

Linux多进程开发I   19.孤儿进程。
  • 父进程运行结束,但子进程还在运行。即没有父进程。
  • 每当出现一个孤儿进程时,内核会把孤儿进程的父进程设置为init,而init进程(pid=1)会循环wait()它的已经退出的子进程。
  • 孤儿进程没有危害。
  20.僵尸进程。
  • 每个进程结束后,都会释放自己地址空间中的用户区数据,内核区的PCB没有办法自己释放掉,需要父进程释放。
  • 进程终止时,父进程尚未回收,子进程残留资源存放于内核中,变成僵尸进程。
  • 僵尸进程不能被 kill -9 杀死。
  • 危害:如果父进程没有回收子进程内核区资源的话,那么该子进程的进程号就会一直被占用,但是系统所能使用的进程号是有限,如果产生大量僵尸进程,则导致系统不能产生新的进程。
  21.进程回收。
  • 在每个进程退出的时候,内核释放该进程所有的资源、包括打开的文件、占用 的内存等,但是仍保留一定的信息,主要指进程控制快PCB的信息(包括进程号、退出状态、运行时间等)。
  • 父进程可以通过wait()或者waitpid()得到子进程的退出状态并同时彻底清除掉这个进程。
  • 一次wait或waitpid调用只能清除一个子进程。
 
#include <sys/types.h>
#include <sys/wait.h>
pid_t wait(int *wstatus);
    - 作用:等待任意一个子进程结束,如果任意一个子进程结束,此函数会回收该子进程资源。
    - wstatus: 进程退出时的状态信息,传出参数
    - 返回值:成功返回被回收的子进程id;失败(所有子进程都结束、调用函数失败)返回-1
调用wait函数的进程会被挂起(阻塞),直到它的一个子进程退出或者收到一个不能被忽略的信号时才被唤醒(相当于继续往下执行),
如果没有子进程了,函数会立刻返回-1;如果子进程都结束了,也会立即返回-1。
 
# 推出信息相关宏函数
WIFEXITED(status)           非0,进程正常退出
WEXITSTATUS(status)         如果上宏为真,获取进程退出的状态,exit()的参数
WIFSIGNALED(status)         非0,进程异常终止
WTERMSIG(status)            如果上宏为真,获取使进程终止的信号编号
WIFSTOPPED(status)          非0,进程处于暂停状态
WSTOPSIG(status)            如果上宏为真,获取使进程暂停的信号的编号
WIFCOTINUED(status)         非0,进程暂停后已经继续运行
 
# e.g.:
int st;
int ret = wait(&st);
if(ret == -1) break;
if(WIFEXITED(st)) {
    printf("退出的状态码: %d\n", WEXITSTATUS(st));
}
if(WIFSIGNALED(st)) {
    printf("被哪个信号干掉了:%d\n", WTERMSIG(st));
}
printf("child die, pid = %d\n", ret);
 
 
pid_t waitpid(pid_t pid, int *wstatus, int options);
    - 作用:回收指定进程号的子进程,可以设置是否阻塞
    - pid: 1.pid>0,某个子进程的pid;2.pid=0,回收当前进程组的所有子进程;3.pid=-1,回收任一子进程,相当于wait();4.pid<-1,回收指定进程组(pid的绝对值)中的子进程。
    - options: 设置阻塞或者非阻塞,0:阻塞;WNOHANG:非阻塞。
    - 返回值:>0 返回子进程的id,=0 如果设置options=WNOHANG,表示没有子进程的状态改变;=-1 错误。

  

       
上一篇:2021/08/07 模拟笔试复盘


下一篇:开发手札:git日常抽风记录