C++ (week5):Linux系统编程2:进程

二、进程

1.模型:为什么要抽象出进程?
为了进一步压榨计算机的资源。
要求:进程间隔离

2.进程是什么?
①用户角度:进程是正在执行的程序
②内核角度:进程是要执行的任务,进程是资源分配的最小单位

3.struct proc:
pid、parent、cwd、占用的内存资源、占用的外部设备资源(ofile)、CPU的状态(context)

4.如何实现进程的隔离?
时分共享。
(1)底层机制:
①指标:性能、安全、控制权
②方案一、方案二、方案三
(2)上层策略:


(一) CPU的虚拟化

1.内核的职责:管理硬件资源

2.操作系统的发展:
(1)批处理系统:①队头等待 ②资源利用率低
(2)分时系统:
(3)多任务处理系统:

3.如何共享资源:时分共享(CPU)、空分共享(内存)
(1)时分共享策略:一个进程占用CPU一段时间(时间片),然后切换到另一个进程执行。但进程间的切换会有开销,进程上下文切换开销。
(2)实现:①底层机制:上下文切换 ②上层策略:调度算法(选择哪一个进程执行)

4.进程
(1)用户角度:进程就是正在执行的程序
内核角度:进程是要执行的任务。

我们不希望一个进程失败会影响另外的进程,所以进程之间必须隔离,进程之间是相互之间看不到的,感知不到其他的进程存在。
从进程的角度看,就好像它独占计算机的所有资源。
抽象机制,就是CPU的虚拟化。

5.上下文,CPU的状态依靠寄存器保存,体现了进程的动态特点
上下文切换的时机:①调用系统调用 ②切换进程

6.系统调用

7.如何实现进程的切换
(1)第一种模式:仅有内核态
效率高,但不安全

(2)第二种模式:用户态、内核态 (引入了CPU的模态机制,为了安全考虑)
用户态使用系统调用时,切换到内核态。但用户态可以不使用系统调用,导致一直不切回核心态。

(3)第三种模式:时钟中断 + 用户态、核内核态 (引入了时钟设备)
①协作式:yield(),进程让出使用权
②抢占式(非协作式):引入硬件时钟设备,时钟中断(几毫秒),执行时钟中断处理函数,切换到内核态,操作系统拿回控制权。时间片应当是时钟中断的整数倍。

①进程是资源分配的最小单位
②进程是隔离的,进程无法感知内核和其他进程


(二) 进程命令

1.查看进程状态:ps 命令

进程快照:process snapshot
ps:显示与该终端相关的进程快照 (TTY是关联 远程控制终端)
ps x:和该用户相关的进程
ps ux
ps aux (all user)
查进程的pidps aux | grep "./可执行程序名"
查看线程的具体信息
查具体的可执行程序:ps -elLf | grep "./可执行程序名"
分页查看:ps -elLf | more


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

STAT (进程状态):
R:运行状态
S (Sleeping):阻塞状态(睡眠状态),进程正在等待某个事件,如I/O操作完成
D:不可中断的阻塞状态。进程在等待某些不可中断的系统调用,如等待硬件操作。
Z:僵尸状态。进程已经终止,但其父进程尚未获取其终止状态,即尚未调用wait()或waitpid()
T:停止状态。
s:会话进程
I I I (Idle):空闲状态。此状态主要出现在多核系统上,表示内核线程处于空闲状态。
<:高优先级
n:低优先级
+:前端
l l l:多线程


2.查看网络状态:netstat 命令

netstat命令是一个非常强大的网络诊断工具,对于监控和排查网络问题非常有用。

netstat -ano


3.其他进程命令

1.top
类似windows的任务管理器,每隔3秒更新信息

2.pstree
打印进程树,显示进程间的父子关系

3.前台进程 vs 后台进程
①前台进程:没有返回,但与控制终端关联
②后台进程:不会与终端控制,命令 &
jobs:查看所有的后台进程
fg 任务编号:将后台进程放到前端 foreground



(三) 进程的基本操作 (API)

1.获取进程的标识 (获得进程id):getpid、getppid

①getpid:获取pid
②getppid:获取父进程的pid

只要能返回,一定执行成功

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

pid_t getpid(void);
pid_t getppid(void);

2.创建进程:fork()

1.函数原型

#include <sys/types.h>	//pid_t
#include <unistd.h>		//fork()

pid_t fork(void); 

2.返回值
无参数,返回值是pid。
①创建子进程成功,父进程返回的是子进程的pid,子进程返回0;
②创建子进程失败,父进程返回-1,设置errno

父进程获得子进程的pid :fork()的返回值
子进程获得自己的pid和父进程的pid:getpid()、getppid()


3.判断是父进程还是子进程
①switch、case

pid_t pid = fork();
switch(pid){
case -1:
	error(1, errno, "fork");
case 0:
	//子进程
default:
	//父进程
}

②if、else

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

int main() {
    pid_t pid = fork();

    if(pid == -1){
        // fork失败
		error(1 ,errno, "fork()");
    }else if(pid == 0){
        // 子进程

    }else{
        // 父进程
    }

    return 0;
}

4.fork的原理
fork()是轻量级的创建进程的函数,创建的子进程会做如下操作:
①复制父进程的proc结构体,修改pid、ppid
②复制父进程的页表,不会复制父进程的物理内存空间。不同的虚拟地址,但映射到同一个物理内存。仅在父子中某个进程进行写操作,才会发生写时复制(copy on write),才开辟新的物理内存

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

在这里插入图片描述


5.父子进程的共享、私有问题
代码段,父子进程共享
数据段、堆、栈,父子进程私有
用户态缓冲区,父子进程私有。父子进程都有自己的缓冲区
打开文件列表是父子进程共享的,共享文件的位置、偏移量,由内核管理
文件描述符列表是父子进程私有的,由进程管理

在这里插入图片描述


6.从调用fork()开始分叉,父子进程;它们不会执行fork()前面的代码,父子进程从fork()各自返回,通过返回值pid来区分父子进程。fork()后的代码会执行两次,父子进程各执行一次。

注意事项:
①子进程也是从fork返回后,开始执行
②到底是父进程先执行,还是子进程先执行,这是不确定的

在这里插入图片描述



例题1:
假定我们可以修改一个程序的源代码,我们如何在一个指定的时间获取进程的 core 文件 (当时程序执行的状态),同时让该进程可以继续执行?

#include <func.h>
#include <stdio.h>

int main(int argc, char* argv[])
{
    //执行一些代码
    //...
    
    //获取core文件,请在这里填写你的代码:
    pid_t pid = fork();
    if(pid == -1){
        error(1, errno, "fork");
    }else if(pid == 0){ //子进程
        abort();
    }else{ //父进程
        //什么都不做
    }

    //获取core文件,请在这里填写你的代码:
    pid_t pid = fork();
    switch(pid){
    case -1:  
         error(1, errno, "fork"); 
    case 0 : //子进程 
         printf("子进程\n"); 
         abort();
    default: //父进程
         printf("父进程\n");
         break; 
    } 

    //执行后续逻辑
    //...
    
    return 0;
}


3.终止进程:exit()、_exit()、abort()、wait()、waitpid()

①正常终止:最终由系统调用_exit() 终止
②异常终止:最终由信号导致终止
在这里插入图片描述

在这里插入图片描述


(1)正常终止:exit()、_exit()

(1)atexit()
用函数名作为函数指针。返回0成功,非0失败。

#include <stdlib.h>
int atexit(void (*function)(void));

(2)exit()
是一个库函数,有三个步骤
①调用atexit(函数名)之前注册的函数
②刷新用户态缓冲区
③调用_exit(),正常终止进程

(3)_exit()
是一个系统调用,仅仅导致进程的终止

#include <unistd.h>
void _exit(int status);

#include <stdlib.h>
void _Exit(int status);

(2)异常终止:abort()

(4)异常终止:abort()
内核会给该进程发生SIGABRT信号,会导致进程异常终止。会产生coredump文件。


(5)信号导致终止:
收到信号,信号导致进程异常终止


4.进程控制

(1)孤儿进程 (Orphan Process)

1.孤儿进程:父进程先于子进程死亡。子进程存活,父进程终止。

2.孤儿进程会被 init进程(PID为1的进程) 收养。
init进程会定期调用 wait() 系统调用来清理这些孤儿进程,确保它们的资源被释放。

#include <func.h>
#include <stdio.h>

int main(int argc, char* argv[])
{
    pid_t pid = fork();
    switch(pid){
    case -1:
        error(1, errno, "fork");
    case 0:
        //子进程
        sleep(2);
        printf("pid = %d, ppid = %d\n", getpid(), getppid());
        exit(0);
    default:
        //父进程
        printf("Parent: pid = %d, childPid = %d\n", getpid(), getppid());
        exit(0);
    }

    return 0;
}

在这里插入图片描述


原因是 init进程的职责就是为孤儿进程收尸

for( ;  ;){
	wait();
}

(2)僵尸进程

1.僵尸进程 (Zombie Process)
僵尸进程:子进程死亡,但父进程没有被回收
僵尸进程:已经终止,但其终止状态尚未被父进程获取的进程。
僵尸进程:已经终止但其父进程尚未调用 wait() 或 waitpid() 系统调用读取其退出状态的进程。

当一个进程死亡时,绝大部分信息会被释放,而有一些信息会保存在内核 (pid、退出状态、CPU时间),方便父进程以后查看这些信息。并且给父进程发给SIGCHLD信号(告诉父进程,孩子已死),但父进程默认会忽略信号。

2.父进程如何回收僵尸进程?
答:父进程手动调用wait() 和 waitpid()

3.僵尸进程不处理会造成什么影响?
子进程的proc结构体没有被回收,导致没有pid可用

僵尸进程本身不会消耗大量系统资源,但如果有大量僵尸进程未被清理,进程描述符的数量会逐渐增多,可能导致系统无法为新的进程分配进程描述符,进而影响系统性能。[大量僵尸进程会导致进程描述符耗尽]


(3)wait

0.阻塞等待
阻塞(blocking)指的是一个进程因为某种原因(通常是等待某个事件的发生)而暂停执行,直到该事件发生后才恢复执行。阻塞等待(blocking wait)是一种常见的进程同步机制,用于确保某些操作在特定条件满足后再进行。


1.函数原型

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

pid_t wait(int *wstatus);  //子进程的终止状态信息

2.参数
wstatus:一个指向整数的指针,用于存储子进程的终止状态。可以通过五个宏来解析状态值。
wait的五个宏获取子进程的终止状态信息:
①WIFEXITED(wstatus):子进程是否正常终止
②WEXITSTATUS(wstatus):获取正常终止的退出状态码   _exit(status)
③WIFSIGNALED(wstatus):子进程是否异常终止
④WTERMSIG(wstatus):获取导致异常终止的信号
⑤WCOREDUMP(wstatus):是否能够产生core文件

W是wait的意思


3.返回值
①成功,返回终止的子进程的PID
②失败,返回-1,并且设置errno


4.wait()是阻塞点,会无限期阻塞,直到有子进程终止

int status;	//当作位图
pid_t childPid = wait(&status);

验证代码::


5.wait 和 waitpid 的比较
①wait:等待任意子进程终止,无法指定特定的子进程,会无限期阻塞;只能获取终止状态。
②waitpid:功能更为强大,可以指定等待特定的子进程,并且可以通过选项参数控制等待的行为。


6.代码示例

//解析子进程的退出状态
void print_wstatus(int status) {
    if (WIFEXITED(status)) {
        int exit_code = WEXITSTATUS(status);
        printf("exit_code = %d", exit_code);
    } else if (WIFSIGNALED(status)) {
        int signo = WTERMSIG(status);
        printf("term_sig = %d", signo);
#ifdef WCOREDUMP
        if (WCOREDUMP(status)) {
            printf(" (core dump)");
        }
#endif
    }
    printf("\n");
}

在这里插入图片描述

#include <func.h>

void print_wstatus(int status) {
    if (WIFEXITED(status)) {
        int exit_code = WEXITSTATUS(status);
        printf("exit_code = %d", exit_code);
    } else if (WIFSIGNALED(status)) {
        int signo = WTERMSIG(status);
        printf("term_sig = %d", signo);
#ifdef WCOREDUMP
        if (WCOREDUMP(status)) {
            printf(" (core dump)");
        }
#endif
    }
    printf("\n");
}

int main(int argc, char* argv[])
{
    pid_t pid = fork();
    switch (pid) {
    case -1:
        error(1, errno, "fork");
    case 0:
        // 子进程
        printf("CHILD: pid = %d\n", getpid());
        // sleep(2);
        // return 123;
        // exit(96);
        // _exit(9);
        // abort();
        while (1);
    default:
        // 父进程
        int status; // 保存子进程的终止状态信息, 位图。
        pid_t childPid = wait(&status); // 阻塞点:一直等待,直到有子进程终止
        if (childPid > 0) {
            printf("PARENT: %d terminated\n", childPid);
            print_wstatus(status);
        }
        exit(0);
    }
    return 0;
}

(4)waitpid

1.函数原型

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

pid_t waitpid(pid_t pid, int *wstatus, int options);
wait(&wstatus) 等价于 waitpid(-1, &wstatus, 0);

2.参数
(1)pid参数
pid > 0等待指定的子进程 (进程id 等于 pid)
pid == -1等待任意子进程,此时 waitpid 的行为与 wait 相同
③pid == 0:等待同进程组的子进程 [等待任何与调用进程属于同一进程组的子进程]
④pid < -1:等待指定进程组 |pid| 的子进程 [等待进程组id 等于 |pid| (绝对值)的热议子进程]


(2)int* status 状态
用于存储子进程的终止状态。可以通过一系列宏来解析状态值,如 WIFEXITED、WEXITSTATUS、WIFSIGNALED 等

waitpid(pid, NULL, 0); //无限期阻塞等待特定子进程退出,但对其退出状态不感兴趣

(3)int options
①0:无限期阻塞等待
②WNOHANG:不阻塞。[没有任何子进程的状态发生变化,waitpid 立即返回,而不是阻塞等待]
③WUNTRACED:如果子进程进入暂停状态(如被 SIGSTOP 信号停止),waitpid 返回其状态
④WCONTINUED:如果子进程在暂停后恢复运行(如被 SIGCONT 信号继续),waitpid 返回其状态

在这里插入图片描述

3.返回值
①成功,返回状态已经改变的子进程的pid
②成功,如果设置了WNOHANG,并且没有子进程修改状态,返回0
③失败,返回-1,并设置errno

4.使用场景
①处理僵尸进程:使用waitpid()可以防止僵尸进程的出现。调用waitpid()后,子进程的资源会被释放。


5.执行程序:exec函数簇

1.函数原型

#include <unistd.h>

extern char **environ;  //外部环境变量,字符指针数组(二级指针)

int execl(const char *path, const char *arg, ... /* (char  *) NULL */);
int execlp(const char *file, const char *arg, .../* (char  *) NULL */);
int execle(const char *path, const char *arg, .../*,(char *) NULL, char * const envp[] */);
int execv(const char *path, char *const argv[]);
int execvp(const char *file, char *const argv[]);
int execvpe(const char *file, char *const argv[], char *const envp[]);

l (list):命令行参数以可变长参数指定,并且以NULL结尾 (类似指针数组)
p (PATH):只需要指定可执行程序的文件名,会根据PATH环境变量搜索可执行程序
e (environment):会替换(重新设置)当前进程的环境变量 (环境变量的保存以指针数组保存)
v (vector):命令行参数以数组的形式指定,并以NULL结尾

①execl:第一个参数是路径path,根据输入的path找可执行程序,后面的参数是命令行参数(命令行及选项),以NULL结尾 (不定长)
②execlp:从系统环境变量PATH里找可执行程序,第一个参数是可执行程序名,后面的参数是命令行参数(命令行及选项),以NULL结尾 (不定长)
③v:命令行以定长数组保存,
④e:可以替换环境变量


2.环境变量environ的存储
二级指针,需要以NULL结尾
在这里插入图片描述

3.返回值
成功,不返回
失败,返回-1,并设置errno


4.exec的原理
①清除进程的代码段、数据段、堆、栈、上下文
②加载新的可执行程序,并设置代码段、数据段
不会创建新的进程,pid、parent pid不变。从新可执行程序的main函数的第一行开始执行。


5.exec的惯用法:
①先fork()
②子进程执行新的可执行程序
③父进程等待子进程结束

pid_t pid = fork();

switch(pid){
case -1:
	error(1, errno, "fork");
case 0:
	//子进程执行新的可执行程序
	execlp("sh", "sh", "-c", cmd, NULL);
	error(1, errno, "exelp");
default:
	//父进程等待子进程结束
	waitpid(pid, NULL, 0);
}

6.作业:实现一个简易的shell(命令行解释器)

for( ; ;){
	//读取用户输入的命令
	
	pid_t pid = fork();
	
	switch(pid){
	case -1:
		error(1, errno, "fork");
	case 0: 
		//子进程执行新的可执行程序
		
	default:
		//父进程等待子进程结束
		
	}
}

7.strtok()
(1)原理图

碰到分隔符,将分隔符改成’\0’,cur++,返回start

(2)demo代码



(四) 进程之间的通信 (IPC)

进程间通信 (interprocess communication,IPC):
①管道
②信号
③消息队列
④共享内存 + 信号量
⑤套接字

在这里插入图片描述


1.管道

1.管道
①管道是内核管理的数据结构,管道在内核中。
②一端是写端,另一端是读端。
③一根管道是单工的通信方式,两根管道是全双工通信方式。

在这里插入图片描述

2.阻塞点
①open阻塞:管道的读端和写端必须同时就绪,open才会返回。否则一直阻塞。
②read阻塞:当写端写入(write)数据时,读端才解除阻塞。否则会一直阻塞。

3.用两根管道实现全双工通信:点对点的聊天系统
①注意1,打开两个管道的顺序要一致,先打开同一根管道的读端和写端,否则会死锁。
②注意2,聊天一卡一卡的,输出消息时才能接收到消息。
原因:一个执行流程有多个阻塞点
解决办法:一个执行流程最多只能有一个阻塞点
③注意3,当管道的写端关闭时,读端可以读到剩余数据。如果数据都读完了,读端会读到EOF,read会返回0
④注意4,如果读端关闭,往管道写数据,内核会发送SIGPIPE信号。
【读端关闭的管道,称为broken pipe】


解决方案:使用 signal()将SIGPIPE信号忽略

//直接忽略掉SIGPIPE信号 (注册SIGPIPE信号)
signal(SIGPIPE, SIG_IGN);

在这里插入图片描述


(1)有名管道:FIFO,named pipe

1.mkfifo 管道名:创建一个有名管道


例题:使用有名管道实现远程拷贝的功能. (一个进程读文件,然后通过管道输送给另一个进程, 另一个进程写文件)。

思路:send_file.c 读源文件内容,写到管道;recv_file.c读管道,将内容写到目标文件。

//send_file.c 
#include <func.h>
#include <stdio.h>
#define MAXSIZE 1024

int main(int argc, char* argv[])
{
    if(argc != 2){
        error(1, 0 ,"Usage:%s filename",argv[0]);
    }
    
    int fd_file = open(argv[1], O_RDONLY);
    if(fd_file == -1){
        error(1, errno, "open file");
    }

    int fd_fifo = open("fifo", O_WRONLY);
    if(fd_fifo == -1){
        error(1 ,errno, "open fifo");
    }

    char buffer[MAXSIZE];
    int nbytes;
    while((nbytes = read(fd_file, buffer, MAXSIZE)) > 0){
        write(fd_fifo, buffer, nbytes);
    }
    
    close(fd_file);
    close(fd_fifo);
    
    return 0;
}
//rev_file.c 
#include <func.h>
#include <stdio.h>
#define MAXSIZE 128

int main(int argc, char* argv[])
{
    if(argc != 2){
        error(1 ,0, "Usage:%s filename",argv[1]);
    }
    
    int fd_file = open(argv[1], O_RDWR | O_CREAT | O_TRUNC, 0666);
    if(fd_file == -1){
        error(1, errno, "open file");
    }

    int fd_fifo = open("fifo", O_RDONLY);
    if(fd_fifo == -1){
        error(1 ,errno, "open");
    }
    
    char buffer[MAXSIZE];
    int nbytes;
    while((nbytes = read(fd_fifo, buffer, MAXSIZE)) > 0){
        write(fd_file, buffer, nbytes);
    }   
    
    close(fd_file);
    close(fd_fifo);

    return 0;
}


(2)无名管道(匿名管道):pipe

1.概念
①无名管道,在文件系统上没有名字。(无名管道在文件流上是没有名字的。)
②无名管道只能用于有亲缘关系的进程之间通信,一般为父子关系。 [有亲缘关系就行,兄弟关系也可以]
③匿名管道适用于传递简单的文本或二进制数据。


2.系统调用:pipe()

#include <unistd.h>

int pipe(int pipefd[2]);
int pipefd[2];
if(pipe(pipefd) == -1){
	error(1, errno, "pipe");
}

3.返回值
①成功,返回0
②失败,返回-1,并设置errno

4.原理
读端关联到pipe[0]写端关联到pipe[1]
②分配fd,写给用户态空间。
在这里插入图片描述

自己给自己发消息

#include <func.h>

//自己与自己进行通信
int main(int argc, char* argv[])
{
    int pipefd[2];
    if(pipe(pipefd) == -1){
        error(1, errno, "pipe");
    }

    printf("pipefd[0] = %d, pipefd[1] = %d\n", pipefd[0], pipefd[1]);

    char buf[1024];

    write(pipefd[1], "Hello from pipe.",17); //记得为'\0'留出空间

    read(pipefd[0], buf, 1024);
    puts(buf);

    return 0;
}

在这里插入图片描述


5.父子进程通信的惯用法
①先pipe
②后fork()    [子进程会复制父进程的文件描述符列表]
③父进程关闭管道的一端
④子进程关闭管道的另一端


①父子进程的半双工通信(一根管道)
#include <func.h>

int main(int argc, char* argv[])
{
    //1.先pipe()
    int pipefd[2];
    if(pipe(pipefd) == -1){
        error(1, errno, "pipe");
    }
    
    //2.后fork()
    char buffer[1024];

    switch(fork()){
    case -1:
        error(1, errno, "fork");
    case 0:
        //4.子进程关闭管道的另一端
        close(pipefd[1]);  //子进程关闭写端
        read(pipefd[0], buffer, 1024);
        printf("Child:%s\n", buffer);
        exit(0);
    default:
        //3.父进程关闭管道的一端
        close(pipefd[0]);  //父进程关闭读端
        /* sleep(2); */
        write(pipefd[1], "Hello from parent", 18);
        exit(0);
    }
    
    return 0;
}

在这里插入图片描述


②父子进程的全双工通信(两根管道)
#include <func.h>

int main(int argc, char* argv[])
{
    int pipefd1[2];
    int pipefd2[2];
    if(pipe(pipefd1) == -1){
        error(1, errno, "pipe(pipefd1)");
    }
    if(pipe(pipefd2) == -1){
        error(1, errno, "pipe(pipefd2)");
    }
    
    char buffer1[1024] = {0};
    char buffer2[1024] = {0};

    switch(fork()){
    case -1:
        error(1, errno, "fork");
    case 0:  //子进程
        close(pipefd2[0]);  //子进程关闭pipe2的读端
        write(pipefd2[1], "hello from child", 17);
        
        close(pipefd1[1]);  //子进程关闭pipe1的写端
        read(pipefd1[0], buffer1, 1024);  //子进程从pipe1读数据
        printf("Child: %s\n", buffer1);
        
        exit(0);  //子进程退出,否则会执行default部分代码
    default: //父进程
        close(pipefd2[1]);  //父进程关闭pipe2的写端
        read(pipefd2[0], buffer2, 1024);  //父进程从pipe2读数据
        printf("Parent: %s\n", buffer2);
        
        close(pipefd1[0]);  //父进程关闭pipe1的读端
        write(pipefd1[1], "Hello from parent", 18); //父进程向pipe1写数据
    }

    return 0;
}

在这里插入图片描述


③select_pipe 实现点对点聊天
mkfifo pipe1
mkfifo pipe2
//select_p1.c
#include <func.h>
#include <stdio.h>

#define MAXLINE 256

int main(int argc, char* argv[])
{
    int fd1 = open("pipe1", O_WRONLY);
    if(fd1 == -1){
        error(1, errno, "open pipe1");
    }

    int fd2 = open("pipe2", O_RDONLY);
    if(fd2 == -1){
        error(1, errno, "open pipe2");
    }

    printf("Estalibshed.\n");

    char recvline[MAXLINE];
    char sendline[MAXLINE];

    fd_set mainfds; //局部变量,定义一个文件描述符集合
    FD_ZERO(&mainfds);  //清空,将所有的位 置为0
    FD_SET(STDIN_FILENO, &mainfds);
    int maxfds = STDIN_FILENO;

    FD_SET(fd2, &mainfds);
    if(fd2 > maxfds){
        maxfds = fd2;
    }
    
    for( ; ; ){
        fd_set readfds = mainfds;  //结构体复制

        int events = select(maxfds + 1, &readfds, NULL, NULL, NULL);
        switch(events){
        case -1:
            error(1, errno, "select");
        case 0:
            //超时
            printf("TIMEOUT\n");
            continue;
        default:    //返回就绪事件的个数
            //STDIN_FILENO 就绪
            if(FD_ISSET(STDIN_FILENO, &readfds)){
                //一定不会阻塞
                fgets(sendline, MAXLINE, stdin);
                write(fd1, sendline, strlen(sendline) + 1);  // +1: '\0'
            }
            //pipe2就绪
            if(FD_ISSET(fd2, &readfds)){
                //一定不会阻塞
                int nbytes = read(fd2, recvline, MAXLINE);
                switch(nbytes){
                case 0:
                    //管道的写端关闭了
                    goto end;
                case -1:
                    error(1, errno, "read pipe2");
                default:
                    printf("from p2: %s", recvline);
                }
            }
        }
    }   
end:
    close(fd1);
    close(fd2);

    return 0;
}
//select_p2.c
#include <func.h>
#include <stdio.h>

#define MAXLINE 256

int main(int argc, char* argv[])
{
    int fd1 = open("pipe1", O_RDONLY);
    if(fd1 == -1){
        error(1, errno, "open pipe1");
    }

    int fd2 = open("pipe2", O_WRONLY);
    if(fd2 == -1){
        error(1, errno, "open pipe2");
    }

    printf("Estalibshed.\n");

    char recvline[MAXLINE];
    char sendline[MAXLINE];

    fd_set mainfds; //局部变量,定义一个文件描述符集合
    FD_ZERO(&mainfds);  //清空,将所有的位 置为0
    FD_SET(STDIN_FILENO, &mainfds);
    int maxfds = STDIN_FILENO;

    FD_SET(fd1, &mainfds);
    if(fd1 > maxfds){
        maxfds = fd1;
    }
    
    for( ; ; ){
        fd_set readfds = mainfds;  //结构体复制

        int events = select(maxfds + 1, &readfds, NULL, NULL, NULL);
        switch(events){
        case -1:
            error(1, errno, "select");
        case 0:
            //超时
            printf("TIMEOUT\n");
            continue;
        default:    //返回就绪事件的个数
            //STDIN_FILENO 就绪
            if(FD_ISSET(STDIN_FILENO, &readfds)){
                //一定不会阻塞
                fgets(sendline, MAXLINE, stdin);
                write(fd2, sendline, strlen(sendline) + 1);  // +1: '\0'
            }
            //pipe2就绪
            if(FD_ISSET(fd1, &readfds)){
                //一定不会阻塞
                int nbytes = read(fd1, recvline, MAXLINE);
                switch(nbytes){
                case 0:
                    //管道的写端关闭了
                    goto end;
                case -1:
                    error(1, errno, "read pipe1");
                default:
                    printf("from p1: %s", recvline);
                }
            }
        }
    }   
end:
    close(fd1);
    close(fd2);

    return 0;
}

2.信号

(1)信号的概念
①信号是什么

1.信号异步的事件通知机制。 [例如select是一种IO事件通知机制]
信号是应用程序感知外界的桥梁。
原理:事件源发生了事件,内核发送信号给应用程序。

在这里插入图片描述


②信号的特点

①不稳定的:处于pending状态的信号,可能会丢失
②异步的:什么时候收到信号是不确定的,收到信号后会立刻马上执行信号处理函数
③不同系统信号的语义是不一样的


(2)产生信号的4个事件源

1.硬件:
①访问非法的内存:SIGSEGV (段错误,segment volation)
②执行非法的指令:SIGILL (illegal)
③算数异常 (除0):SIGFPE (浮点异常,float point exception)

2.内核:
①写一个读端关闭的管道 (broken pipe):SIGPIPE

3.应用程序:
①自己调用abort():SIGABRT
②子进程终止:SIGCHLD

4.用户:
①crtl +C:SIGINT,终止进程
②crtl + \:SIGQUIT,终止进程,并生成核心转储文件 (core dump)
③crtl + Z:SIGTSTP,暂停进程,将进程挂起到后台
④kill命令:kill -SIGINT 子进程pid


(3)内核会感知事件,并给进程发送相应的信号

①事件源产生事件
②内核感知事件的发生,产生信号,先pending状态 (未决信号),在下次调度进程时,将信号发生给进程
③进程收到信号,会立刻执行 信号处理函数handler

在这里插入图片描述


(4)信号

1.man 7 signal
(1)默认处理方式:signal dispositons (不捕获信号的情况)
在这里插入图片描述

(2)标准信号:standard signal
SIGKILL:不能被捕获,杀死进程
SIGSTOP:不能被捕获,暂停进程

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


2.kill -l
在这里插入图片描述
SIGSEGV:段错误 (segment volation)
①尝试访问未分配的内存地址。
②尝试写入只读内存区域。
③访问超过数组边界的内存。
④解引用空指针或无效指针。


(5)信号的执行流程

信号是异步的,什么时候收到信号是不确定的。
一收到信号,就会立刻执行 信号处理函数handler

在这里插入图片描述


(6)使用信号的3个步骤

步骤:①用signal注册信号处理函数 ②编写handler函数 ③发送信号
但有两个信号不可被捕获:SIGKILL、SIGSTOP


注册信号处理函数:signal

signal:注册信号处理函数,捕获信号

#include <signal.h>

void (*signal(int signum, void (*handler)(int)))(int);

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

signal(SIGINT, SIG_IGN);

第二个参数:handler是信号处理函数,也可以用两个宏
SIG_IGN:忽略
SIG_DFL:默认


在这里插入图片描述


②编写信号处理函数:handler
#include <func.h>

void handler(int signo){
    switch(signo){
    case SIGINT:
        printf("Caught SIGINT\n");
        break;
    case SIGTSTP:
        printf("Caught SIGTSTP\n");
        break;
    case SIGQUIT:
        printf("Caught SIGQUIT\n");
        break;
    default:
        printf("Unknown %d\n",signo);
    }
}

int main(int argc, char* argv[])
{
    //注册信号处理函数(捕获信号)
    sighandler_t oldhandler = signal(SIGINT, handler);
    
    if(oldhandler == SIG_ERR){
        error(1, errno, "signal %d", SIGINT);
    }

    oldhandler = signal(SIGTSTP, handler);
    if(oldhandler == SIG_ERR){
        error(1, errno, "signal %d", SIGTSTP);
    }

    oldhandler = signal(SIGQUIT, handler);
    if(oldhandler == SIG_ERR){
        error(1, errno, "signal %d",SIGQUIT);
    }
    
    for( ; ; ){
        sleep(1);
    }

    return 0;
}

发送信号:kill (pid, signo)

1.shell命令:kill命令
kill -SIGKILL pid, ...kill -信号编号 pid1 pid2 ...

在这里插入图片描述


2.系统调用:kill() 函数
(1)函数原型

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

int kill(pid_t pid, int signo);

pid:
①>0:给指定的进程发送信号
②=0:给同进程组的所有进程发送信号
③-1:给所有有权限的进程 (能够发送信号的进程) 发送信号,除了init进程
④<-1:给指定进程组的进程发送信号

(2)返回值
①成功,返回0 (有一个就算成功)
②失败,返回-1,并设置errno


3.库函数:raise()
(1)函数原型
在这里插入图片描述

(2)返回值:
成功,返回0
失败,返回非0

(3)作用:

raise(signo) 相当于  kill(getpid(), signo)

raise函数用于向当前进程发送信号。换句话说,它是给自己(当前进程)发送信号。通过raise函数,程序可以引发一个信号,从而调用预先定义的信号处理程序。这在模拟某些异常或中断处理场景时非常有用。


(7)sleep()

执行态到阻塞态,睡若干秒
头文件:#include <unistd.h>


3.文件、套接字、信号量、内存映射、共享内存、消息队列、消息传递接口、数据库



(五) 其他

1.crtl+D、crtl+C、crtl+Z

①ctrl + D:EOF,文件结束/输入结束
②ctrl + C:终止进程。[用户按下 ctrl+c ,将导致内核向进程发送一个 SIGINT 的信号]
③ctrl + Z:暂停,挂起进程,放入后台。挂起的进程可以通过命令 fg(将进程恢复到前台运行)或 bg(在后台继续运行)来管理。[SIGTSTP]
④crtl + \:退出进程 [SIGQUIT]


2.printf()加不加\n的区别

用户态缓冲区:分别给stdin、stdout、stderr流分了一部分
刷新用户态缓冲区,是将用户态缓冲区的内容写回内核态缓冲区,再写入dev1文件。

  • 24
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
在 Windows 下使用 C++ 编程语言,如果需要使用 HDF5 库中的 H5::H5File 类,则需要完成以下步骤: 1. 下载并安装 HDF5 库:可以从 HDF Group 的官方网站(https://www.hdfgroup.org/downloads/hdf5/)下载 HDF5 库的 Windows 版本,然后按照安装向导完成安装过程。 2. 配置编译器环境:在 C++ 项目中,需要将 HDF5 库的头文件路径和库文件路径添加到编译器的搜索路径中。在 Visual Studio 等集成开发环境中,可以通过以下方式完成配置: - 在项目属性中设置 C/C++ 编译器的附加包含目录,将 HDF5 库的 include 目录添加到列表中; - 在项目属性中设置链接器的附加库目录,将 HDF5 库的 lib 目录添加到列表中; - 在项目属性中设置链接器的附加依赖项,将需要使用的 HDF5 库的名称添加到列表中,例如:hdf5_cpp.lib。 3. 在代码中包含头文件:在需要使用 H5::H5File 类的源文件中,需要包含 HDF5 库的头文件,例如: ```c++ #include <H5Cpp.h> ``` 4. 创建 H5::H5File 对象:使用 H5::H5File 类的构造函数可以创建一个 HDF5 文件对象,例如: ```c++ H5::H5File file("test.h5", H5F_ACC_TRUNC); ``` 这样就创建了一个名为 “test.h5” 的 HDF5 文件,并且设置了文件的访问权限为 H5F_ACC_TRUNC,即如果文件已经存在,则会被截断清空。 以上就是在 Windows 下使用 C++ 编程语言,使用 HDF5 库中的 H5::H5File 类的基本步骤。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

程序员爱德华

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值