文章目录
- 二、进程
- 1.CPU的虚拟化
- 2.进程命令
- (1)`ps`
- 3.进程的基本操作 (API)
- (1)获取进程的标识 (获得进程id):getpid、getppid
- (2)创建进程:fork()
- (3)终止进程:exit()、_exit()、abort()、wait()、waitpid()
- ①正常终止:exit()、_exit()
- ②异常终止:abort()
- (4)进程控制
- ①孤儿进程 (Orphan Process)
- ②僵尸进程
- ③wait
- ④waitpid
- (5)执行程序:exec函数簇
- 4.进程之间的通信 (IPC)
- (1)管道
- ①有名管道:FIFO,named pipe
- ②无名管道(匿名管道):pipe
- (2)共享内存
- (3)信号量
- (4)消息队列
- 5.IO多路复用 (I/O multilplexing)
- 1.select
- 6.信号
- (1)产生信号的4个事件源
- (2)内核会感知事件,并给进程发送相应的信号
- (3)信号
- (4)信号的执行流程
- (5)使用信号的流程
- ①注册信号处理函数:捕获信号
- ②发送信号
- (6)sleep()
- 7.其他
- (1)crtl+D、crtl+C、crtl+Z
- (2)printf()加不加\n的区别
二、进程
1.模型:为什么要抽象出进程?
为了进一步压榨计算机的资源。
要求:进程间隔离
2.进程是什么?
①用户角度:进程是正在执行的程序
②内核角度:进程是要执行的任务,进程是资源分配的最小单位
3.struct proc:
pid、parent、cwd、占用的内存资源、占用的外部设备资源(ofile)、CPU的状态(context)
4.如何实现进程的隔离?
时分共享。
(1)底层机制:
①指标:性能、安全、控制权
②方案一、方案二、方案三
(2)上层策略:
1.CPU的虚拟化
1.内核的职责:管理硬件资源
2.操作系统的发展:
(1)批处理系统:①队头等待 ②资源利用率低
(2)分时系统:
(3)多任务处理系统:
3.如何共享资源:时分共享(CPU)、空分共享(内存)
(1)时分共享策略:一个进程占用CPU一段时间(时间片),然后切换到另一个进程执行。但进程间的切换会有开销,进程上下文切换开销。
(2)实现:①底层机制:上下文切换 ②上层策略:调度算法(选择哪一个进程执行)
4.进程
(1)用户角度:进程就是正在执行的程序
内核角度:进程是要执行的任务。
我们不希望一个进程失败会影响另外的进程,所以进程之间必须隔离,进程之间是相互之间看不到的,感知不到其他的进程存在。
从进程的角度看,就好像它独占计算机的所有资源。
抽象机制,就是CPU的虚拟化。
5.上下文,CPU的状态依靠寄存器保存,体现了进程的动态特点
上下文切换的时机:①调用系统调用 ②切换进程
6.系统调用
7.如何实现进程的切换
(1)第一种模式:仅有内核态
效率高,但不安全
(2)第二种模式:用户态、内核态 (引入了CPU的模态机制,为了安全考虑)
用户态使用系统调用时,切换到内核态。但用户态可以不使用系统调用,导致一直不切回核心态。
(3)第三种模式:时钟中断 + 用户态、核内核态 (引入了时钟设备)
①协作式:yield(),进程让出使用权
②抢占式(非协作式):引入硬件时钟设备,时钟中断(几毫秒),执行时钟中断处理函数,切换到内核态,操作系统拿回控制权。时间片应当是时钟中断的整数倍。
①进程是资源分配的最小单位
②进程是隔离的,进程无法感知内核和其他进程
2.进程命令
(1)ps
进程快照:process snapshot
①ps
:显示与该终端相关的进程快照 (TTY是关联 远程控制终端)
②ps x
:和该用户相关的进程
③ps ux
ps aux
(all user)
④查进程的pid:ps aux | grep "./可执行程序名"
STAT:
S:阻塞
s:会话进程
I I I:空闲
<:高优先级
n:低优先级
+:前端
l l l:多线程
2.top
类似windows的任务管理器,每隔3秒更新信息
3.pstree
打印进程树,显示进程间的父子关系
4.前台进程 vs 后台进程
①前台进程:没有返回,但与控制终端关联
②后台进程:不会与终端控制,命令 &
jobs
:查看所有的后台进程
fg 任务编号
:将后台进程放到前端 foreground
3.进程的基本操作 (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() 终止
②异常终止:最终由信号导致终止
①正常终止: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);
②异常终止:abort()
(4)异常终止:abort()
内核会给该进程发生SIGABRT信号,会导致进程异常终止。会产生coredump文件。
(5)信号导致终止:
收到信号,信号导致进程异常终止
(4)进程控制
①孤儿进程 (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();
}
②僵尸进程
1.僵尸进程 (Zombie Process)
僵尸进程:子进程死亡,但父进程没有被回收
僵尸进程:已经终止,但其终止状态尚未被父进程获取的进程。
僵尸进程:已经终止但其父进程尚未调用 wait() 或 waitpid() 系统调用读取其退出状态的进程。
当一个进程死亡时,绝大部分信息会被释放,而有一些信息会保存在内核 (pid、退出状态、CPU时间),方便父进程以后查看这些信息。并且给父进程发给SIGCHLD信号(告诉父进程,孩子已死),但父进程默认会忽略信号。
2.父进程如何回收僵尸进程?
答:父进程手动调用wait() 和 waitpid()
3.僵尸进程不处理会造成什么影响?
子进程的proc结构体没有被回收,导致没有pid可用
僵尸进程本身不会消耗大量系统资源,但如果有大量僵尸进程未被清理,进程描述符的数量会逐渐增多,可能导致系统无法为新的进程分配进程描述符,进而影响系统性能。[大量僵尸进程会导致进程描述符耗尽]
③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;
}
④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代码
4.进程之间的通信 (IPC)
进程间通信 (interprocess communication,IPC):
①管道
②信号
③消息队列
④共享内存 + 信号量
⑤套接字
(1)管道
1.管道
①管道是内核管理的数据结构,管道在内核中。
②一端是写端,另一端是读端。
③管道是半双工的通信方式
2.阻塞点
①open阻塞:管道的读端和写端必须同时就绪,open才会返回。否则一直阻塞。
②read阻塞:当写端写入(write)数据时,读端才解除阻塞。否则会一直阻塞。
3.用两根管道实现全双工通信:点对点的聊天系统
①注意1,打开两个管道的顺序要一致,先打开同一根管道的读端和写端,否则会死锁。
②注意2,聊天一卡一卡的,输出消息时才能接收到消息。
原因:一个执行流程有多个阻塞点
解决办法:一个执行流程最多只能有一个阻塞点
③注意3,当管道的写端关闭时,读端可以读到剩余数据。如果数据都读完了,读端会读到EOF,read会返回0
④注意4,如果读端关闭,往管道写数据,内核会发送SIGPIPE信号。
【读端关闭的管道,称为broken pipe】
①有名管道: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;
}
②无名管道(匿名管道):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() [子进程会复制父进程的文件描述符列表]
③父进程关闭管道的一端
④子进程关闭管道的另一端
(1)父子进程的半双工通信(一根管道)
#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;
}
(2)父子进程的全双工通信(两根管道)
#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;
}
(2)共享内存
(3)信号量
(4)消息队列
5.IO多路复用 (I/O multilplexing)
5种IO模型:
①阻塞IO:[类似独占查询]
②非阻塞式IO:配合轮询。[类似定时查询]
③IO多路复用:监听多个I/O事件,将多个阻塞点变成一个阻塞点。select、poll、epoll
④信号驱动IO:阻塞点主动发生信号,是异步方式。[类似中断]
⑤异步IO:不需要CPU主动处理 [类似DMA方式]
1.select
1.函数原型
#include <sys/select.h>
int select(int nfds, fd_set *readfds, fd_set *writefds,
fd_set *exceptfds, struct timeval *timeout);
select(6, &readfds, &writefds, NULL, &timeout)
①nfds:监听的最大文件描述符+1 [为了内核提升效率,只检查前几nfds个文件描述符]
②readfds:传入(调用时),表示对哪些文件描述符的读事件感兴趣 ;传出(函数返回时),读事件已就绪的文件描述符
③writefds:传入(调用时),表示对哪些文件描述符的写事件感兴趣 ;传出(函数返回时),写事件已就绪的文件描述符
④exceptfds:传入(调用时),表示对哪些文件描述符的异常事件感兴趣 ;传出(函数返回时),发生异常事件的文件描述符
⑤timieout:超时时间,最多阻塞的时间长度。[超过就不要了]。
i.定时等待:{秒,微秒}
ii.无限等待:若为NULL,则无限期阻塞
iii.立即返回:若为{0,0},则不阻塞,立刻返回
timieout也是传入传出参数,传入时是超时时间,传出时是剩余时间。返回值为0,则表示超时(时间用完,但没有时间就绪)。
fds:文件描述符集合 (file descriptor set)
select是同步的,当select返回时,说明有事件就绪了。【中途会切换到其他进程,到下次select检查时,若有一个或多个事件就绪,就返回。若超时,返回0。若未超时,继续切换其他进程等待(事件就绪)】
多个阻塞点,变成了select一个阻塞点
2.数据类型
(1)fd_set:传入传出参数(指针),大小为1024的位图
①FD_ZERO(&set)
②FD_SET(fd, &set)
③FD_ISSET(fd, &set)
④FD_CLR(fd, &set)
(2)struct timeval:{tv_sec, tv_usec}
3.select的返回时机
①有事件就绪
②超时时间到达
③被信号中断
4.返回值
①成功,返回就绪事件的个数。
②成功,如果超时,返回0
③失败,返回-1,并设置errno
5.原理
select 是一种系统调用,用于在多个文件描述符上进行多路复用,以便监视多个文件描述符的可读、可写或异常状态。
6.select的缺陷
①监听数量有限:监听的文件描述符的个数是有限的。fd_set是大小为1024的位图,最大只能监听1024个fd。
②效率低:返回值只能表示就绪的事件数量,但不知道具体是哪个事件就绪。需要遍历fd_set,找到就绪的文件描述符。时间复杂度为O(n)。(若场景为10万个事件在连接,但只有10个事件就绪,也需要遍历10万个事件)
7.select 的应用场景
select 主要用于网络服务器和客户端,允许在单线程中高效处理多个连接或文件描述符的 I/O 操作。
select 主要用于需要同时监视多个文件描述符的场景,如网络服务器需要同时处理多个客户端连接。通过使用 select,程序可以在一个线程中处理多个连接,而不需要为每个连接创建一个线程,从而减少资源开销。
8.select介绍
8.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;
}
6.信号
1.信号是异步的事件通知机制。 [例如select是一种IO事件通知机制]
信号是应用程序感知外界的桥梁。
原理:事件源发生了事件,内核发送信号给应用程序。
2.信号的特点:
①不稳定
②异步的 (什么时候收到信号是不确定的,收到信号后会立刻马上执行信号处理函数)
③信号的语义,在不同系统中不一样
(1)产生信号的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
(2)内核会感知事件,并给进程发送相应的信号
①事件源产生事件
②内核感知事件的发生,产生信号,先pending (未决信号),在下次调度进程时,将信号发生给进程
③进程收到信号,会立刻处理
(3)信号
1.man 7 signal
(1)默认处理方式:signal dispositons (不捕获信号的情况)
(2)标准信号:standard signal
SIGKILL:不能被捕获,杀死进程
SIGSTOP:不能被捕获,暂停进程
2.kill -l
SIGSEGV:段错误 (segment volation)
①尝试访问未分配的内存地址。
②尝试写入只读内存区域。
③访问超过数组边界的内存。
④解引用空指针或无效指针。
(4)信号的执行流程
signal:信号处理函数,捕获信号
信号是异步的,什么时候收到信号是不确定的。
signal(SIGINT, SIG_IGN);
第二个参数:handler是信号处理函数,也可以用两个宏
SIG_IGN
:忽略
SIG_DFL
:默认
(5)使用信号的流程
①注册信号处理函数:捕获信号
步骤:①注册信号 ②编写handler函数
但有两个信号不可被捕获:SIGKILL、SIGSTOP
#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;
}
②发送信号
1.kill命令
kill -SIGKILL pid, ...
或 kill -信号编号 pid1 pid2 ...
2.系统调用:kill
(1)函数原型
#include <sys/types.h>
#include <signal.h>
int kill(pid_t pid, int sig);
pid:
①>0:给指定的进程发送信号
②=0:给同进程组的所有进程发送信号
③-1:给所有能够发送信号的进程发送信号 (除了init)
④<-1:给指定进程组的进程发送信号
(2)返回值
①成功,返回0 (有一个就算成功)
②失败,返回-1,并设置errno
3.库函数:raise()
(1)函数原型
(2)返回值:
成功,返回0
失败,返回非0
(3)作用:
raise函数用于向当前进程发送信号。换句话说,它是给自己(当前进程)发送信号。通过raise函数,程序可以引发一个信号,从而调用预先定义的信号处理程序。这在模拟某些异常或中断处理场景时非常有用。
(6)sleep()
执行态到阻塞态,睡若干秒
头文件:#include <unistd.h>
7.其他
(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文件。