Linux基础第五章 进程控制
5.2 fork
fork函数实现进程复制,类似于动物界的单性繁殖,fork函数直接创建一个子进程。这是Linux创建进程最常用的方法。在这一小节中,子进程概念指fork产生的进程,父进程指主动调用fork的进程。
fork后,子进程继承了父进程很多属性,包括:
文件描述符:相当与dup,标准输入标准输出标准错误三个文件
账户/组ID:
进程组ID
会话ID
控制终端
set-user-ID和set-group-ID标记
当前工作目录
根目录
umask
信号掩码
文件描述符的close-on-exec标记
环境变量
共享内存
内存映射
资源限制
但是也有一些不同,包括:
fork返回值
进程ID
父进程
进程运行时间记录,在子进程中被清0
文件锁没有继承
闹钟
信号集合
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h> int main()
{
printf("before fork\n"); // 在父进程中打开的文件描述符
// int fd = open("a.txt", O_RDWR|O_CREAT, 0777); // FILE* fp = fopen("a.txt", "r");
int fd = open("a.txt", O_RDWR);
pid_t pid = fork(); // 创建一个新进程
if(pid == 0)
{
// 子进程可以使用父进程的描述符
// write(fd, "hello", 5); // char ch = fgetc(fp);
char ch;
read(fd, &ch, 1);
printf("ch is %c\n", ch); printf("this is in child, ppid=%d\n", (int)getppid());
}
else if(pid > 0)
{
// write(fd, "world", 5);
char ch;
read(fd, &ch, 1);
printf("ch is %c\n", ch); // 当fork返回值大于0时,说明该进程是父进程
// 此时,返回值就是子进程的pid
printf("this is in parent, pid=%d\n", (int)getpid());
}
else
{
printf("error fork\n");
} printf("hello fork\n");
}#include <stdio.h>
#include <unistd.h> int global_var = 0;//fork()出来的子进程的值改变,不会影响父进程 因为开开辟了新的空间 int main()
{
int var = 0;
int* p = (int*)malloc(sizeof(int));
*p = 0; pid_t pid = fork();
if(pid == 0)
{
global_var = 100;
*p = 100;
var = 100;
printf("set var\n");
}
else if(pid > 0)
{
sleep(1);
// 确定的结果,就是0
printf("%d\n", global_var);
printf("var is %d\n", var); // 0
printf("*p = %d\n", *p);
} printf("hello world\n");
}#include <stdio.h>
#include <unistd.h> void forkn(int n)
{
int i;
for(i=0; i<n; ++i)
{
pid_t pid = fork();
if(pid == 0)
break;
}
} int main()
{
forkn(10); printf("hello world\n");
}
5.3 进程终止
进程有许多终止方法:
方法 | |
---|---|
main函数return | 正常退出 |
调用exit或者_Exit或者_exit | 正常退出 |
在多线程程序中,最后一个线程例程结束 | 正常退出 |
在多线程程序中,最后一个线程调用pthread_exit | 正常退出 |
调用abort | 异常退出 |
收到信号退出 | 异常退出 |
多线程程序中,最后一个线程响应pthread_cancel | 异常退出 |
当进程退出时,内核会为进程清除它申请的内存,这里的内存是指物理内存,比如栈空间、堆、代码段、数据段等,并且关闭所有文件描述符。
一般来说,进程退出时,需要告诉父亲进程退出的结果,如果是正常退出,那么这个结果保存在内核的PCB中。如果是异常退出,那么PCB中保存退出结果的字段,是一个不确定的值。因此程序员应该避免程序的异常退出。
进程退出时,除了它的PCB所占内存,其他资源都会清除。
5.4 wait和waitpid
一个进程终止后,其实这个进程的痕迹还没有完全被清除,因为还有一个PCB在内核中,如果不回收,那么会导致内存泄漏。父进程可以调用wait函数来回收子进程PCB,并得到子进程的结果。
wait
是一个阻塞调用,它的条件是一个子进程退出或者一个子进程有状态变化。wait
得到的status,包含了子进程的状态变化原因和退出码信息等等。
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h> int main()
{
pid_t pid = fork(); if(pid == 0)
{
sleep(1);
printf("child process\n"); return 18;
}
else if(pid > 0)
{
printf("parent process\n"); // 等待子进程结束,并且回收子进程的PCB
int status;
wait(&status); // 如何得到子进程的返回值
if(WIFEXITED(status))
{
printf("normal child process exit\n"); // 正常退出 int code =WEXITSTATUS(status);
printf("code is %d\n", code);
}
else if(WIFSIGNALED(status))
{
printf("signal\n");
}
else if(WIFSTOPPED(status))
{
printf("child stopped\n");
}
else if(WIFCONTINUED(status))
{
printf("child continue...\n");
} printf("after wait\n");
} return 0;
}
wait和waitpid可能会阻塞父进程,所以一般使用SIGCHLD信号来监控子进程
5.5 僵尸进程和孤儿进程
5.5.1 僵尸进程
是指已经退出的进程,但是父进程没有调用wait回收的子进程。僵尸进程没有任何作用,唯一的副作用就是内存泄漏。如果父进程退出,那么它的所有僵尸儿子会得到清理,因此僵尸进程一般指那些用不停歇的后台服务进程的僵尸儿子。
程序员应该避免僵尸进程的产生。
#include <stdio.h>
#include <unistd.h> int main()
{
pid_t pid = fork();
if(pid == 0)
{
// 子进程什么事儿都不干,退出了,此时子进程是僵尸进程
}
else if(pid > 0)
{
getchar(); // 父进程不退出
} return 0;
}
5.5.2 孤儿进程
父进程退出了,而子进程没有退出,那么子进程就成了没有父亲的孤儿进程。孤儿进程不会在系统中出现很长时间,因为系统一旦发现孤儿进程,就会将其父进程设置为init进程。那么将来该进程的回收,由init来负责。
5.6 exec
exec函数执行一个进程,当一个进程调用exec后,调用该函数的进程的虚拟地址空间的代码段、数据段、堆、栈被释放,替换成新进程的代码段、数据段、堆、栈,而PCB依旧使用之前进程的PCB。这个函数用中文来说就是鸠占鹊巢。
exec后使用的是同一个PCB,所以exec之后和之前,由很多进程属性是相同的,包括:
进程ID和父进程ID
账户相关
进程组相关
定时器
当前目录和根目录
umask
文件锁
信号mask
未决的信号
资源限制
进程优先级
进程时间
没有close-on-exec属性的文件描述符
使用fork和exec来执行一个新程序
#include <unistd.h>
#include <stdio.h> // execle, e表示环境变量environ
//
int main(int argc, char* argv[])
{
char* args[] = {
"/bin/ls",
"-a",
"-l",
NULL
};
execv("/bin/ls", args);
} int main2(int argc, char* argv[])
{
// p表示在PATH的环境变量中寻找这个程序
execlp("ls", "ls", NULL);
} int main1(int argc, char* argv[])
{
// 执行一个程序
execl("/bin/ls", "/bin/ls", "-a", "-l", NULL); // 该函数不会被执行
printf("hello world\n");
}
#include <stdio.h>
#include <fcntl.h>
#include <sys/types.h>
#include <unistd.h> int main()
{
// fd is 3
int fd = open("exec.txt", O_RDWR|O_CREAT|O_CLOEXEC, 0777); execl("./exec_test", "./exec_test", NULL);
}
函数后缀 | 解析 |
---|---|
l | list 用不定参数列表来表示命令参数,如果用不定参数列表,那么用NULL表示结束 |
v | vector 用数组来传递命令行参数 |
p | path 表示程序使用程序名即可,在$PATH中搜索该程序,不带p的需要提供全路径 |
e | environ 表示环境变量 |
补充:不定参数
不定参数函数定义:
int main()
{
int a = add(3, 12, 13, 14);
int b = add(2, 12, 13);
int c = add(4, 12, 13, 14, 15);
printf("%d, %d, %d\n", a, b, c); char* p = concat("abc", "bcd", NULL);
printf("p is %s\n", p); // 最后的NULL,被称之为哨兵
p = concat("aaaa", "bbbb", "cccc", NULL);
printf("p is %s\n", p); }
#include <stdio.h>
#include <fcntl.h>
#include <sys/types.h> // 如果没有__VA_ARGS__不带##,表示__VA_ARGS__至少要表示一个参数
// #define mylog(fmt, ...) printf("[%s:%d] "fmt, __FILE__, __LINE__, __VA_ARGS__) // __VA_ARGS__如果有##,表示可以没有参数
#define mylog(fmt, ...) printf("[%s:%d] "fmt, __FILE__, __LINE__, ##__VA_ARGS__) int main()
{
int fd = open("a.txt", O_RDWR);
if(fd < 0)
{
mylog("error open file\n");
}
}
#include <stdio.h> // 转字符串 abc "abc"
#define STR(a) #a // 拼接标识符
#define CC(a, b) a##b int main()
{
int abcxyz = 100;
printf("%d\n", CC(abc, xyz));
}
5.8 账户和组控制
5.9 进程间关系
在Linux系统中,进程间除了有父子关系,还有组关系、Session关系、进程和终端进程关系。设计这些关系是为了更好的管理进程。
5.9.1 Session
一次登陆算一个session,exit命令可以退出session,session包括多个进程组,一旦session领导退出,那么一个session内所有进程退出(它的所有进程收到一个信号)。
#include <unistd.h>
int main()
{
pid_t pid = fork(); if(pid == 0)
{
// 独立一个session
setsid();
} while(1)
{
sleep(1);
}
}
5.9.2 进程组
在终端执行进程,就会生成一个进程组。执行的进程fork之后,子进程和父进程在一个组中。
进程组长退出后,进程组的其他进程的组号依旧没有变化。
5.10 练习
5.10.1 fork任意个子进程。
5.10.2 使用多进程加速文件拷贝
使用-job
定义进程数量,加速文件拷贝。
5.10.3 实现自定义终端
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <stdlib.h> // ls
// mkdir aaa
// cp ../aa bb
// cd
void handle_cmd(char* cmd)
{
char* args[1024];
char* p = strtok(cmd, " ");
int i = 0;
while(p)
{
args[i++] = p;
p = strtok(NULL, " ");
}
args[i] = NULL; // 表示参数结束位置 if(strcmp(args[0], "cd") == 0)
{
// 切换当前目录
chdir(args[1]);
return;
} pid_t pid = fork();
if(pid == 0)
{
execvp(args[0], args);
// 如果命令执行失败,应该让子进程退出
printf("invalid command\n");
exit(0);
}
else
{
wait(NULL);
}
} int main()
{
while(1)
{
printf("myshell> ");
// 等待用户输入
char buf[4096];
fgets(buf, sizeof(buf), stdin);
buf[strlen(buf)-1] = 0; // remove \n if(strlen(buf) == 0)
{
continue;
} handle_cmd(buf);
}
}
5.11 函数和命令
5.11.1 函数
fork:创建子进程
exec:执行新的程序
wait/waitpid:等待子进程结束,回收子进程PCB内存。
va_list:
va_start:定义指向不定参数的第一个参数的地址
va_arg:从参数列表中获取一个参数,并且让指针指向下一个参数
va_end:清除ap
Linux基础第五章 进程控制的更多相关文章
- Linux网络编程学习(三) ----- 进程控制实例(第三章)
本节主要介绍一个进程控制的实例,功能就是在前台或者后台接收命令并执行命令,还能处理由若干个命令组成的命令行,该程序命名为samllsh. 基本逻辑就是 while(EOF not typed) { 从 ...
- Linux网络编程学习(二) ----- 进程控制(第三章)
1.进程和程序 程序是一个可执行文件,而一个进程是一个执行中的程序实例.一个进程对应于一个程序的执行,进程是动态的,程序是静态的,多个进程可以并发执行同一个程序.比如几个用户可以同时运行一个编辑程序, ...
- UNIX环境高级编程 第8章 进程控制
本章是UNIX系统中进程控制原语,包括进程创建.执行新程序.进程终止,另外还会对进程的属性加以说明,包括进程ID.实际/有效用户ID. 进程标识 每个进程某一时刻在系统中都是独一无二的,它们之间是用一 ...
- JAVA基础第五章-集合框架Map篇
业内经常说的一句话是不要重复造轮子,但是有时候,只有自己造一个轮子了,才会深刻明白什么样的轮子适合山路,什么样的轮子适合平地! 我将会持续更新java基础知识,欢迎关注. 往期章节: JAVA基础第一 ...
- (已转)Linux基础第六章 信号
6.1 前言 本章简单描述信号.信号是Linux系统中,内核和进程通信的一种方式.如果内核希望打断进程的顺序执行,那么内核会在进程的PCB中记录信号.而当这个进程被分配到CPU,进入执行状态时,首先会 ...
- Linux基础命令---top显示进程信息
top top指令用来显示Linux的进程信息,这是一个动态显示的过程.top提供运行系统的动态实时视图.它可以显示系统摘要信息以及当前由Linux内核管理的任务列表.所显示的系统摘要信息的类型以及为 ...
- Linux基础--------监控系统、进程管理、软件包管理-------free、dd、kill、 rpm、yum、源码安装python
作业一:1) 开启Linux系统前添加一块大小为15G的SCSI硬盘 2) 开启系统,右击桌面,打开终端 3) 为新加的硬盘分区,一个主分区大小为5G,剩余空间给扩展分区,在扩展分区上划分1个逻辑分区 ...
- 《Unix环境高级编程》读书笔记 第8章-进程控制
1. 进程标识 进程ID标识符是唯一.可复用的.大多数Unix系统实现延迟复用算法,使得赋予新建进程的ID不同于最近终止所使用的ID ID为0的进程通常是调度进程,也常被称为交换进程.它是内核的一部分 ...
- Linux 笔记 - 第五章 Linux 用户与用户组管理
博客地址:http://www.moonxy.com Linux 是一个多用户的操作系统,在日常的使用中,从安全角度考虑,应该尽量避免直接使用 root 用户登录,而使用普通用户. 1. 关于用户 u ...
随机推荐
- [CG从零开始] 4. pyopengl 绘制一个正方形
在做了 1-3 的基础工作后,我们的开发环境基本 OK 了,我们可以开始尝试利用 pyopengl 来进行绘制了. 本文主要有三个部分 利用 glfw 封装窗口类,并打开窗口: 封装 shader 类 ...
- 关于aws-SecurityGroup-安全组策略的批量添加的方法记录
因一些服务的客户端网络地址段计划变更,会影响到aws上配置这这些网段安全组策略所绑定的资源 因此需要先整理包含了出那些服务的网络地址段的安全组 然后根据旧网段的策略信息,将新的地址段给添加上,待后续正 ...
- GitLab + Jenkins + Harbor 工具链快速落地指南
目录 一.今天想干啥? 二.今天干点啥? 三.今天怎么干? 3.1.常规打法 3.2.不走寻常路 四.开干吧! 4.1.工具链部署 4.2.网络配置 4.3.验证工具链部署结果 4.3.1.GitLa ...
- 【算法】基础DP
参考资料 背包九讲 一.线性DP 如果现在在状态 i 下,它上一步可能的状态是什么. 上一步不同的状态依赖于什么. 根据上面的分析,分析出状态和转移方程.注意:dp 不一定只有两维或者一维,一开始设计 ...
- python视频与帧图片的相互转化,以及查看视频分辨率
1.拆分视频为帧图片 import cv2 def video2frame(videos_path,frames_save_path,time_interval): vidcap = cv2.Vide ...
- iptables规则查询
iptables规则查询 之前在iptables的概念中已经提到过,在实际操作iptables的过程中,是以"表"作为操作入口的,如果你经常操作关系型数据库,那么当你听到" ...
- Java多线程-ThreadPool线程池-2(四)
线程池是个神器,用得好会非常地方便.本来觉得线程池的构造器有些复杂,即使讲清楚了对今后的用处可能也不太大,因为有一些Java定义好的线程池可以直接使用.但是(凡事总有个但是),还是觉得讲一讲可能跟有助 ...
- Redis 先操作数据库和先删除缓存, 一致性分析
初始状态: 数据库和缓存中v=10 第一种,先删除缓存在操作数据库: 线程1准备更新数据库的值v=20,先删除缓存, 此时线程2进来, 缓存未命中,查询数据库v=10, 写入缓存v=10, cpu回到 ...
- pyinstaller 打包exe相关
-w 只有窗口,没有console -p 加入路径 -F 生成一个exe文件 有虚拟环境时,需要先在cmd中进入虚拟环境,再执行打包程序 # 生成一个exe 无窗口 有icon Pyside2 pyi ...
- 【Azure API 管理】Azure APIM服务集成在内部虚拟网络后,在内部环境中打开APIM门户使用APIs中的TEST功能失败
问题描述 使用微软API管理服务(Azure API Management),简称APIM. 因为公司策略要求只能内部网络访问,所以启用了VNET集成.集成方式见: (在内部模式下使用 Azure A ...