1.写时拷贝的概念
在Linux系统中,调用fork系统调用创建子进程时,并不会把父进程所有占用的内存页复制一份,而是与父进程公用相同的内存页,当子进程或者父进程对内存页进行修时才会进行复制,这就是写时拷贝。
当父子进程都没有修改那几个页面 那子进程那几个页面就是共享的,什么时候修改了我们就才复制新的页面(也就是创建新的物理空间),其实提高了复制的效率。
2.实现原理
fork()之后,kernel(实时操作系统)把父进程中所有的内存页的权限都设为read-only,然后子进程的地址空间指向父进程。当父子进程都只读内存时,不做任何动作,当其中某个进程写内存时,CPU硬件检测到内存页是read-only的,于是触发页异常中断(page-fault),陷入kernel的一个中断例程。中断例程中,kernel就会把触发的异常的页复制一份,于是父子进程独立了。
举例:
fork()之后调用excve()。
由此引申一个问题 fork()是系统调用,那么系统调用和库函数调用有什么区别?
fopen -> open 系统调用
printf-> write
例如:printf打印时1. 库函数 ->系统调用->内核->硬件
2.也可以不用经过库函数 直接系统调用到内核
库函数本质是系统调用,本质上是一个接口(API),运行时属于用户态,开销小,而系统调用属于内核态,用户态和内核态的上下文切换开销比较大。
3.系统调用优点:
可以减少分配内存页(大量资源)带来的瞬时延时。
可减少不必要的资源分配。
4.系统调用的主要作用
为用户提供了一种硬件的抽象接口
保证了系统的稳定和安全
避免了用户直接对底层硬件的编程还有一个问题 :c编程时开辟的栈空间属于系统调用吗?
系统调用和库函数调用的区别
1、进程fork()后在open() 之后写入的过程中共享写入偏移量吗??非共享
** 2、进程open() 之后fork(),写入的过程中共享写入偏移量吗**??共享
PCB中有 一个文件表,在fork()过程中文件表也被复制了一份,所以先open()再fork()会发生偏移量的共享、。
当先fork()再open()时文件表产生的3号位置的指针对应的STRUCT FILE结构体都是各自产生的,所以不共享偏移量。
二、文件操作的系统调用 open read write close
**man
系统调用 用户空间进入到内核空间 (切换到内核态)
window 上才有 文本文件和二进制文件区分,linux并没有这种区分
open() //(文件名,打开方式,权限mode),
函数如下:
oflags
我们试着往文件中写入数据,如下图:
open 系统调用中的0600是0400(S_IRUSR:读权限)和0200(S_IWUSR:写权限)的结合
我们可以看到写入的数据是从头部开始写入的。
write()函数
这里是引用
write(fd(文件id信息,叫做文件描述符),“数据”,“数据长度”)
close(fd)
read返回零文件到末尾 read()==0;
1标准输入 stdin 返回FILE*
2. 标准输出 stdout 返回FILE*
3. 标准错误输出 stderr FILE*
ls -i 文件的ID (inode)号
2.我们可以从文件读入数据写到buff数组中:
3.我们可以把文件1的内容读完写入到文件2中(cp命令的实现)
不管有无b.txt文件 ,fdw 都能获取到新文件的地址,因为open()了。
注意open()函数里面第一个参数是文件的地址。
4.我们也可以通过把文件名做主函数参数传入到程序中以供使用
read ()函数
exec 系列系统调用
Unix 有一个系统调用,可以将二进制文件的程序的映像载入内存,替换原先进程的地址空间,并开始运行它。
举例:用替换实现ps-f 命令
execl的用法
#include<unistd.h>
int execl (const char *path,const char *arg,…);
arg是它第一个参数,最后一个参数必须是NULL结尾的。
错误返回-1。
execl("/usr/bin/ps","ps","-f",(char*)0);//不返回值,替换失败就往下继续执行。
printf("替换失败");
exit(0);
}
这里的(char *)0 就是NULL。
execlp的用法: ( execlp中 l 代表以列表方式提供,p意味在用户的PATH环境变量中寻找可执行文件)
只要出现在用户的路径中,带p 的exec参数可以只提供文件名。
printf("main pid=%d\n",getpid());
execlp("ps","ps","-f",(char*)0);//p:环境变量path
printf("替换失败");
exit(0);
execle的用法:(e代表会提供给新进程以新的环境变量)
int main(int argc,char*argv[],char*envp[])
{
printf("main pid=%d\n",getpid());
execlp("/usr/bin/ps","ps","-f",(char*)0,envp);//
printf("替换失败");
exit(0);
}
execv的用法:
int main(int argc,char*argv[],char*envp[])
{
printf("main pid=%d\n",getpid());
cahr *myargv[]={"ps","-f",0};
execv("/usr/bin/ps",myargv);//参数放数组里了
printf("替换失败");
exit(0);
}
execvp的用法:
int execvp(const char * file,char *const argv[ ] );//file 就是可以直接提供可执行文件名。比如file可以是"ls" 这种直接给文件名。
(p:path路径)* (v 代表以数组方式提供),p意味在用户的PATH环境变量中寻找可执行文件)
int main(int argc,char*argv[],char*envp[])
{
printf("main pid=%d\n",getpid());
char *myargv[]={"ps","-f",0};
execvp("ps",myargv);//参数放数组里了
printf("替换失败");
exit(0);
}
execve(系统调用):根本方法
int main(int argc,char*argv[],char*envp[])
{
printf("main pid=%d\n",getpid());
cahr *myargv[]={"ps","-f",0};
execve("/usr/bin/ps",myargv,envp);
printf("替换失败");
exit(0);
}
下图我们可以看到我们把replace这个进程替换成了Ps-f 这个命令:。
我们可以看到PCB.里的PID是没有变的
bash 是所有待生成子进程 的父进程,bash 上新增一个环境变量,那么复制的进程中就带有原bash新增的这个环境变量
我们可以新增一个环境变量,然后供主函数envp 调用:
export MYSTR
在把所有环境变量打印:gcc -o main main9.c
./main
打印了所有环境变量。然后我们可以看见我们自己增加的环境变量MYSTR
虚拟内存是计算机系统内存管理的一种技术。它使得应用程序认为它拥有连续的可用的内存(一个连续完整的地址空间),而实际上,它通常是被分隔成多个物理内存碎片,还有部分暂时存储在外部磁盘存储器上,在需要时进行数据交换。
面试问题:在 4GB 物理内存的机器上,申请 8G 内存会怎么样?
如果在是32位机器,用户空间为3g,申请一个8g 的内存导致申请失败。
如果是64位机器,用户空间是128T,虽然内存是4g,也可以申请成功,因为malloc申请的是虚拟内存,在未读写时未映射物理内存。一旦读写了 ,发现内存不够就会报错。
链接:https://www.toutiao.com/article/7106490064708059686/?log_from=73f1356717f65_1659259885895
小知识点
打印环境变量的值($是取值符)
五、操作系统精髓与设计原理摘录
1.多线程
进程中的所有线程共享该进程的状态和资源,他们驻留在同一块地址空间中并且可以访问到相同的数据。
线程提高了不用的执行程序之间通讯的效率。在大多数操作系统中独立进程间的通讯需要内核的介入,以保提供保护和通所需要的机制。但是,由于在同一个进程中的线程共享内存和文件,它们无序调用内核就可以互相通讯。
如果应用程序或函数应该被实现为一组相关联的执行单位时,那么用一组线程比用一组独立的进程更有效。
这里是引用
2.线程的功能特性
和进程一样,主要状态是运行态就绪态和阻塞态。
线程的四种基本操作:
派生:线程可以在同一个进程中产生另外一个线程,新线程拥有自己的寄存器上下文和栈空间,被放置在就绪队列中。
阻塞:当线程需要等待一个事件时,它将被阻塞(保存它的用户寄存器程序技术器和栈指针,),此时处理器转而执行另外一个处于统一进程中或不同进程中的就绪线程。
解除阻塞: 当阻塞一个线程的事件发生时,该线程就被转移到就绪队列中。
结束:当一个线程完成时,其寄存器上下文和栈都将被释放。
3.用户级和内核级线程
在一个纯粹的用户级线程中,有关线程管理的所有工作都应由程序完成,内核并没有一式到有线程的存在。(当前线程的上下文实际上包括用户寄存器的内容,程序计数器和栈指针)
用户级线程有两个明显的缺点:
1.在典型的操作系统中,许多系统调用都会引起阻塞。因此当执行一个系统调用时不仅这个线程会被阻塞,进程中的所有线程都被阻塞。
2.在典型的操作系统中,一个多线程应用程序不能利用多处理技术。内核一次只把一个进程分配给一个处理器,因此一个进程中只有一个线程 可以执行。
解决的办法
1.把应用程序写成一个多进程程序。
2. 使用JACKETING技术。例如:让线程调用应用级的I/O设备JCAKET例程而不是直接调用一个系统I/O例程。
内核级线程:
在一个纯粹的内核级线程软件中,应用程序部分么有进行线程管理的代码,只有一个内核级线程的应用程序编程接口api。 LINUX使用的是这种方法。
一个纯粹的内核级线程,其应用程序可以设计成多线程程序。一个应用程序的所有线程都在有个进程内。内核为该进程及其内部的每一个线程维护上下文信息。
调度是在内核基于线程框架的基础上完成的。该方法克服
了用户级线程方法的两个基本缺陷。首先内核可以同时把同一个进程的多个线程调度到多个处理器中(用户级线程则是:内核一次只把一个进程分配给一个处理器,因此一个进程中只有一个线程 可以执行。);再者,如果进程中的一个线程被阻塞,内核可以调度同一个进程的另一个线程。内核级方法的另一个优点是内核例程自身也可以使用多线程。
相对于用户级线程方法:内核级线程的主要缺点是同一个进程在把控制从一个线程时传送到另一个线程时需要用到内核的模式切换。
如果应用程序中的大多数线程切换都需要内核模式的访问,那么基于用户级线程的方案不会比基于内核的线程方案好多少(虽然内核模式切换有一定的开销,但基于用户级线程经常访问内核从而阻塞状态,严重影响了程序运行的效率)。