05_父子进程_文件描述符

代码: https://github.com/WHaoL/study/tree/master/00_06_Linux_SystemCode_and_SocketCode

代码: https://gitee.com/liangwenhao/study/tree/master/00_06_Linux_SystemCode_and_SocketCode

1. 进程概述

1.1 程序和进程

程序:磁盘文件, 只占用磁盘资源
进程:一个启动之后的程序
    进程不占用磁盘资源,占用cpu和内存资源
    
进程,是并发执行的程序在执行过程中分配和管理资源的基本单位,是一个动态概念,竟争计算机系统资源的基本单位。每一个进程都有一个自己的地址空间,即进程空间或(虚空间)。进程空间的大小 只与处理机的位数有关,一个 16 位长处理机的进程空间大小为 2^16^ ,而 32 位处理机的进程空间大小为 2^32^ 。    

1.2 并行和并发

并行和并发

1.并发

2.并行
在这里插入图片描述

并发: 在一个比较短的时间段之内处理的任务的量

CPU时间片

时间片即CPU分配给各个程序的时间,每个进程被分配一个时间段,称作它的时间片,即该进程允许运行的时间,使各个程序从表面上看是同时进行的。如果在时间片结束时进程还在运行,则CPU将被剥夺并分配给另一个进程。如果进程在时间片结束前阻塞或结束,则CPU当即进行切换。而不会造成CPU资源浪费。

  • 在宏观上:我们可以同时打开多个应用程序,每个程序并行不悖,同时运行。

  • 但在微观上:由于只有一个CPU,一次只能处理程序要求的一部分,为了公平处理,方法就是引入时间片,让每个程序轮流执行。

在Linux的内核处理过程中,每一个进程默认会有一个固定的时间片来执行命令(默认为1/100秒),这段时间内进程被分配到CPU,然后独占使用。如果使用完,同时未到时间片的规定时间,那么就主动放弃CPU的占用,如果到时间片尚未完成工作,那么CPU的使用权也会被收回,进程将会被中断挂起等待下一个时间片。

1.3 PCB

PCB - 进程控制块(Processing Control Block),Linux内核的进程控制块是task_struct结构体。

理解该结构体中包含的如下信息:

  • 进程id。系统中每个进程有唯一的id,在C语言中用pid_t类型表示,其实就是一个非负整数。
    • 相当于身份证号, 唯一的
    • 类型: pid_t, 正整数
  • 进程的状态,有就绪、运行、挂起、停止等状态。
  • 进程切换时需要保存和恢复数据的一些CPU寄存器。
  • 描述虚拟地址空间的信息。
  • 描述控制终端的信息。
  • 当前工作目录(Current Working Directory)。
    • 进程是在哪个目录下启动的, 这个目录就是当前进程的工作目录
  • umask掩码。
    • umask函数可以调用
  • 文件描述符表,包含很多指向file结构体的指针。
    • 每个进程都有一个属于自己的文件描述符表
  • 和信号相关的信息。
  • 用户id和组id。
    • 进程是哪个用户下启动的就属于哪个用户
    • 有对应的api函数可以获取到这些信息
  • 会话(Session)和进程组。
  • 进程可以使用的资源上限(Resource Limit)。
  • ulimit -a

1.4 进程状态

进程有五种状态, 分别是: 创建态, 就绪态, 运行态, 阻塞态, 退出态。

  • 创建态 == 初始态
  • 阻塞态 == 挂起态

在这里插入图片描述

创建态: 进程从无到有的过程, 加载资源并初始化
	- 进程被创建完毕 -> 就绪态

就绪态: 进程已经存在了, 但是不能运行, 因为没有cpu资源
	- 抢到cpu碎片 -> 运行态
	
运行态: 正常的进程, 并且拥有cpu资源, 可以运行
	- 抢到cpu碎片时间用完, 失去cpu -> 就绪态
	- 通过外力让进程时间碎片没有用完的情况下失去cpu, 比如: sleep(100) -> 阻塞态(挂起态)
		
阻塞态: 通过外力让进程时间碎片没有用完的情况下失去cpu, 在特定的时机下才能解除阻塞
	- 通过某事操作, 让进程失去cpu
	- 当某些条件满足, 才会解除阻塞
		比如: sleep(100)之后, 睡醒了 -> 就绪态
	
退出态: 进程被销毁, 进程占用的所有系统资源全部被释放
	- 就绪, 运行, 挂起态时都可以直接退出

2. 进程创建

2.1 进程ID

2.1.1 命令

  • 查看进程

    ps aux / ajx
    	a: 显示当前终端下的所有的程序, 包括所有的用户
    	u: 显示用户信息
    	x: 打印和tty终端相关的信息
    	j: 显示更多的用户信息
    
  • 杀死进程

    # 命令, 9代表的九号信号
    kill -9 进程ID
    kill -SIGKILL 进程ID
    #linux中的信号
    $ kill -l
     1) SIGHUP       2) SIGINT       3) SIGQUIT      4) SIGILL       5) SIGTRAP
     6) SIGABRT      7) SIGBUS       8) SIGFPE       9) SIGKILL     10) SIGUSR1
    11) SIGSEGV     12) SIGUSR2     13) SIGPIPE     14) SIGALRM     15) SIGTERM
    16) SIGSTKFLT   17) SIGCHLD     18) SIGCONT     19) SIGSTOP     20) SIGTSTP
    21) SIGTTIN     22) SIGTTOU     23) SIGURG      24) SIGXCPU     25) SIGXFSZ
    26) SIGVTALRM   27) SIGPROF     28) SIGWINCH    29) SIGIO       30) SIGPWR
    31) SIGSYS      34) SIGRTMIN    35) SIGRTMIN+1  36) SIGRTMIN+2  37) SIGRTMIN+3
    38) SIGRTMIN+4  39) SIGRTMIN+5  40) SIGRTMIN+6  41) SIGRTMIN+7  42) SIGRTMIN+8
    43) SIGRTMIN+9  44) SIGRTMIN+10 45) SIGRTMIN+11 46) SIGRTMIN+12 47) SIGRTM IN+13
    48) SIGRTMIN+14 49) SIGRTMIN+15 50) SIGRTMAX-14 51) SIGRTMAX-13 52) SIGRTMAX-12
    53) SIGRTMAX-11 54) SIGRTMAX-10 55) SIGRTMAX-9  56) SIGRTMAX-8  57) SIGRTMAX-7
    58) SIGRTMAX-6  59) SIGRTMAX-5  60) SIGRTMAX-4  61) SIGRTMAX-3  62) SIGRTMAX-2
    63) SIGRTMAX-1  64) SIGRTMAX
    

2.1.2 函数

  • 获取当前进程的进程ID

  • 获取当前进程父进程的进程ID

    #include <sys/types.h>
    #include <unistd.h>
    
    pid_t getpid(void);		// 当前进程
    pid_t getppid(void); 	// 当前进程的父进程
    

2.2 进程的创建

  • 如何在程序中创建进程?

    #include <unistd.h>
    // 返回值是进程ID
    // 返回两个值:
    // 	- 一个是父进程的: 返回值 > 0
    //  - 一个是子进程的: 返回值 == 0
    pid_t fork(void);
    

在这里插入图片描述

  • 子进程创建成功之后,代码从什么位置执行?

在这里插入图片描述
在这里插入图片描述

  • 如何区分父子进程?

    fork返回值:
    	pid_t pid > 0: 父进程
    	pid_t pid == 0: 子进程
    

2.3 父子进程

1.区别
	- pcb中存储的进程ID不同
	- 进程的状态
	- 寄存器数据信息
2.共同点
2.1.当子进程被fork创建之后, 还没有进行任何操作之前, 父子进程用户区数据是完全相同的的
  	- 父子进程各自进行代码处理, 用户区数据会发生变化, 变得不一样了
2.2.内核区:
  	- 文件描述符表是相同的
  
3.父子进程对变量是不是共享的?
    - 不能共享
    - 进程和进程之间的数据是相互独立的, 一个进程数据变化对其他进程内部数据没有影响(父子进程)
    - 如果子进程被创建, 父子进程里边的变量都`没有进行写操作`
      - 这两个进程中的同名数据共用同一块内存
    - 如果子进程被创建, 父子进程里边的变量`进行了写操作`
      - 这两个进程中的同名数据使用的不是同一块内存  

在这里插入图片描述

3. 多进程的gdb调试

  • 默认gdb跟踪的是父进程

  • 切换命令 -> 在启动gdb的时候设置

    # 设置跟踪子进程
    set follow-fork-mode child
    # 设置跟踪父进程
    set follow-fork-mode parent
    

4. exec族函数

exec函数族的作用是根据指定的文件名找到可执行文件,并用它来取代调用进程的内容,换句话说,就是在调用进程内部执行一个可执行文件。

exec函数族的函数执行成功后不会返回,因为调用进程的实体,包括代码段数据段堆栈等都已经被新的内容取代,只留下进程ID等一些表面上的信息仍保持原样,颇有些神似"三十六计"中的"金蝉脱壳"。看上去还是旧的躯壳,却已经注入了新的灵魂。只有调用失败了,它们才会返回一个-1,从原程序的调用点接着往下执行。

// 使用exec族函数, 创建的进程的个数?
/*
使用思路:
- 父进程: 处理对应的逻辑
- 在合适的时机, 由父进程创建子进程: fork()
	- 子进程中的代码和父进程中的代码完全相同
- 在子进程中调用exec函数, 启动另一个磁盘程序a.out
	- 子进程中的代码被替换了, 变成了a.out中的代码
		- 栈区, 堆区, 全局数据区, 共享库区都会发生变化
	- 这个时候子进程就不受父进程控制了
*/

#include <unistd.h>
extern char **environ;

//ps aux

// 经常使用
// 使用这个函数启动自己编译得到的可执行程序
int execl(const char *path, const char *arg, ...);
参数: 
- path: 要启动的磁盘程序对应的路径, 建议使用`绝对路径`
- arg: 要启动的可执行程序的参数;参数结束, NULL作为最后一个参数
	- 第二个参数对应的arg: 名字随便写, 这个名字出现在ps aux 出现在COMMAND这一列(第二个参数一般和第一个参数相同)
	- 从第三个arg开始就是可执行程序的实际参数
	- 最后一个arg,NULL

// 启动由操作系统提供的可执行程序使用该函数
int execlp(const char *file, const char *arg, ...);
参数:
	- file: 只需要指定程序名, 程序会自动搜索环境变量PATH
	- arg: 要启动的可执行程序的参数;参数结束, NULL作为最后一个参数
		- 第二个参数对应的arg: 名字随便写, 这个名字出现在ps aux 出现在COMMAND这一列
		- 从第三个arg开始就是可执行程序的实际参数
		- 最后一个arg,NULL
		

  	理解	/
		
int execle(const char *path, const char *arg, ..., char *const envp[]);
参数:
	- path: 要启动的程序的名字
	- arg: 要启动的可执行程序的参数;参数结束, NULL作为最后一个参数
		- 第二个参数对应的arg: 名字随便写, 这个名字出现在ps aux 出现在COMMAND这一列
		- 从第三个arg开始就是可执行程序的实际参数
		- 最后一个arg,NULL
	- envp: 第一个参数对应的程序名去当前参数指定的目录下搜索
		char* envp[] = {
    
    "/bin", "/home/robin", NULL};

// 使用这个函数启动自己编译得到的可执行程序
int execv(const char *path, char *const argv[]);
参数:
	- path: 要启动程序的绝对路径
	- argv: 要启动的程序的参数
		char* args[] = {
    
    "123", "hello", "world", NULL};

// 启动由操作系统提供的可执行程序使用该函数
int execvp(const char *file, char *const argv[]);
参数:
	- file: 文件名, 程序会自动搜索环境变量PATH
	- argv: 要启动的程序的参数
		char* args[] = {
    
    "123", "hello", "world", NULL};

// 只有这个函数才是系统函数, 前五个都不是
int execvpe(const char *file, char *const argv[], char *const envp[]);
参数:
	- file: 写个文件名就可以, 要启动的程序
	- argv: 要启动的程序的参数
		char* args[] = {
    
    "123", "hello", "world", NULL};
	- envp: 第一个参数对应的程序名去当前参数指定的目录下搜索
		char* envp[] = {
    
    "/bin", "/home/robin", NULL};
l(list) 参数地址列表,以空指针结尾
v(vector) 存有各参数地址的指针数组的地址
p(path) 按 PATH 环境变量指定的目录搜索可执行文件
e(environment) 存有环境变量字符串地址的指针数组的地址

Qt中的exec族函数

QDialog dlg;
dlg.exec();	// 阻塞

5. 进程控制

每个进程结束之后, 都会自己释放自己地址空间中的 用户区 数据, 内核区的pcb没有办法自己释放掉, 需要父进程去释放.

5.1 结束进程

int main()
{
    
    
    int num = func();
    return 0;
}

int func()
{
    
    
    return 0;
}

在main函数中直接调用return函数可以退出进程
如果不是在main函数中return, 则程序返回到调用者的位置

// exit -> c库函数
#include <stdlib.h>
void exit(int status);
exit(-1);

// _exit() -> linux系统函数
#include <unistd.h>
void _exit(int status);

5.2 孤儿进程

父进程创建子进程, 父进程退出了, 子进程还在

操作系统中PID==1的进程会收养这个没有父亲的子进程

为什么要领养?

  • 所有进程的内核区的PCB资源自己无法释放, 必须要由父进程完成该操作

5.3 僵尸进程

首先有一个父进程, 创建了子进程

  • 子进程先死, 父进程一直活着
  • 父进程不负责任, 不去释放子进程的PCB资源
    • 这个时候的子进程称之为: 僵尸进程
    • 使用 kill -9 杀不死僵尸进程

5.3 进程回收 wait waitpid

#include <sys/types.h>
#include <sys/wait.h>	// 主要加这个

// 阻塞函数, 停到调用该函数的位置, 不会继续执行, 时机成熟之后继续执行
// 当子进程死亡之后, 该函数解除阻塞
// 该函数调用一次只能回收一个子进程资源
// 当前没有子进程之后, 函数解除阻塞, 调用一次, 返回-1
pid_t wait(int *status);
参数:
	- status: 死亡的子进程的状态信息, 一般该参数不用, 直接写NULL
返回值:
	成功: 被资源回收的子进程的进程PID
	失败: -1

// 可以设置阻塞, 也可以设置为非阻塞    
// 可以指定回收那个子进程资源
// 该函数调用一次只能回收一个子进程资源
// 如果该函数设置为阻塞, 当前没有子进程之后, 函数解除阻塞, 调用一次, 返回-1
pid_t waitpid(pid_t pid, int *status, int options);
// pid_t pid = waitpid(-1,NULL,WNOHANG)
参数:
- pid: 
	- >0: 某个进程的PID, 要回收那个进程就把哪个进程的PID写到这-> 经常用
	- ==0: 回收当前进程组中的所有子进程的资源
			只回收,本组内的自己的孩子
	- ==-1: 回收所有的子进程的资源							-> 经常用
			只要是自己的孩子,不论是否被送人,都回收
	- < 0: 回收送到其他组的子进程进程资源, 进程ID取反(比如: 12345), 因此 == -12345
            自己的孩子&&被送到其他的组了,回收
- status: 记录进程退出的状态, 一般写NULL
- options:
	- 0: 该函数阻塞
	- WNOHANG: 函数非阻塞
返回值:
	- >0: 成功回收的子进程对应的进程ID
	- -1: 失败
	- ==0: 该函数如果是非阻塞状态, 代表当前父进程还有子进程在运行
	
	
// 写程序:
在父进程中创建5个子进程, 使用waitpid回收这些子进程资源
// 这是错误的, 创建出的子进程个数>5
// 如果是子进程, 不要再创建子进程了
for(int i=0; i<5; ++i)
{
    
    
    pid_t pid = fork();
    if(0 == pid)
    {
    
    
        break;
    }
}

6. dup,dup2,fcntl函数

6.1 dup和dup2

#include <unistd.h>
// 复制文件描述符, 参数 old_fd 就是要复制的文件描述符
// 返回值  :被复制出的 new_fd 文件描述符
// 结论	:通过返回值和参数都能访问到同一个磁盘文件
int dup(int oldfd);

// 测试程序
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <fcntl.h>

int main()
{
    
    
    // 打开文件
    int fd = open("a.txt", O_RDWR|O_CREAT, 0664);
    if(fd == -1)
    {
    
    
        perror("open");
        return 0;
    }

    // 复制文件描述符
    int fd1 = dup(fd);
    // 关闭fd
    close(fd);
    // 写文件
    char* str = "hello, world";
    write(fd1, str, strlen(str));
    close(fd1);

    return 0;
}
// 要求: oldfd必须是一个有效的文件描述法, 指向某一个已经存在的磁盘文件
/*
1.可以复制文件描述符
	- 参数newfd, 没有指向一个有效的磁盘文件
	- 通过dup2可以让newfd指向oldfd对应的磁盘文件

2.可以重定向文件描述符
	- newfd指向磁盘文件A, oldfd指向磁盘文件B
	- 通过调用dup2
		- newfd和磁盘文件A断开连接, 同时指向了磁盘文件B
*/
// 如果newfd == oldfd, 相当于什么也没干
int dup2(int oldfd, int newfd);

// 测试代码
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <fcntl.h>

int main()
{
    
    
    // 打开文件
    int fd = open("a.txt", O_RDWR|O_CREAT, 0664);
    if(fd == -1)
    {
    
    
        perror("open");
        return 0;
    }

    int fd1 = open("b.txt", O_RDWR|O_CREAT, 0664);
    if(fd1 == -1)
    {
    
    
        perror("open");
        return 0;
    }
    
    int fd3 = 6;
#if 0
    // 重定向文件描述符
    dup2(fd, fd1);
#else
    // 文件描述符复制
    dup2(fd, fd3);
#endif
    close(fd);

    // 写文件
    char* str = "你好, 世界";
#if 0
    write(fd1, str, strlen(str));
    close(fd1);
#else
    write(fd3, str, strlen(str));
    close(fd3);
#endif


    return 0;
}

6.2 fcntl函数

// 控制函数
#include <unistd.h>
#include <fcntl.h>
int fcntl(int fd, int cmd, ... /* arg */ );

//复制文件描述符: 函数的第二个参数cmd: F_DUPFD
int newfd = fcntl(fd, F_DUPFD);

//修改文件的属性, 不是所有的属性都可以追加, 只能对一些附属属性进行追加/删除操作
//	- O_APPEND, 在文件尾部追加数据
//	- O_NONBLOCK, 设置为非阻塞

//使用步骤
//  1.int open(const char *pathname, int flags);
//    int fd = open("a", O_WRONLY);
//  2.得到原来设置好的属性信息: 第二个参数cmd: F_GETFL
//    int flag = fcntl(fd, F_GETFL);
//  3.追加属性
//    flag = flag | O_APPEND;
//  4.将新的属性设置给文件描述符: 第二个参数cmd: F_SETFL
//    fcntl(fd, F_SETFL, flag);
// 测试代码
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <fcntl.h>

int main()
{
    
    
    // 打开文件
    int fd = open("a.txt", O_RDWR|O_CREAT, 0664);
    if(fd == -1)
    {
    
    
        perror("open");
        return 0;
    }

    // fd添加追加数据的属性
    int flag = fcntl(fd, F_GETFL);
    //flag = flag | O_APPEND;
    flag |= O_APPEND;
    fcntl(fd, F_SETFL, flag);

    // 写文件
    char* str = "我是要成为海贼王的男人...";
    write(fd, str, strlen(str));
    close(fd);

    return 0;
}

猜你喜欢

转载自blog.csdn.net/liangwenhao1108/article/details/107494597