0%

Linux进程(二)

进程的状态

  • Linux 系统下进程通常存在 6 种不同的状态,分为:就绪态、运行态、僵尸态、可中断睡眠状态(浅度 睡眠)、不可中断睡眠状态(深度睡眠)以及暂停态。
    • 就绪态(Ready):指该进程满足被 CPU 调度的所有条件但此时并没有被调度执行,只要得到 CPU 就能够直接运行;意味着该进程已经准备好被 CPU 执行,当一个进程的时间片到达,操作系统调 度程序会从就绪态链表中调度一个进程;
    • 运行态:指该进程当前正在被 CPU 调度运行,处于就绪态的进程得到 CPU 调度就会进入运行态;
    • 僵尸态:僵尸态进程其实指的就是僵尸进程,指该进程已经结束、但其父进程还未给它“收尸”;
    • 可中断睡眠状态:可中断睡眠也称为浅度睡眠,表示睡的不够“死”,还可以被唤醒,一般来说可 以通过信号来唤醒;
    • 不可中断睡眠状态:不可中断睡眠称为深度睡眠,深度睡眠无法被信号唤醒,只能等待相应的条件 成立才能结束睡眠状态。把浅度睡眠和深度睡眠统称为等待态(或者叫阻塞态),表示进程处于一 种等待状态,等待某种条件成立之后便会进入到就绪态;所以,处于等待态的进程是无法参与进程 系统调度的。
    • 暂停态:暂停并不是进程的终止,表示进程暂停运行,一般可通过信号将进程暂停,譬如 SIGSTOP 信号;处于暂停态的进程是可以恢复进入到就绪态的,譬如收到 SIGCONT 信号。

image-20220121212212609

进程组

  • 每个进程除了有一个进程 ID、父进程 ID 之外,还有一个进程组 ID,用于标识该进程属于哪一个进程 组,进程组是一个或多个进程的集合,这些进程并不是孤立的,它们彼此之间或者存在父子、兄弟关系,或 者在功能上有联系。

  • Linux 系统设计进程组实质上是为了方便对进程进行管理。假设为了完成一个任务,需要并发运行 100 个进程,但当处于某种场景时需要终止这 100 个进程,若没有进程组就需要一个一个去终止,这样非常麻烦 且容易出现一些问题;有了进程组的概念之后,就可以将这 100 个进程设置为一个进程组,这些进程共享一 个进程组 ID,这样一来,终止这 100 个进程只需要终止该进程组即可。

  • 进程组的特性

    • 每个进程必定属于某一个进程组、且只能属于一个进程组;
    • 每一个进程组有一个组长进程,组长进程的 ID 就等于进程组 ID;
    • 在组长进程的 ID 前面加上一个负号即是操作进程组;
    • 组长进程不能再创建新的进程组;
    • 只要进程组中还存在一个进程,则该进程组就存在,这与其组长进程是否终止无关;
    • 一个进程组可以包含一个或多个进程,进程组的生命周期从被创建开始,到其内所有进程终止或离 开该进程组;
    • 默认情况下,新创建的进程会继承父进程的进程组 ID。
  • 通过系统调用 getpgrp()或 getpgid()可以获取进程对应的进程组 ID

#include <unistd.h>
pid_t getpgid(pid_t pid);//对应的线程的进程组ID
pid_t getpgrp(void);
  • 调用系统调用 setpgid()或 setpgrp()可以加入一个现有的进程组或创建一个新的进程组
#include <unistd.h>
int setpgid(pid_t pid, pid_t pgid);
int setpgrp(void);
  • setpgid()函数将参数 pid 指定的进程的进程组 ID 设置为参数 gpid。如果这两个参数相等(pid==gpid), 则由 pid 指定的进程变成为进程组的组长进程,创建了一个新的进程;如果参数 pid 等于 0,则使用调用者 的进程 ID;另外,如果参数 gpid 等于 0,则创建一个新的进程组,由参数 pid 指定的进程作为进程组组长 进程
  • setpgrp()函数等价于 setpgid(0, 0)。
  • 一个进程只能为它自己或它的子进程设置进程组 ID,在它的子进程调用 exec 函数后,它就不能更改该 子进程的进程组 ID 了

会话

  • 会话是一个或多个进程组的集合

  • image-20220121213639860

  • 一个会话可包含一个或多个进程组,但只能有一个前台进程组,其它的是后台进程组;每个会话都有一 个会话首领(leader),即创建会话的进程。

  • 一个会话可以有控制终端、也可没有控制终端,在有控制终端的情况下也只能连接一个控制终端,这通常是登录到其上的终端设备(在终端登录情况下)或伪终端设备 (譬如通过 SSH 协议网络登录),一个会话中的进程组可被分为一个前台进程组以及一个或多个后台进程 组。

  • 会话的首领进程连接一个终端之后,该终端就成为会话的控制终端,与控制终端建立连接的会话首领进 程被称为控制进程;产生在终端上的输入和信号将发送给会话的前台进程组中的所有进程,譬如 Ctrl + C(产 生 SIGINT 信号)、Ctrl + Z(产生 SIGTSTP 信号)、Ctrl + \(产生 SIGQUIT 信号)等等

  • 一个进程组由组长进程的 ID 标识,而对于会话来说,会话的首领进程的进程组 ID 将作为该会话的标 识,也就是会话 ID(sid),在默认情况下,新创建的进程会继承父进程的会话 ID。通过系统调用 getsid()可 以获取进程的会话 ID

#include <unistd.h>
pid_t getsid(pid_t pid);
  • 使用系统调用 setsid()可以创建一个会话,其函数原型如下所示
#include <unistd.h>
pid_t setsid(void);
  • 如果调用者进程不是进程组的组长进程,调用 setsid()将创建一个新的会话调用者进程是新会话的首 领进程,同样也是一个新的进程组的组长进程,调用 setsid()创建的会话将没有控制终端。

守护进程

  • 守护进程(Daemon)也称为精灵进程,是运行在后台的一种特殊进程,它独立于控制终端并且周期性 地执行某种任务或等待处理某些事情的发生

  • 长期运行。守护进程是一种生存期很长的一种进程,它们一般在系统启动时开始运行,除非强行终 止,否则直到系统关机都会保持运行。与守护进程相比,普通进程都是在用户登录或运行程序时创 建,在运行结束或用户注销时终止,但守护进程不受用户登录注销的影响,它们将会一直运行着、 直到系统关机。

  • 与控制终端脱离。在 Linux 中,系统与用户交互的界面称为终端,每一个从终端开始运行的进程都 会依附于这个终端,这是上一小节给大家介绍的控制终端,也就是会话的控制终端。当控制终端被 关闭的时候,该会话就会退出,由控制终端运行的所有进程都会被终止,这使得普通进程都是和运 行该进程的终端相绑定的;但守护进程能突破这种限制,它脱离终端并且在后台运行,脱离终端的 目的是为了避免进程在运行的过程中的信息在终端显示并且进程也不会被任何终端所产生的信息 所打断。

  • 守护进程 Daemon,通常简称为 d,一般进程名后面带有 d 就表示它是一个守护进程。

创建守护进程的步骤

  • 创建子进程、终止父进程
    • 父进程调用 fork()创建子进程,然后父进程使用 exit()退出,这样做实现了下面几点。第一,如果该守护 进程是作为一条简单地 shell 命令启动,那么父进程终止会让 shell 认为这条命令已经执行完毕。第二,虽然 子进程继承了父进程的进程组ID,但它有自己独立的进程ID,这保证了子进程不是一个进程组的组长进程, 这是下面将要调用 setsid 函数的先决条件!
  • 子进程调用 setsid 创建会话
    • 这步是关键,在子进程中调用上一小节给大家介绍的 setsid()函数创建新的会话,由于之前子进程并不 是进程组的组长进程,所以调用 setsid()会使得子进程创建一个新的会话,子进程成为新会话的首领进程, 同样也创建了新的进程组、子进程成为组长进程,此时创建的会话将没有控制终端。所以这里调用 setsid 有 三个作用:让子进程摆脱原会话的控制、让子进程摆脱原进程组的控制和让子进程摆脱原控制终端的控制。 在调用 fork 函数时,子进程继承了父进程的会话、进程组、控制终端等,虽然父进程退出了,但原先 的会话期、进程组、控制终端等并没有改变,因此,那还不是真正意义上使两者独立开来。setsid 函数能够 使子进程完全独立出来,从而脱离所有其他进程的控制
  • 将工作目录更改为根目录
    • 子进程是继承了父进程的当前工作目录,由于在进程运行中,当前目录所在的文件系统是不能卸载的, 这对以后使用会造成很多的麻烦。因此通常的做法是让“/”作为守护进程的当前目录,当然也可以指定其 它目录来作为守护进程的工作目录。
  • 重设文件权限掩码 umask
    • 文件权限掩码 umask 用于对新建文件的权限位进行屏蔽,在 5.5.5 小节中有介绍。由于使用 fork 函数新 建的子进程继承了父进程的文件权限掩码,这就给子进程使用文件带来了诸多的麻烦。因此,把文件权限掩 码设置为 0,确保子进程有最大操作权限、这样可以大大增强该守护进程的灵活性。设置文件权限掩码的函 数是 umask,通常的使用方法为 umask(0)。
  • 关闭不再需要的文件描述符
    • 子进程继承了父进程的所有文件描述符,这些被打开的文件可能永远不会被守护进程(此时守护进程指 的就是子进程,父进程退出、子进程成为守护进程)读或写,但它们一样消耗系统资源,可能导致所在的文 件系统无法卸载,所以必须关闭这些文件,这使得守护进程不再持有从其父进程继承过来的任何文件描述 符。
  • 将文件描述符号为 0、1、2 定位到/dev/null
    • 将守护进程的标准输入、标准输出以及标准错误重定向到/dev/null,这使得守护进程的输出无处显示、 也无处从交互式用户那里接收输入。
  • 其它:忽略 SIGCHLD 信号
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <signal.h>
int main(void)
{
pid_t pid;
int i;
/* 创建子进程 */
pid = fork();
if (0 > pid)
{
perror("fork error");
exit(-1);
}
else if (0 < pid) //父进
exit(0); //直接退出
/*
*子进程
*/
/* 1.创建新的会话、脱离控制终端 */
if (0 > setsid())
{
perror("setsid error");
exit(-1);
}
/* 2.设置当前工作目录为根目录 */
if (0 > chdir("/"))
{
perror("chdir error");
exit(-1);
}
/* 3.重设文件权限掩码 umask */
umask(0);
/* 4.关闭所有文件描述符 */
for (i = 0; i < sysconf(_SC_OPEN_MAX); i++)
close(i);
/* 5.将文件描述符号为 0、1、2 定位到/dev/null */
open("/dev/null", O_RDWR);
dup(0);
dup(0);
/* 6.忽略 SIGCHLD 信号 */
signal(SIGCHLD, SIG_IGN);
/* 正式进入到守护进程 */
for (;;)
{
sleep(1);
puts("守护进程运行中......");
}
exit(0);
}

image-20220121223837693

  • 可见没有输出,因为输出已经重定向到dev/null

  • ps -ajx中看到这个进程

  • image-20220121224001123

  • 这个进程可以用kill pid停止

单例模式

  • 对于有些程序设计来说,程序只能被执行一次,只要该程序没有结束,就无法 再次运行,我们把这种情况称为单例模式运行,多次同时运行并没有意义、甚至还会带来错误!。

方法:

  • 通过一个特定的文件存在与否进行判断(进程开始的时候新建一个文件,结束的时候删除)
  • 文件锁
    • 当程序启动之后,首先打开该文件,调用 open 时一般使用 O_WRONLY | O_CREAT 标志,当文件不存在则创建该文件,然后尝试去获取文件锁,若是成功,则将程序 的进程号(PID)写入到该文件中,写入后不要关闭文件或解锁(释放文件锁),保证进程一直持有该文件 锁;若是程序获取锁失败,代表程序已经被运行、则退出本次启动。
    • 通过系统调用 flock()fcntl()或库函数 lockf()均可实现对文件进行上锁,本小节我们以系统调用flock()为例,系统调用 flock() 产生的是咨询锁(建议性锁)、并不能产生强制性锁
#include <stdio.h>
#include <stdlib.h>
#include <sys/file.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <string.h>
#define LOCK_FILE "./testApp.pid"
int main(void)
{
char str[20] = {0};
int fd;
/* 打开 lock 文件,如果文件不存在则创建 */
fd = open(LOCK_FILE, O_WRONLY | O_CREAT, 0666);
if (-1 == fd)
{
perror("open error");
exit(-1);
}
/* 以非阻塞方式获取文件锁 */
if (-1 == flock(fd, LOCK_EX | LOCK_NB))
{
fputs("不能重复执行该程序!\n", stderr);
close(fd);
exit(-1);
}
puts("程序运行中...");
ftruncate(fd, 0); //将文件长度截断为 0
sprintf(str, "%d\n", getpid());
write(fd, str, strlen(str)); //写入 pid
for (;;)
sleep(1);
exit(0);
}

image-20220121225414970

  • 首先打开一个特定的文件,这里只是举例,以当前目录下的 testApp.pid 文件作为特定文件, 以 O_WRONLY | O_CREAT 方式打开,如果文件不存在则创建该文件;打开文件之后使用 flock 尝试获取文 件锁,调用 flock()时指定了互斥锁标志 LOCK_NB,意味着同时只能有一个进程拥有该锁,如果获取锁失败, 表示该程序已经启动了,无需再次执行,然后退出;如果获取锁成功,将进程的 PID 写入到该文件中,当程 序退出时,会自动解锁、关闭文件

Linux进程

  • main传参
int main(int argc, char *argv[])

进程

  • 进程是一个动态过程,而非静态文件,它是程序的一次运行过程,当应用程序被加载到内存中运行之后 它就称为了一个进程,当程序运行结束后也就意味着进程终止,这就是进程的一个生命周期。

  • Linux 系统下的每一个进程都有一个进程号(processID,简称 PID),进程号是一个正数,用于唯一标 识系统中的某一个进程。在 Ubuntu 系统下执行 **ps -aux **命令可以查到系统中进程相关的一些信息,包括每个进程 的进程号

  • image-20220121134232404

  • 通过系统调用 getpid()来获取本进程的进程号

#include <sys/types.h>
#include <unistd.h>
pid_t getpid(void);
  • 还可以使用 getppid()系统调用获取父进程的进程号

进程的环境变量

  • 每一个进程都有一组与其相关的环境变量,这些环境变量以字符串形式存储在一个字符串数组列表中, 把这个数组称为环境列表。其中每个字符串都是以“名称=值(name=value)”形式定义

  • 在 shell 终端下可以使用 env 命令查看到 shell 进程的所有环境变量

  • 使用 export 命令还可以添加一个新的环境变量或删除一个环境变量

    • export LINUX_APP=123456
  • 使用”export -n LINUX_APP”命令则可以删除 LINUX_APP 环境变量

    • export -n LINUX_APP

环境变量存放在一个字符串数组中,在应用程序中,通过 environ 变量指向它,environ 是一个全局变 量,在我们的应用程序中只需申明它即可使用

extern char **environ; // 申明外部全局变量 environ

如果只想要获取某个指定的环境变量,可以使用库函数 getenv()

#include <stdlib.h>
char *getenv(const char *name);
  • 不应该去修改其返回的字符串,修改该字符串意味着修改了环境变量对应的值

  • 添加/修改环境变量

  • putenv()函数

#include <stdlib.h>
int putenv(char *string);

​ string:参数 string 是一个字符串指针,指向 name=value 形式的字符串。

​ 返回值:成功返回 0;失败将返回非 0 值,并设置 errno。

  • setenv()函数
#include <stdlib.h>
int setenv(const char *name, const char *value, int overwrite);

​ name:需要添加或修改的环境变量名称。

​ value:环境变量的值。

​ overwrite:若参数 name 标识的环境变量已经存在,在参数 overwrite 为 0 的情况下,setenv()函数将不 改变现有环境变量的值,也就是说本次调用没有产生任何影响;如果参数 overwrite 的值为非 0,若参数 name 标识的环境变量已经存在,则覆盖,不存在则表示添加新的环境变量。

​ 返回值:成功返回 0;失败将返回-1,并设置 errno。

​ setenv()函数为形如 name=value 的字符串分配一块内存缓冲区,并将参数 name 和参数 value 所指向的 字符串复制到此缓冲区中,以此来创建一个新的环境变量

  • 除了上面给大家介绍的函数之外,我们还可以通过一种更简单地方式向进程环境变量表中添加环境变量
NAME=value ./app
  • 在执行程序的时候,在其路径前面添加环境变量,以 name=value 的形式添加,如果是多个环境变量, 则在./app 前面放置多对 name=value 即可,以空格分隔

  • 清除环境变量:

    • environ = NULL;或者
    • 使用stdlib中的int clearenv(void);

环境变量的作用

  • 环境变量常见的用途之一是在 shell 中,每一个环境变量都有它所表示的含义,譬如 HOME 环境变量表 示用户的家目录,USER 环境变量表示当前用户名,SHELL 环境变量表示 shell 解析器名称,PWD 环境变 量表示当前所在目录等,在我们自己的应用程序当中,也可以使用进程的环境变量。

进程的内存布置

  • 正文段。也可称为代码段,这是 CPU 执行的机器语言指令部分,文本段具有只读属性,以防止程 序由于意外而修改其指令;正文段是可以共享的,即使在多个进程间也可同时运行同一段程序。

  • 初始化数据段。通常将此段称为数据段,包含了显式初始化的全局变量和静态变量,当程序加载到 内存中时,从可执行文件中读取这些变量的值。

  • 未初始化数据段。包含了未进行显式初始化的全局变量和静态变量,通常将此段称为 bss 段,这一 名词来源于早期汇编程序中的一个操作符,意思是“由符号开始的块”(block started by symbol), 在程序开始执行之前,系统会将本段内所有内存初始化为 0,可执行文件并没有为 bss 段变量分配 存储空间,在可执行文件中只需记录 bss 段的位置及其所需大小,直到程序运行时,由加载器来分 配这一段内存空间。

  • 栈。函数内的局部变量以及每次函数调用时所需保存的信息都放在此段中,每次调用函数时,函数 传递的实参以及函数返回值等也都存放在栈中。栈是一个动态增长和收缩的段,由栈帧组成,系统 会为每个当前调用的函数分配一个栈帧,栈帧中存储了函数的局部变量(所谓自动变量)、实参和 返回值。

  • 。可在运行时动态进行内存分配的一块区域,譬如使用 **malloc()**分配的内存空间,就是从系统堆 内存中申请分配的。

  • image-20220121141423676

虚拟地址

  • 在 32 位系统中,每个进程的逻辑地址空间均为 4GB,这 4GB 的内存空间按照 3:1 的比例 进行分配,其中用户进程享有 3G 的空间,而内核独自享有剩下的 1G 空间,如下所示:

  • image-20220121141526808

  • 学习过驱动开发的读者对“虚拟地址”这个概念应该并不陌生,虚拟地址会通过硬件 MMU(内存管理 单元)映射到实际的物理地址空间中,建立虚拟地址到物理地址的映射关系后,对虚拟地址的读写操作实际 上就是对物理地址的读写操作,MMU 会将物理地址“翻译”为对应的物理地址

    • 虚拟地址解决的问题
    • 内存使用效率低。内存空间不足时,就需要将其它程序暂时拷贝到硬盘中,然后将新的程序装入内 存。然而由于大量的数据装入装出,内存的使用效率就会非常低。(实际上不会真正给程序分配所有的空间,程序用到的时候才会分配空间)
    • 进程地址空间不隔离。由于程序是直接访问物理内存的,所以每一个进程都可以修改其它进程的 内存数据,甚至修改内核地址空间中的数据,所以有些恶意程序可以随意修改别的进程,就会造成 一些破坏,系统不安全、不稳定。
    • 无法确定程序的链接地址。程序运行时,链接地址和运行地址必须一致,否则程序无法运行!因为 程序代码加载到内存的地址是由系统随机分配的,是无法预知的,所以程序的运行地址在编译程序 时是无法确认的。
    • 在某些应用场合下,两个或者更多进程能够共享内存。因为每个进程都有自己的映射表,可以让不 同进程的虚拟地址空间映射到相同的物理地址空间中。通常,共享内存可用于实现进程间通信。
    • 便于实现内存保护机制。譬如在多个进程共享内存时,允许每个进程对内存采取不同的保护措施, 例如,一个进程可能以只读方式访问内存,而另一进程则能够以可读可写的方式访问。

创建子线程

#include <unistd.h>
pid_t fork(void);
  • fork()调用成功后,将会在父进程中返回子进程的 PID,而在子进程中返回值是 0;如果调用失败,父进 程返回值-1,不创建子进程,并设置 errno。
  • fork()系统调用的关键在于,完成对其调用后将存在两个进程,一个是原进程(父进程)、另一个 则是创建出来的子进程,并且每个进程都会从 fork()函数的返回处继续执行,会导致调用 fork()返回两次值, 子进程返回一个值、父进程返回一个值。在程序代码中,可通过返回值来区分是子进程还是父进程。
  • fork()调用成功后,子进程和父进程会继续执行 fork()调用之后的指令,子进程、父进程各自在自己的进 程空间中运行。事实上,子进程是父进程的一个副本,譬如子进程拷贝了父进程的数据段、堆、栈以及继承 了父进程打开的文件描述符,父进程与子进程并不共享这些存储空间,这是子进程对父进程相应部分存储 空间的完全复制,执行 fork()之后,每个进程均可修改各自的栈数据以及堆段中的变量,而并不影响另一个 进程。
  • 虽然子进程是父进程的一个副本,但是对于程序代码段(文本段)来说,两个进程执行相同的代码段, 因为代码段是只读的,也就是说父子进程共享代码段,在内存中只存在一份代码段数据。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main(void)
{
pid_t pid;
pid = fork();
switch (pid)
{
case -1:
perror("fork error");
exit(-1);
case 0:
printf("这是子进程打印信息<pid: %d, 父进程 pid: %d>\n",
getpid(), getppid());
_exit(0); //子进程使用_exit()退出
default:
printf("这是父进程打印信息<pid: %d, 子进程 pid: %d>\n",
getpid(), pid);
exit(0);
}
}

进程和子进程之间的文件共享

  • 调用 fork()函数之后,子进程会获得父进程所有文件描述符的副本,这些副本的创建方式类似于 dup(), 这也意味着父、子进程对应的文件描述符均指向相同的文件表

  • image-20220121181944858

  • 由此可知,子进程拷贝了父进程的文件描述符表,使得父、子进程中对应的文件描述符指向了相同的文 件表,也意味着父、子进程中对应的文件描述符指向了磁盘中相同的文件,因而这些文件在父、子进程间实 现了共享,譬如,如果子进程更新了文件偏移量,那么这个改变也会影响到父进程中相应文件描述符的位置 偏移量

  • 假如是父子进程分别打开同一个文件的话(在fork()之后),则读写文件会互相覆盖,因为偏移量没有互相影响

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main(void)
{
pid_t pid;
int fd;
int i;
pid = fork();
switch (pid)
{
case -1:
perror("fork error");
exit(-1);
case 0:
/* 子进程 */
fd = open("./Text.txt", O_WRONLY);
if (0 > fd)
{
perror("open error");
_exit(-1);
}
for (i = 0; i < 4; i++) //循环写入 4 次
write(fd, "1122", 4);
close(fd);
_exit(0);
default:
/* 父进程 */
fd = open("./Text.txt", O_WRONLY);
if (0 > fd)
{
perror("open error");
exit(-1);
}
for (i = 0; i < 5; i++) //循环写入 4 次
write(fd, "AABB", 4);
close(fd);
exit(0);
}
}
  • 父进程写入5次,子进程写入4次,最终的输出为

    • image-20220121184306317
  • vfork()

  • 除了 fork()系统调用之外,Linux 系统还提供了 vfork()系统调用用于创建子进程,vfork()与 fork()函数在 功能上是相同的,并且返回值也相同,在一些细节上存在区别

#include <sys/types.h>
#include <unistd.h>

pid_t vfork(void);
  • 从前面的介绍可知,可以将 fork()认作对父进程的数据段、堆段、栈段以及其它一些数据结构创建拷贝, 由此可以看出,使用 fork()系统调用的代价是很大的,它复制了父进程中的数据段和堆栈段中的绝大部分内 容,这将会消耗比较多的时间,效率会有所降低,而且太浪费,原因有很多,其中之一在于,fork()函数之 后子进程通常会调用 exec 函数.子进程不再执行父程序中的代码 段,而是执行新程序的代码段,从新程序的 main 函数开始执行、并为新程序重新初始化其数据段、堆段、 栈段等.

  • 出于这一原因,引入了 vfork()系统调用,虽然在一些细节上有所不同,但其效率要高于 fork()函数。类 似于 fork(),vfork()可以为调用该函数的进程创建一个新的子进程,然而,vfork()是为子进程立即执行 exec() 新的程序而专门设计的

    • vfork的区别:
      • vfork()与 fork()一样都创建了子进程,但 vfork()函数并不会将父进程的地址空间完全复制到子进程 中,因为子进程会立即调用 exec(或_exit),于是也就不会引用该地址空间的数据。不过在子进程 调用 exec 或_exit 之前,它在父进程的空间中运行、子进程共享父进程的内存。这种优化工作方式 的实现提高的效率;但如果子进程修改了父进程的数据(除了 vfork 返回值的变量)、进行了函数 调用、或者没有调用 exec 或_exit 就返回将可能带来未知的结果。
      • 另一个区别在于,vfork()保证子进程先运行子进程调用 exec 之后父进程才可能被调度运行
  • 现代的 Linux 系统内核已经采 用了写时复制技术来实现 fork(),其效率较之于早期的 fork()实现要高出许多,除非速度绝对重要的场合, 我们的程序当中应舍弃 vfork()而使用 fork()。

  • 调用 fork()之后,子进程成为了一个独立的进程,可被系统调度运行,而父进程也继续被系统调度运行, 这里出现了一个问题,调用 fork 之后,无法确定父、子两个进程谁将率先访问 CPU

  • 此时可以采用先让某个进程堵塞,然后另一个进程向其发送信号将其唤醒

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>
#include <sys/types.h>
static void sig_handler(int sig)
{
printf("接收到信号\n");
}
int main(void)
{
struct sigaction sig = {0};
sigset_t wait_mask;
/* 初始化信号集 */
sigemptyset(&wait_mask);
/* 设置信号处理方式 */
sig.sa_handler = sig_handler;
sig.sa_flags = 0;
if (-1 == sigaction(SIGUSR1, &sig, NULL))
{
perror("sigaction error");
exit(-1);
}
switch (fork())
{
case -1:
perror("fork error");
exit(-1);
case 0:
/* 子进程 */
printf("子进程开始执行\n");
printf("子进程打印信息\n");
printf("~~~~~~~~~~~~~~~\n");
sleep(2);
kill(getppid(), SIGUSR1); //发送信号给父进程、唤醒它
_exit(0);
default:
/* 父进程 */
if (-1 != sigsuspend(&wait_mask)) //挂起、阻塞
exit(-1);
printf("父进程开始执行\n");
printf("父进程打印信息\n");
exit(0);
}
}

  • 上面代码中的父进程先通过调用sigsuspend(&wait_mask)堵塞,然后等待子线程发送kill(getppid(), SIGUSR1)发送信号将其唤醒

init 进程的 PID 总是为 1,它是所有子进程的父进程,一切从 1 开始、一切从 init 进程开始

进程的退出

  • 如果程序中注册了进程终止处理函数,那么会调用终止处理函数。在 9.1.2 小节给大家介绍如何注 册进程的终止处理函数;
  • 刷新 stdio 流缓冲区
  • 执行_exit()系统调用
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main(void)
{
printf("Hello World!");
switch (fork())
{
case -1:
perror("fork error");
exit(-1);
case 0:
/* 子进程 */
exit(0);
default:
/* 父进程 */
exit(0);
}
}

这个程序会将其中的”Hello World”打印两次,但是假如上述字符串包含换行符的话就不会打印显示,原因如下

  • 进程的用户空间内存中维护了 stdio 缓冲区,0 小节给大家 介绍过,因此通过 fork()创建子进程时会复制这些缓冲区。标准输出设备默认使用的是行缓冲,当检测到换 行符\n 时会立即显示函数 printf()输出的字符串,此时输出之后的缓冲区的空的
  • 假如没有换行符的话,系统并不会立即输出内容,这就导致在创建子线程的时候会将缓冲去等待显示的字符串也拷贝一次。当它们调用 exit()函数时,都会刷 新各自的缓冲区、显示字符串,所以就会看到打印出了两次相同的字符串
    • 防止上述问题的办法
    • 在调用 fork()之前,使用函数 fflush()来刷新 stdio 缓冲区,当然,作为另一种选择,也可以使用 setvbuf()和 setbuf()来关闭 stdio 流的缓冲功能

监视子进程

  • 系统调用 wait()可以等待进程的任一子进程终止,同时获取子进程 的终止状态信息
#include <sys/types.h>
#include <sys/wait.h>
pid_t wait(int *status);
  • status:参数 status 用于存放子进程终止时的状态信息,参数 status 可以为 NULL,表示不接收子进程 终止时的状态信息。

  • 返回值:若成功则返回终止的子进程对应的进程号;失败则返回-1。

  • wait函数的内容

    • 调用 wait()函数,如果其所有子进程都还在运行,则 wait()会一直阻塞等待,直到某一个子进程终 止
    • 如果进程调用 wait(),但是该进程并没有子进程,也就意味着该进程并没有需要等待的子进程,那 么 wait()将返回错误,也就是返回-1、并且会将 errno 设置为 ECHILD。
    • 如果进程调用 wait()之前,它的子进程当中已经有一个或多个子进程已经终止了,那么调用 wait() 也不会阻塞。wait()函数的作用除了获取子进程的终止状态信息之外,更重要的一点,就是回收子 进程的一些资源,俗称为子进程“收尸”,关于这个问题后面再给大家进行介绍。所以在调用 wait() 函数之前,已经有子进程终止了,意味着正等待着父进程为其“收尸”,所以调用 wait()将不会阻 塞,而是会立即替该子进程“收尸”、处理它的“后事”,然后返回到正常的程序流程中,一次 wait() 调用只能处理一次
    • 参数 status 不为 NULL 的情况下,则 wait()会将子进程的终止时的状态信息存储在它指向的 int 变量中, 可以通过宏来检查 status 参数(略)
  • waitpid()

#include <sys/types.h>
#include <sys/wait.h>
pid_t waitpid(pid_t pid, int *status, int options);
  • pid:参数 pid 用于表示需要等待的某个具体子进程,关于参数 pid 的取值范围如下:

    • 如果 pid 大于 0,表示等待进程号为 pid 的子进程;
    • 如果 pid 等于 0,则等待与调用进程(父进程)同一个进程组的所有子进程;
    • 如果 pid 小于-1,则会等待进程组标识符与 pid 绝对值相等的所有子进程;
    • 如果 pid 等于-1,则等待任意子进程。wait(&status)与 waitpid(-1, &status, 0)等价。
  • status:与 wait()函数的 status 参数意义相同。

  • 参数 options 是一个位掩码,可以包括 0 个或多个标志(略)

僵尸进程和孤儿进程

孤儿进程

  • 父进程先于子进程结束,也就是意味着,此时子进程变成了一个“孤儿”,我们把这种进程就称为孤儿 进程。
  • 在 Linux 系统当中,所有的孤儿进程都自动成为 init 进程(进程号为 1)的子进程,换言之,某一子 进程的父进程结束后,该子进程调用 getppid()将返回 1

僵尸进程

  • 进程结束之后,通常需要其父进程为其“收尸”,回收子进程占用的一些内存资源,父进程通过调用 wait()(或其变体 waitpid()、waitid()等)函数回收子进程资源,归还给系统。
  • 如果子进程先于父进程结束,此时父进程还未来得及给子进程“收尸”,那么此时子进程就变成了一个 僵尸进程。
  • 当父进程调用 wait()(或其变体,下文不再强调)为子进程“收尸”后,僵尸进程就会被内核彻底删除。 另外一种情况,如果父进程并没有调用 wait()函数然后就退出了,那么此时 **init 进程将会接管它的子进程并 自动调用 wait()**,故而从系统中移除僵尸进程。
  • 如果系统中存在大量的 僵尸进程,它们势必会填满内核进程表,从而阻碍新进程的创建。需要注意的是,僵尸进程是无法通过信号 将其杀死的,即使是“一击必杀”信号 SIGKILL 也无法将其杀死,那么这种情况下,只能杀死僵尸进程的 父进程(或等待其父进程终止),这样 init 进程将会接管这些僵尸进程,从而将它们从系统中清理掉

执行新程序

execve()

#include <unistd.h>
int execve(const char *filename, char *const argv[], char *const envp[]);
  • filename:参数 filename 指向需要载入当前进程空间的新程序的路径名,既可以是绝对路径、也可以是 相对路径。

  • argv:参数 argv 则指定了传递给新程序的命令行参数。是一个字符串数组,**该数组对应于 main(int argc, char *argv[])函数的第二个参数 argv,且格式也与之相同,是由字符串指针所组成的数组**,以 NULL 结束。 argv[0]对应的便是新程序自身路径名。

  • envp:参数 envp 也是一个字符串指针数组,指定了新程序的环境变量列表,参数 envp 其实对应于新程 序的 environ 数组,同样也是以 NULL 结束,所指向的字符串格式为 name=value

  • execve 调用成功将不会返回;失败将返回-1,并设置 errno。对 execve()的成功调用将永不返回,而且也无需检查它的返回值,实际上,一旦该函数返回,就表明它 发生了错误。

执行shell命令

  • 使用 system()函数可以很方便地在我们的程序当中执行任意 shell 命令,本小节来学习下 system()函数的 用法,以及介绍 system()函数的实现方法。 首先来看看 system()函数原型,如下所示
#include <stdlib.h>
int system(const char *command);
  • system()函数其内部的是通过调用 fork()、execl()以及 waitpid()这三个函数来实现它的功能,首先 system() 会调用 fork()创建一个子进程来运行 shell(可以把这个子进程成为 shell 进程),并通过 shell 执行参数 command 所指定的命令。
    • 当参数 command 为 NULL,如果 shell 可用则返回一个非 0 值,若不可用则返回 0;针对一些非 UNIX 系统,该系统上可能是没有 shell 的,这样就会导致 shell 不可能;如果 command 参数不为 NULL,则返回值从以下的各种情况所决定。
    • 如果无法创建子进程或无法获取子进程的终止状态,那么 system()返回-1;
    • 如果子进程不能执行 shell,则 system()的返回值就好像是子进程通过调用_exit(127)终止了;
    • 如果所有的系统调用都成功,system()函数会返回执行 command 的 shell 进程的终止状态。

manjaro停止应用程序

  • 在命令行输入xkill

  • 然后鼠标光标变化,点击想关闭的窗口即可

堆排序

 //heapSort 构建大顶堆或者小顶堆,将堆顶元素与堆尾元素交换后再调整,如此反复
public void heapSort(int[] arr)
{
//构建大顶堆 k为最后一个非叶子节点,逐渐-1,即从下向上,从右往左
for(int k = arr.length/2 - 1;k>=0;k--)
{
adjustHeap(arr,k,arr.length);
}

//排序 交换+调整
int temp =0;
for (int i = arr.length-1; i >= 0; i--)
{
temp =arr [0];
arr[0] = arr[i];
arr[i] = temp;
adjustHeap(arr,0,i);
}
}

/**
*
* @param arr 待调整数组
* @param i 非叶子节点在数组中的索引
* @param length 对多少个元素进行调整
*/
public void adjustHeap(int[] arr,int i,int length)
{
int temp = arr[i];//取出当前非叶子叶结点的值
//k为当前节点的左子节点
for(int k = 2*i+1;k<length;k=2*k+1)
{
if(k+1<length && arr[k+1]>arr[k])
{//右子节点大于左子节点
k++;//k指向右子节点
}
if(arr[k]>temp)
{
//如果当前节点大于父节点就交换
arr[i] = arr[k];
i = k;//!!!!!!精髓,因为该子节点值大小发生了改变,可能会使其子根堆发生改变,索引要调整其子根堆
}
else
{
break;//否则直接退出,因为其后面的节点一定满足堆定义
}
}
arr[i] = temp;
}
  • 一个对象的下标为i,他的左右子节点分别是2i+12i+2
  • 更换节点的时候

快排

//quickSort 每次选择一个元素并且将整个数组以这个元素分为两部分,小于该元素的放右边,大于该元素的放左边
public void quickSort(int[] arr,int l,int r)
{
if(l<r){ //跳出递归的条件
//partition就是划分操作,将arr划分成满足条件的两个子表
int pivotpos = partition(arr,l,r);
//依次对左右两个子表进行递归排序
quickSort(arr,l,pivotpos);
quickSort(arr,pivotpos+1,r);
}
}

public int partition(int[] arr,int l,int r)
{
//以当前数组的最后一个元素作为中枢pivot,进行划分
int pivot = arr[r];
while (l<r)
{
while (l<r && arr[l]<pivot) l++;
arr[r] = arr[l];//将比中枢值大的移动到右端r处 由于r处为中枢或者该位置值已经被替换到l处,所以直接可以替换
while (l<r && arr[r]>=pivot) r--;
arr[l] = arr[r];//将比中枢值小的移动到左端l处 由于前面l处的值已经换到r处,所以该位置值也可以替换掉
}
//l==r时,重合,这个位置就是中枢的最终位置
arr[l] = pivot;
//返回存放中枢的最终位置
return l;
}
  • 主要的思路是每次选择一个(实际上一般是最右边的那一个)元素作为标准,将比这个元素小的元素移动到数组的左边,大的移动到右边,最后实现这个元素的左边的所有元素都比这个元素小,右边的都比这个大。然后再分别对左边和右边的数组分别排序,实现递归。

    Leetcode 295. 数据流的中位数

  • 用一个最小堆记录大于中位数的数字,和一个最大堆记录小于中位数的数字
  • 新加入的数字与小于中位数的数字中的最大值比较,决定插入哪个堆
  • 根据总数替换中位数的值
    class MedianFinder {
    public:
    priority_queue<int, vector<int>, less<int>> queMin;
    priority_queue<int, vector<int>, greater<int>> queMax;

    MedianFinder() {}

    void addNum(int num) {
    if (queMin.empty() || num <= queMin.top()) {
    queMin.push(num);
    if (queMax.size() + 1 < queMin.size()) {
    queMax.push(queMin.top());
    queMin.pop();
    }
    } else {
    queMax.push(num);
    if (queMax.size() > queMin.size()) {
    queMin.push(queMax.top());
    queMax.pop();
    }
    }
    }

    double findMedian() {
    if (queMin.size() > queMax.size()) {
    return queMin.top();
    }
    return (queMin.top() + queMax.top()) / 2.0;
    }
    };

LRU缓存

程序

请你设计并实现一个满足 LRU (最近最少使用) 缓存的数据结构。

  • LRUCache(int capacity) 以 正整数 作为容量 capacity 初始化 LRU 缓存
  • int get(int key) 如果关键字 key 存在于缓存中,则返回关键字的值,否则返回 -1 。
  • void put(int key, int value) 如果关键字 key 已经存在,则变更其数据值 value ;如果不存在,则向缓存中插入该组 key-value 。如果插入操作导致关键字数量超过 capacity ,则应该 逐出 最久未使用的关键字。
  • 函数 get 和 put 必须以 O(1) 的平均时间复杂度运行。

程序思路

  • 使用双向链表存储数据,头节点尾节点另外存储
  • 使用一个unordered_map存储每一个键值对的指针
  • 每一次使用对应的指针,都将其移动到双向链表的头部
  • 容量超出的时候删除链表的末端节点
  • image-12345678

Qt网络请求下载图片并且显示

基本流程

  • 字符串通过QUrl()变成Url对象,然后QNetworkRequest通过setUrl()方法设置URL,然后QNetworkManager通过get方法访问并且返回回复
QNetworkReply *newReply = netWorkManager->get(networkRequest);
  • 回复对象具有downloadProgress(qint64, qint64)readyRead()finished(),三个信号,其中downloadProgress(qint64, qint64)用来更新进度条,readyRead()表示此时已经有数据可以读取(可能还没结束),finished()表示此时接收已经结束可以读到完整的文件
QByteArray data = reply->readAll();
  • 上面是读取到回复的全文内容

更新进度条

void MainWindow::imageDownloadProgress(qint64 bytes, qint64 totalBytes)
{
static qint8 count = 0;
count++;
if(count == 1)
progressBar->setMaximum(totalBytes);

progressBar->setValue(bytes);
}

下载结束显示图片

void MainWindow::replyFinished()
{
//QNetworkReply *reply = (QNetworkReply *)sender();

//reply->deleteLater();

QNetworkReply *reply = (QNetworkReply *)sender();

QByteArray data = reply->readAll();
reply->deleteLater();

if(data.isEmpty())
{
qDebug()<<"data is null, please try it again!"<<endl;
return;
}

QPixmap pixmap;
pixmap.loadFromData(data);

label[0]->setPixmap(pixmap);

}

开始下载

void MainWindow::startDownload()
{
QUrl newUrl(QUrl(lineEdit->text()));
if(!newUrl.isValid())
{
QMessageBox::information(this, "error", "invalid url");
return;
}

QNetworkRequest networkRequest;

networkRequest.setUrl(newUrl);

QNetworkReply *newReply = netWorkManager->get(networkRequest);

connect(newReply, SIGNAL(finished()), this, SLOT(replyFinished()));
connect(newReply, SIGNAL(readyRead()), this, SLOT(readyReadData()));
connect(newReply, SIGNAL(downloadProgress(qint64, qint64)), this, SLOT(imageDownloadProgress(qint64, qint64)));
}

Qt网络编程

服务端

获取IP

getLocalHostIP()
{
QList<QNetworkInterface> list = QNetworkInterface::allInterfaces();
foreach (QNetworkInterface interface, list)
{
QList<QNetworkAddressEntry> entryList = interface.addressEntries();
foreach(QNetworkAddressEntry entry ,entryList)
{
if(entry.ip().protocol() == QAbstractSocket::IPv4Protocol)
{
if(entry.ip().toString() == "127.0.0.1")continue;
comboBox->addItem(entry.ip().toString());
IPlist<<entry.ip();
}
}
}
}

allInterfaces()获取本地的所有网络接口,比如wlan,本地连接等等,然后使用addressEntries()获取所有IP地址,使用IPv4Protocol筛选所有的IPv4地址,进行连接,同时在

if(entry.ip().toString() == "127.0.0.1")continue;

中筛选去掉本地IP,只留下有意义的(可以从外部连接的)IP地址。

监听

startListen()
{
if(comboBox->currentIndex() != -1)
{
qDebug()<<"start Listen"<<endl;
tcpServer->listen(IPlist[comboBox->currentIndex()], spinBox->value());
btn1->setEnabled(false);
btn2->setEnabled(true);
comboBox->setEnabled(false);
spinBox->setEnabled(false);
clientBrowser->append("server IP address:"+comboBox->currentText());
clientBrowser->append("Listening port: "+spinBox->text());

}
}

监听就是使用tcpServer对象,监听IP和端口,通过函数传入,等待程序链接

停止监听

stopListen()
{
qDebug()<<"stop listen"<<endl;
tcpServer->close();
if(tcpSocket->state() == tcpSocket->ConnectedState)
{
tcpSocket->disconnectFromHost();
}
btn1->setEnabled(true);
btn2->setEnabled(false);
clientBrowser->append("Stopped Listening: "+spinBox->text());
comboBox->setEnabled(true);
spinBox->setEnabled(true);

}

注意,调用tcpServer->close()之后,已经连接的客户端还可以继续与主机进行通信,此时需要tcpSocket->disconnectFromHost();来断开当前连接的客户端与服务器的链接,**注意此时调用的tcpSocket对象是用户连接时候从下面的tcpServer->nextPendingConnection()获得的Socket对象 **。

应对用户链接

clientConnection()
{
tcpSocket = tcpServer->nextPendingConnection();
QString ip = tcpSocket->peerAddress().toString();
quint16 port = tcpSocket->peerPort();
clientBrowser->append("Client IP: "+ip);
clientBrowser->append("Client port"+QString::number(port));

connect(tcpSocket, SIGNAL(readyRead()), this, SLOT(recMessage()));
}

注意用户链接的时候,可以通过tcpServer->nextPendingConnection();获取用户链接的Socket对象,进而获取tcpSocket->peerAddress().toString();IP和tcpSocket->peerPort();端口号

收取消息

recMessage()
{
QString messages = tcpSocket->readAll();
clientBrowser->append(messages);
qDebug()<<messages<<endl;
}

发送消息

sendMessage()
{
if(tcpSocket->state() == tcpSocket->ConnectedState)
{
tcpSocket->write(send->text().toUtf8().data());
clientBrowser->append("Server:"+send->text());
}
}

客户端

此处仅仅强调客户端与服务器的区别

链接服务器

toConnect()
{
if(tcpSocket->state()!=tcpSocket->ConnectedState)
{
QHostAddress hostAdd;
hostAdd.setAddress(ipInput->text());
textBrowser->append("Connecting: " + hostAdd.toString() + " ...");
tcpSocket->connectToHost(hostAdd, spinBox->value());

}
}
  • 注意QHostAddress具有从字符串0-255点分格式的IP地址中parse出IP地址的功能(hostAdd.setAddress(ipInput->text());)

断开连接

toDisconnect()
{
tcpSocket->disconnectFromHost();
tcpSocket->close();
}

其他与编写服务端类似,在此不再赘述。

同一WiFi下两个Linux设备使用scp传输文件

首先要保证两个设备可以互相ping通

image-20220117122232319

然后在主机上使用

scp <文件路径> <对方用户名>@ip:<要复制到的路径>

image-20220117122635576

效果如上图

遇到向windows传输的时候connect refused的问题

  • picture 1

    解决办法

  • 安装openssh
  • 参考
  • 注意要用管理员权限打开Terminal
  • 使用powershell安装即可
  • 安装完之后关闭防火墙
    # Start the sshd service
    Start-Service sshd
  • 之后即可使用scp向Windows发送文件
  • picture 2

    windows发送文件的一些注意事项

  • windows设备的用户名是C盘用户文件夹下的用户文件夹的名字
  • 密码是微软账号的密码
  • 目标地址路径不能带有空格,不能使用引号, 如果含有特殊符号的话,整个路径需要用双引号包括起来