--- layout: "../../../../../layouts/MarkdownPost.astro" title: Linux笔记 - 进程 pubDate: 2022-04-05 13:13:35 categories: - 技术 - Linux cover: https://oss.nbtca.space/blog/clas/20220320105749-Lmcbbo.jpeg tid: linux-note-process description: 有关linux进程的一系列笔记。 permalink: /pages/324bec/ author: name: N3ptune link: https://www.cnblogs.com/N3ptune tags: - 技术 - Linux --- ## Linux 进程 本文会简单讲述进程创建和进程间通信。 简单说一下,进程是操作系统调度资源的基本单位,比如将可执行程序跑起来就变成了进程。 进程是由数据(全局变量、静态变量、只读变量...)、代码和堆栈(局部变量、动态分配的内存...)组成的。 进程的模式有用户模式、内核模式。 可以输入`ps`命令来查看正在运行的进程: ![process-1](./_assets/f835945d5de246bcabdff51dd984aaf2/process-1.png) ![process-2](./_assets/f835945d5de246bcabdff51dd984aaf2/process-2.png) `ps -aue`命令可以查看更为详细的信息: ![process-3](./_assets/f835945d5de246bcabdff51dd984aaf2/process-3.png) `top`命令可以实时查看正在运行的程序: ![process-4](./_assets/f835945d5de246bcabdff51dd984aaf2/process-4.png) 在终端输入`top`,输入 q 可以退出: ![process-5](./_assets/f835945d5de246bcabdff51dd984aaf2/process-5.png) Linux 系统上可以管理多个进程,进程被分时分片处理。 下面演示在程序中如何创建进程: 1. `system`函数可以在命令行中执行命令,可以借此来创建一个进程,不作赘述 ![process-6](./_assets/f835945d5de246bcabdff51dd984aaf2/process-6.png) 2. `fork`函数、`vfork`函数 创建子进程: 当前进程是父进程,被创建进程是子进程,创建完后父子进程通知执行 ![process-7](./_assets/f835945d5de246bcabdff51dd984aaf2/process-7.png) 3. `exec`家族 ![process-8](./_assets/f835945d5de246bcabdff51dd984aaf2/process-8.png) 这些函数都可以创建一个进程,暂且不展开描述。 进程有很多种状态,例如运行时、睡眠、挂起、等待、死亡、僵尸... 下面代码演示: ```c #include #include int main(void) { printf("This is parent process,pid: %d\n",getpid()); sleep(2); printf("Create child process\n"); int ret = fork(); if (ret) { printf("This is parent process: %d\n",getpid()); while(1) sleep(1); } else { printf("This is child process: %d\n",getpid()); while(1) sleep(1); } return 0; } ``` 运行这个程序,程序已经打印出了进程号: ![process-9](./_assets/f835945d5de246bcabdff51dd984aaf2/process-9.png) 这里有必要明确一下`fork`函数的返回值: ![process-10](./_assets/f835945d5de246bcabdff51dd984aaf2/process-10.png) 根据 fork 函数的返回值来看是在子进程中还是父进程中,调用成功的话,父进程中返回值是子进程的 ID,子进程中是 0。实际上这里 fork 返回了两次。 fork 做了两件事,第一件是复制结构,将父进程的数据结构都复制了一份。 第二件事就是唤醒新进程,让子进程运行起来。 运行如下代码: ```c #include #include int main(void) { printf("This is parent process,pid: %d\n",getpid()); sleep(2); printf("Create child process\n"); int ret = fork(); int n = 10; if (ret) { printf("This is parent process: %d\n",getpid()); while(1) { printf ("n = %d\n",n++); sleep(1); } } else { printf("This is child process: %d\n",getpid()); while(1) { printf("n = %d\n", n += 2); sleep(1); } } return 0; } ``` 在代码中定义了一个变量 n,初始值为 10,子进程的 n 同样初始为 10: ![process-11](./_assets/f835945d5de246bcabdff51dd984aaf2/process-11.png) 下面谈谈僵尸进程。 有一种情况,父进程创建了子进程,父进程先于子进程结束,子进程资源没有被释放,就会变成僵尸进程,持续占用系统资源(内核中进程树会保存进程的数据,树中节点会保存进程的一些数据)。 子进程结束前,会向父进程发送 SIGCHILD 信号,父进程收到信号后,回收子进程资源,然后父进程再结束。父进程可以写一个 wait 函数,等待子进程发送 SIGCHILD 信号。 ![process-12](./_assets/f835945d5de246bcabdff51dd984aaf2/process-12.png) 如下是代码演示: ```c #include #include int main(void) { if (fork()) { printf("create child process\n"); wait(0); } else { for(int i=0;i<5;i++) { printf("child process: %d\n",i); sleep(1); } } printf("end parent process\n"); return 0; } ``` 使用 wait 函数就是要等待子进程打印完所有数字,父进程才结束。 最后看看守护进程。 守护进程是一个独立的进程,最常见的用途就是记录其他进程的情况,保存系统日志。 终端输入`ps axj`可以查看系统中的守护进程: ![process-13](./_assets/f835945d5de246bcabdff51dd984aaf2/process-13.png) TPGID 为-1 的话,就说明是守护进程。 如果要把一个进程变成守护进程,要先`kill`其父进程,同时摆脱终端的控制。 要摆脱终端的控制,就要关闭三个文件描述符号:标准输入设备,标准输出设备,标准错误输出设备,然后重定向当前进程 IO 操作到/dev/null (黑洞设备)。然后要创建新的会话,摆脱原有会话进程组的控制。 这里要提到进程的组织形式:多个进程组成一个进程组,多个进程组组成一个会话。这里不详细解释会话是什么。 守护进程创建编程模型: 第一种: 1. 创建新会话 `setsid`; 2. 改变当前工作目录 `chdir`; 3. 重设当前文件权限 `umask`; 4. 关闭文件 `fclose`. 第二种: 1. 重设文件权限 `umask;`     2. 创建子进程 `fork`;     3. 结束父进程;     4. 创建新会话 `setsid;`     5. 防止子进程成为僵尸进程 忽略 SIGCHILD SIGUP 信号;     6. 改变当前工作目录 `chdir`;     7. 重定向文件描述符号 `open dup(fd,0) dup(fd,1)` . 下面演示创建守护进程: ```c #include #include #include #include #include #include #include #include #include int main(void) { // 重设文件权限 umask(0); // 创建子进程 结束父进程 int ret = fork(); if (ret < 0) { printf("create process failed: %m\n"); exit(-1); } if (ret > 0) { printf("parent process end\n"); exit(0); } if (0 == ret) { printf("pid: %d\n",getpid()); // 创建新会话 setsid(); // 防止子进程成为僵尸进程 忽略SIGCHILD、SIGUP信号 signal(SIGCHLD,SIG_IGN); signal(SIGHUP,SIG_IGN); // 改变当前工作目录 chdir("/"); // 重定向文件描述符号 open、dup函数 int fd = open("/dev/null",O_RDWR); dup2(fd,0); dup2(fd,1); } //模拟守护进程工作 while (true) { sleep(1); } return 0; } ``` 在终端中编译运行: ![process-14](./_assets/f835945d5de246bcabdff51dd984aaf2/process-14.png) 现在关闭终端,在命令行输入`ps -axj`,可以看到运行起来的守护进程还在运行: ![process-15](./_assets/f835945d5de246bcabdff51dd984aaf2/process-15.png) 这里要用`kill`杀掉进程。 接下来谈进程间通信。 顾名思义,要实现不同进程间进行传递信息。 这里可以分为两大类,一个是不同主机上的进程间进行通信(网络通信),另一个是同一主机上的进程进行通信。 先谈同一主机上进程通信,此时又可以分为两类:父子进程间通信和非父子进程间通信。 上文提到,`fork`子进程会拷贝父进程的数据,因此父子进程间通信还是比较简单的。 第一种通信方式,较为朴素,使用普通文件,进程 A 将要传递的信息放入这个文件,进程 B 再去读这个文件即可。父子进程间可通过文件描述符号,非父子进程之间就只能通过具体文件来通信。 第二种方式,文件映射虚拟内存 `mmap`。 第三种方式,匿名管道(父子间进程使用)和有名管道(非父子进程间使用)。 除此之外,还有信号、共享内存、消息队列、信号量和网络可用于通信。 本文主要讲前 3 种。 下面代码简单演示了第一种: ```c #include #include #include #include #include // 通过文件进行通信 // 父进程往文件里写入数据 // 子进程读出来并打印 int main(void) { int fd = open("test.dat",O_CREAT,0666); if (-1 == fd) { printf("创建文件失败\n"); exit(-1); } printf("创建文件成功\n"); if (fork()) { int n = 0; while(true) { int fd = open("test.dat",O_WRONLY); write(fd,&n,4); close(fd); sleep(1); n++; } } else { int m; while(true) { sleep(1); int fd = open("test.dat",O_RDONLY); read(fd,&m,4); close(fd); printf(">> %d\n",m); } } } ``` 运行程序,可以看见子进程将父进程写入文件的数据都打印了出来: ![process-16](./_assets/f835945d5de246bcabdff51dd984aaf2/process-16.png) 下面讨论管道。 管道也是 FIFO 结构,分为两种,匿名管道和有名管道。 父子进程使用匿名管道。 1. 创建文件描述符号; 2. 将文件描述符号变成管道。 ![process-17](./_assets/f835945d5de246bcabdff51dd984aaf2/process-17.png) ```c #include #include #include #include #include #include // 使用匿名管道 int main(void) { // 创建管道描述符号 int fd[2]; int ret = pipe(fd); if (ret == -1) { printf("创建管道失败\n"); exit(-1); } printf("创建管道成功\n"); // 父子进程之间使用管道 if (fork()) { char buff[1024] = {0}; while(true) { scanf("%s",buff); write(fd[1],buff,strlen(buff)); } } else { char temp[1024] = {0}; while(true) { ret = read(fd[0],temp,1023); if (ret > 0) { temp[ret] = 0; printf(">> %s\n",temp); } } } close(fd[0]); close(fd[1]); return 0; } ``` 上述代码的功能是父进程接收用户输入后传入管道,子进程从管道读出并显示。 ![process-18](./_assets/f835945d5de246bcabdff51dd984aaf2/process-18.png) 接着演示有名管道,流程如下: | 进程 A | 进程 B | | ------------------- | ------------------ | | 创建管道文件 mkfifo | | | 打开管道文件 | 打开管道文件 | | 往管道文件写入数据 | 从管道文件读取数据 | | 关闭管道 | 关闭管道 | | 删除管道文件 | | | | | ![process-19](./_assets/f835945d5de246bcabdff51dd984aaf2/process-19.png) 进程 A 的代码: ```c #include #include #include #include #include #include #include #include int main(void) { // 创建管道文件 int ret = mkfifo("test.pipe",0); if (-1 == ret) { printf("创建管道文件失败:%m\n"); exit(-1); } printf("创建管道文件成功\n"); // 打开管道文件 int fd = open("test.pipe",O_WRONLY); if (-1 == fd) { printf("打开管道文件失败:%m\n"); unlink("test.pipe"); exit(-1); } printf("打开管道文件成功\n"); // 循环写入 int n = 0; char buff[1024] = {0}; while(true) { sprintf(buff,"Hello Linux %d",n++); write(fd,buff,strlen(buff)); sleep(1); } close(fd); unlink("test.pipe"); return 0; } ``` 进程 B: ```c #include #include #include #include #include #include #include #include int main(void) { // 打开管道文件 int fd = open("test.pipe",O_RDONLY); if (-1 == fd) { printf("打开管道文件失败:%m\n"); unlink("test.pipe"); exit(-1); } printf("打开管道文件成功\n"); // 循环读取 while (true) { char buff[1024] = {0}; int ret = read(fd,buff,1023); if (ret > 0) { buff[ret] = 0; printf(">>> %s\n",buff); } } close(fd); unlink("test.pipe"); return 0; } ``` 运行结果: ![process-20](./_assets/f835945d5de246bcabdff51dd984aaf2/process-20.png) ![process-21](./_assets/f835945d5de246bcabdff51dd984aaf2/process-21.png) ![process-22](./_assets/f835945d5de246bcabdff51dd984aaf2/process-22.png) 这里要注意的是,这个程序不能在共享的文件夹下运行,因为共享文件夹下不能创建管道。同时必须两个进程都打开,这个程序才能返回,否则会阻塞。先关闭读取端,会导致写入端结束而先关写入端,不会对读取端造成影响。