I\O进程线程(Day29)
一、学习内容
-
文件IO
系统调用中,对文件操作,是基于文件描述符进行的,而标准IO是使用的文件指针进行的。-
关于文件描述符的拷贝
-
使用变量进行文件描述符的拷贝,两个文件描述符变量本质上操作的是同一个文件描述符,因此共享同一个文件的文件光标
-
dup(拷贝文件描述符)
int dup(int oldfd);-
注意
新旧文件描述符共享同一个文件的文件指针,当其中一个文件描述符关闭后,不影响另一个文件描述符的使用 -
返回值
成功返回新文件描述符,失败返回-1并置位错误码 -
参数
旧文件描述符 -
功能
拷贝旧的文件描述符,并产生一个新的文件描述符,新的文件描述符符合最小未使用原则
-
-
dup2函数(拷贝文件描述符)
int dup2(int oldfd, int newfd);-
返回值
成功返回新文件描述符的值,失败返回-1并置位错误码 -
参数
参数1:旧文件描述符
参数2:新文件描述符 -
功能
将旧文件描述符拷贝给新文件描述符,结果为就文件描述符和新文件描述符都指向就文件描述符对应的文件,如果新文件描述符之前已经打开了一个文件,拷贝之前将其进行关闭
-
-
多次使用open函数打开同一个文件,每个文件描述符是独立使用自己文件的文件光标
-
-
lseek(光标移动)
off_t lseek(int fd, off_t offset, int whence);-
返回值
成功返回光标当前所在位置,失败返回(off_t)-1,并置位错误码 -
参数
参数1:文件描述符
参数2:偏移量
=0:不偏移
<0:向左偏移
>0:向右偏移
参数3:偏移的起始位置
SEEK_END:从末尾开始偏移
SEEK_CUR:从当前位置开始偏移
SEEK_SET:从文件头开始偏移 -
功能
将fd描述符指向文件中的光标进行偏移
-
-
write(写数据)
ssize_t write(int fd, const void *buf, size_t count);-
返回值
成功返回写入非字节个数,失败返回-1,并置位错误码 -
参数
参数1:文件描述符
参数2:要写入的内容
参数3:写入的字节个数 -
功能
将buf的内容写入到指定的文件中去
-
-
read(读取数据)
ssize_t read(int fd, void *buf, size_t count)-
返回值
成功返回读取的字节个数,如果读取到文件末尾返回0,失败返回-1,并置位错误码 -
参数
参数1:文件描述符
参数2:缓冲区buf,存储读取的内容
参数3:读取的字节个数 -
功能
从文件中读取内容放到缓冲区buf(任何形式)中
-
-
close(关闭文件)
int close(int fd);-
返回值
成功返回0,失败返回-1,并置位错误码 -
参数
文件描述符 -
功能
关闭用open打开的指定文件
-
-
open(打开文件)
int open(const char *pathname, int flags);
int open(const char *pathname, int flags, mode_t mode);-
返回值
返回一个文件描述符,文件描述遵循最小整数原则 -
参数
参数1:文件路径
参数2:打开文件的状态-
O_APPEND:追加
-
O_NONBLOCK:非阻塞
-
O_TRUNC:情空
-
O_EXCL:存在
-
O_CREAT:创建
若有O_CREAT,那么参数3必须加上
参数3:是系统给定的权限位与上取反的umask值得到的
文件夹的默认权限:0775 文件的默认权限:0664
-
-
功能
以指定方式打开指定文件
-
-
文件描述符
-
文件描述符是进行文件系统访问或者进行外部文件输入输出的操作句柄
-
2文件描述符在操作系统中使用的是一个非负整数表示
-
3使用open函数打开一个文件时,就会返回当前文件对应的文件描述符
-
4文件描述符的使用规则:最小未分配原则
-
一个程序默认能够打开的文件描述符个数为 1024 个,可以通过指令 ulimit -a 查看
-
特殊文件描述符:0、1、2 当程序启动后,系统会默认打开上面三个文件描述符,分别对应标准输入、标准输出、标准出错的文件指针的描述符
-
-
-
多进程
-
并发和并行的区别
-
并发
系统能处理多个任务,但不一定同时执行。在单核系统中,通过任务调度快速切换任务,实现宏观上的同时执行,旨在提高资源使用效率和系统响应速度。 -
并行
多个处理器或多核处理器同时处理多个任务或任务的不同部分,实现真正的同时执行,旨在缩短程序总执行时间,加速处理速度,尤其在大规模数据处理和高性能计算中重要。
-
-
进程的概念
-
进程是程序的一次执行过程
-
进程是程序资源分配的基本单位,系统会给每个进程分配4G的虚拟内存,分为0--3G的用户空间和3--4G的内核空间 多个进程共享内核空间,用户空间相互独立
-
3进程是一个动态的过程,有生命周期的概念,分为创建态、就绪态、运行态、阻塞态、死亡态
-
进程在内核空间中存储在一个名为task_struct的结构体中(PCB)
-
进程描述符
task_struct包含了描述一个进程所需的所有信息。 -
进程状态
包括运行、就绪、阻塞等状态 -
进程标识符
如进程ID(PID) -
进程调度信息
如优先级、调度策略等 -
内存管理信息
如虚拟地址空间、页表等 -
文件系统信息
如打开的文件、文件系统根目录等 -
信号处理
包括待处理信号和信号处理函数 -
进程间通信
如消息队列、共享内存等IPC机制相关信息 -
时间和定时器
如进程创建时间、CPU使用时间等 -
线程信息
在Linux中,线程被视为轻量级进程,也用task_struct表示
-
-
单核cpu处理多任务时,一般使用的是时间片轮询机制
-
进程与程序的区别:程序是静态的,是存储在磁盘上的二进制代码 进程是动态的,是有生命周期的
-
进程的组成:进程控制块(PCB)、数据段、程序段
-
-
进程的种类
-
交互进程
他是由shell控制,用于直接跟用户进行交互的进程。例如:vim编辑器、文本编辑器 -
批处理进程
本质上维护了一个队列,被放入队列中的进程会统一被调度。例如gcc编译器的一步到位的编译 -
守护进程
脱了了终端而存在的进程,随着系统的启动而开始,随着系统的结束而终止。例如:服务进程
-
-
进程号的概念
每个进程在系统中都有一个唯一的标识位,用一个整数表示,这就是该进程的进程号(PID)-
PPID(parent process ID):当前进程的父进程的进程号
-
PID(process ID):当前进程的进程号
-
-
特殊的进程
-
0号进程
也成为 idel 进程,他是操作系统启动后执行的第一个进程,这个进程也叫空闲进程,当没有其他进程执行时,系统会默认执行该进程。1号进程和2号进程都是由0号进程创建出来的。 -
1号进程
也称 init 进程,该进程由0号进程产生,主要完成系统创建时一些软件硬件的初始化工作。当其他进程的父进程死亡后,会托管其子进程 -
2号进程
也称 kthreadd,该进程由0号进程产生,也成为调度进程,当某个就绪进程时间片轮到时,该进程负责进程的调度工作 -
孤儿进程
当前进程的父进程死亡后,但是当前进程还没有结束,那么当前进程称为孤儿进程,孤儿进程会由1号进程收养 -
僵尸进程
当前进程已经死亡,但是其父进程没有为其收尸,那么该进程为僵尸进程
-
-
进程的相关指令
-
ps
-
ps -ef:显示进程之间的关系
-
ps -ajx:可以显示进程的状态
-
ps -aux:可以查看进程资源使用情况
-
-
top或htop
可以动态展示进程的占用情况 -
pstree
展示进程树,可以显示进程的父子关系 -
pidof 进程名
查看给定进程的进程号 -
kill
向进程发送信号,发信号的格式 kill -信号名(号) 进程号-
SIGSTOP、SIGTSTP
表示让一个进程暂停,当用户从键盘上键入 ctrl +z时,就会发送该信号 -
SIGCONT
该信号表示让暂停的进程继续执行 -
SIGCHLD
当一个进程的子进程退出时,会向该进程的父进程发送该信号,表示让其为子进程收尸 -
SIGALRM
当启动一个定时器,并且定时器超时时,会发送该信号 -
SIGPIPE
当操作管道文件时,如果管道的读端被关闭,写端继续写的话,就会出现管道破裂 -
SIGSEGV
表示指针访问越界时的段错误 -
SIGUSR1、SIGUSR2
没有特殊的含义,留给程序员使用 -
SIGKILL
表示杀死进程 -
SIGILL
当进程自己发生非法操作时,内核空间会发送该信号 -
SIGQUIT
当终端键入 ctrl + \ 时,表示终端要终止进程的结束 -
SIGINT
当终端键入 ctrl+c时,终端向指定进程发送的就是该信号,表示中断进程 -
SIGHUP
当进程所在的终端关闭时,该终端会向运行在该终端上的所有进程发送该信号,让其结束进程
-
-
-
进程的状态
-
主要状态一共有五个:创建态、就绪态、运行态、阻塞态、死亡态
-
程序中的进程的状态显示:可以通过指令 man ps查看进程的状态 进程的状态由两部分组成:主状态和附加态
-
主状态
-
D
不可中断的休眠态 (usually IO) -
R
运行态 (on run queue) -
S
可中断的休眠态 (waiting for an event to complete) -
T
暂停态,会给出作业号进行控制 -
t
程序调试时的暂停态 -
W
已经弃用 -
X
死亡态 (should never be seen) -
Z
僵尸态
-
-
附加态
-
<
高优先级的进程 (not nice to other users) -
N
低优先级的进程 (nice to other users) -
L
锁到内存中的进程 (for real-time and custom IO) -
s
会话组组长,默认为当前终端 -
l
包含多线程的进程 (using CLONE_THREAD, like NPTL pthreads do) -
+
表示是前台运行的进程
-
-
-
-
-
多进程编程
在linux中,每个进程都是拷贝其父进程资源而得到的,当父进程创建子进程时,父子进程资源除了有关进程属性之外的所有内容都是一样的。父子进程独立拥有各自的用户空间,并且共享内核空间。子进程的创建使用 fork 函数来实现-
waitpi(进程资源回收)
pid_t waitpid(pid_t pid, int *wstatus, int options)-
返回值
回收的当前进程的PID号 -
参数
-
参数1:要回收的进程pid号,可正可负可0
-
>0
回收指定的某个进程 -
=0
回收当前进程组中的任意一个子进程 -
=-1
表示回收任意一个子进程 -
<-1
回收进程组id为当前pid的绝对值的那个进程中的任意一个子进程
-
-
参数2:用于接受子进程退出时的状态,一般填NULL
-
参数3:回收选项
-
0
表示阻塞回收 -
WNOHANG
非阻塞回收
-
-
-
功能
阻塞或者非阻塞回收子进程资源
-
-
wait(进程资源回收)
pid_t wait(int *wstatus)-
返回值
回收的当前进程的PID号 -
参数
用于接受子进程退出时的状态,一般填NULL -
功能
阻塞等待子进程的退出,如果没有子进程退出,那么当前进程会一直阻塞等待
-
-
_exit(进程退出)
void _exit(int status)-
返回值
无 -
参数
退出时的状态-
EXIT_FAILURE:1
-
EXIT_SUCCESS:0
-
-
功能
退出当前进程,但是不会刷新所以标准io的缓冲区
-
-
exit(进程退出)
void exit(int status)-
返回值
无 -
参数
退出时的状态-
EXIT_FAILURE:1
-
EXIT_SUCCESS:0
-
-
功能
退出当前进程,并刷新所有标准io的缓冲区
-
-
getppid(父进程号获取)
pid_t getppid(void)-
返回值
当前进程的父进程进程号 -
参数
无 -
功能
获取当前进程的父进程进程号
-
-
getpid(进程号获取)
pid_t getpid(void)-
返回值
当前进程的进程号 -
参数
无 -
功能
获取当前进程的进程号
-
-
fork(复制子进程)
pid_t fork(void)-
返回值
对于父进程而言,会得到创建的子进程的进程号,对于子进程而言,会得到0,失败返回-1并置位错误码 -
参数
无 -
功能
通过复制父进程,创建出一个子进程
-
-
- 脑图
二、作业
作业1
使用父子进程完成两个文件的拷贝,父进程拷贝前一半,子进程拷贝后一半,两个进程同时进行
代码解答:
#include <myhead.h>
#define MAX 1024 // 定义最大缓冲区大小为1024
int main(int argc, const char *argv[])
{
int source = -1;
// 打开源文件 "1.txt",以只读模式打开
// 如果打开失败,输出错误信息并返回 -1
if ((source = open("./1.txt", O_RDONLY)) == -1)
{
perror("open source error");
return -1;
}
int dest = -1;
// 打开目标文件 "2.txt",以只写模式打开,
// 如果文件不存在则创建,并赋予权限 0664,
// 若打开失败,输出错误信息并返回 -1
if ((dest = open("2.txt", O_WRONLY | O_CREAT | O_TRUNC, 0664)) == -1)
{
perror("open dest error");
close(source); // 若目标文件打开失败,关闭源文件
return -1;
}
// 获取源文件的长度,lseek 设置文件指针到文件末尾,并返回偏移量(即文件长度)
int len = lseek(source, 0, SEEK_END);
if (len == -1) // 检查 lseek 是否出错
{
perror("lseek error");
close(source); // 关闭源文件
close(dest); // 关闭目标文件
return -1;
}
int half_len = 0;
// 计算文件一半的长度,处理奇数长度情况
if (len / 2 != 0)
{
half_len = (len + 1) / 2; // 如果文件长度为奇数,向上取整分配给前半部分
}
else
{
half_len = len / 2; // 文件长度为偶数,直接对半分
}
// 创建子进程,fork 返回值:
// 父进程中返回子进程ID,子进程中返回0,出错时返回-1
int pid = fork();
if (pid == -1) // fork() 错误处理
{
perror("fork error");
close(source); // 关闭源文件
close(dest); // 关闭目标文件
return -1;
}
// 父进程执行部分,拷贝文件前一半
if (pid > 0)
{
lseek(source, 0, SEEK_SET); // 重置文件指针到文件开头
char rbuf[half_len]; // 定义读取缓冲区大小为文件前一半的长度
while (1)
{
// 读取文件内容到缓冲区,返回读取的字节数
int a = read(source, rbuf, sizeof(rbuf));
if (a <= 0) // 当读取失败或读取完毕,跳出循环
{
break;
}
// 将读取到的内容写入目标文件
write(dest, rbuf, a);
}
}
// 子进程执行部分,拷贝文件后一半
else if (pid == 0)
{
lseek(source, half_len, SEEK_SET); // 将文件指针移动到文件中间位置
char rbuf2[half_len]; // 定义读取缓冲区大小为文件后一半的长度
while (1)
{
// 读取文件内容到缓冲区,返回读取的字节数
int a = read(source, rbuf2, sizeof(rbuf2));
if (a <= 0) // 当读取失败或读取完毕,跳出循环
{
break;
}
// 将读取到的内容写入目标文件
write(dest, rbuf2, a);
}
_exit(EXIT_SUCCESS); // 子进程退出,避免创建僵尸进程
}
// 父进程等待子进程结束,避免出现僵尸进程
wait(NULL);
// 关闭源文件和目标文件
close(source);
close(dest);
return 0;
}
成果展现:
三、总结
学习内容概述
1. 文件IO与系统调用
文件描述符和文件指针的区别,文件描述符的复制(`dup` 和 `dup2` 函数),文件光标的移动(`lseek` 函数),文件的打开、读取、写入、关闭操作。
2. 进程管理
进程的概念、种类和状态,进程的创建(`fork` 函数),进程号(`getpid`、`getppid`),进程退出(`exit` 和 `_exit`),以及进程资源的回收(`wait` 和 `waitpid`)。
3. 多进程编程
并发与并行的概念,以及如何通过父子进程的资源共享实现多进程编程。
学习难点
1. 文件描述符的拷贝和共享
`dup` 和 `dup2` 函数涉及文件描述符的复制,其中拷贝后共享同一文件的文件指针,这可能导致多个文件描述符对同一文件的不同位置读写,因此需要理解文件光标和偏移操作的细节。
2. 进程间关系和进程状态管理
需要掌握进程的不同状态,特别是僵尸进程和孤儿进程的处理。同时,理解`fork`函数如何复制父进程,以及`waitpid`和`wait`函数在回收子进程资源中的应用是关键。
主要事项
1. 文件描述符与文件指针
文件描述符是一个较低层的操作系统概念,操作时以整数形式存在,并与文件系统的资源进行交互。标准IO操作使用的是文件指针,理解二者的不同有助于选择适合的文件操作函数。
2. 进程资源的回收
使用`wait`或`waitpid`函数来阻塞或非阻塞地回收子进程资源,避免僵尸进程的出现。特别是当父进程未正确处理子进程退出时,子进程将进入僵尸状态,消耗系统资源。
未来学习重点
1. 信号处理与进程间通信(IPC):
在多进程编程中,进程之间的通信和同步是进一步的重点学习方向,包括管道、消息队列、共享内存等IPC机制的应用,以及信号的捕捉与处理。
2. 线程编程
除了进程的并发和并行,线程作为更轻量级的并发单元,也是需要深入学习的,尤其是如何通过线程共享数据和同步机制实现高效的并行执行。