day3之进程

C语言程序一致由下面几部分组成:

1)正文段。称之为代码段,是CPU执行的机器语言指令部分,文本段有只读属性,防止程序由意外而修改其指令;正文段是可以共享的,多个进程间可以同时运行同一段程序。

2)初始化数据段: 包含了显式初始化的全局变量与静态变量。当程序加载到内存中,从可执行文件中读取这些变量值。

3)未初始化数据段: 包含了未进行显式初始化的全局变量和静态变量,通常称之为bss段。在程序执行之前,系统将本段内所有的内存初始化为0,可执行文件并没哟bss段变量分配空间。

4): 函数内的局部变量以及每次函数调用时所需保存的信息都放在此段中,每次调用函数时候,函数传递的实参以及函数返回值都会存放在栈中。栈是一个动态增长和收缩的段。系统会为每一个当前调用的函数分配一个栈帧,栈帧中存储了函数的局部变量、实参和返回值。

5):可在运行时动态进行内存分配的一块区域,譬如使用malloc来分配的内存空间,就是从系统的对内存中申请分配的。

进程内存布局

----- 进程的虚拟地址空间

Liunx 系统中,采用了虚拟内存的管理技术。每一个进程都有自己独立的地址空间运行。而在32位系统中,每个进程的逻辑地址空间均为4GB, 按照3:1的比例进行分配,其中用户进程享有3G空间,内核自己享有1G空间。

 虚拟地址会通过硬件MMU(内存管理单元)映射到实际的物理地址空间,建立虚拟地址到物理地址的映射关系后,对虚拟地址的读写操作实际上就是对物理地址的读写操作。 

 应用程序在一个虚拟地址空间中,所以程序中读写的内存地址对应的也是虚拟地址。

计算机的物理内存的大小式固定的, 就是计算机的实际物理内存式固定的。如果都在访问实际的物理地址,那么我们实际的物理地址只有4G,就会出现以下问题

1)多个程序运行的时候,需要保证全部程序用到的总量不能超过实际的物理地址。

2)内存使用的效率很低,,使用物理地址,需要经常对其进行拷贝到硬盘,然后新的程序装入内存。然而由大量的数据装入装出,内存的使用率就会非常的低。

3)程序直接访问内存,就会导致每一个进程都可以修改其他进程的内存数据,甚至修改内核地址空间中的数据。就会由几率造成一些系统的安全问题。

4)无法确定程序的链接地址。程序运行的时候,链接地址和运行地址必须进行一致,否则程序无法运行!因为,程序代码加载到内存的地址是随机分配的,所以程序的运行地址在编译的时候无法确认。

使用虚拟内存的优点

1.1 fork()创建子进程

调用fork进程的函数叫父进程,fork函数创建出来的进程叫子进程。创建子进程通常会简化程序的设计,同时提高了系统的并发行(同时能够吹了更多的任务或请求,多个进程在宏观上实现同时运行)。

完成fork调用之后,存在两个进程,一个是父进程,一个是创建出来的子进程,并且每个进程都会从fork函数的返回处继续执行。会导致调用的fork返回两次值。

fork调用成功的话,将会在父进程中返回子进程的PID,而在子进程中返回值是0;如果调用失败,父进程返回-1,不创建子进程,并设置errno;

https://kb.cnblogs.com/page/76622/ 这边文章中讲述了LInux  中fork函数的运行过程。

在进行fork之后,子父进程就会继续执行fork之后的命令。所以通常有两个返回值。

1.2 父、子进程之间的文件共享

调用fork函数之后,子进程会获得父进程所有的文件描述符的副本,意味着父、子进程对应的文件描述符均指向相同的文件表。

 由于子进程拷贝了父进程中的文件描述符,所以意味着,对应的文件描述符指向了磁盘中相同的文件,而这些文件在父、子进程中实现共享。

fork使用场景

1)父进程希望子进程复制自己,在网络服务中是常见的。

2)要去执行不同的程序。子进程重新去执行另外一个主函数

1.3 系统调用

除了fork系统调用之外,还有vfork()系统调用用于创建子进程,vfork()和fork()函数在功能上是相同的。并且返回值也是相同 vfork的函数原型:

 

 但是由于vfork会导致一些难以发现的程序bug,所以应当舍弃vfork而使用fork

1.4 fork之后的竞争条件。

在fork之后,子进程作为一个独立的进程,可被系统调度运行,所以无法确定父子两个进程谁先进行访问CPU,也就是说无法确认谁先被系统调用。

绝大部分情况之下,父进程会先于子进程被执行,但是不排斥子进程先于父进程被执行的可能性。

正常情况而言,我们希望在子程序中进行使用_exit()而在主程序中使用exit()

因为当程序没有使用\n的时候,子进程会复制父进程的缓冲区内容,而如果都使用exit()的时候,子父进程都会刷新缓存区的内容,就可能会导致某些内容会重复出现,而使用_exit()函数并不会刷新Stdio的缓冲区。

1.5 监视子进程

为了了解子进程是何时结束,并且何时被终止,是正常还是异常,都需要对子进程进行监视。

wait()函数就可以等待进程的任一子程序终止,并且获取子进程的终止状态信息。

status: 用于存放子进程终止的状态信息,可以为NULL, 表示不被接受子进程终止的状态信息。

返回值: 若成功则返回终止的子进程对应的进程号,失败则返回-1

如果调用wait函数,则所有子进程都还在运行, wait就会一直阻塞等待,直到某一个子进程终止;

  

1.6 waitpid()函数

wait的限制:

1)可能无法等到特定的子进程完成,只能够按照顺序等待下一个子进程的终止。

2)如果子进程没有终止,正在运行,那么wait()总是保持阻塞,有时候希望执行非阻塞等待。

3)wait函数只能发现那些被终止的子进程,无法直到因什么信号而终止。

waitpid()函数

 

status: 与wait的函数意义一样

options: 

1.7僵尸进程与孤儿进程

孤儿进程

父进程优先于子进程结束

此时子进程就以为着是一个“孤儿”,则在调用getppid()的时候就会返回1,此时init进程就变成了孤儿进程的养父。

僵尸进程

子进程先于父进程结束,但是父进程未能够及时回收子进程的资源,则就会形成一个僵尸进程。

父进程需要使用wait()区回收子进程,如果没有回收子进程则会存在大量的僵尸进程,它们势必会填满内核进程表。并且僵尸进程是无法通过信号来将其杀死,只能杀死僵尸进程的父进程,让Init进程来接管僵尸进程,从而把他们从系统中清理掉。

1.8 执行新程序

如果子进程的工作不再是运行父进程的代码段,而是运行另一个新程序的代码。则需要用过execd函数来实现运行一个新的程序。

exec函数的原型

 

 

1.9 进程状态与进程关系

Linux 系统下进程通常存在6种不同状态,分为:就绪态、运行态、僵尸态、可中断睡眠状态(度睡眠)、不可中断睡眠状态(深度睡眠)以及暂停态。

 其进程之间个状态之间的切换如图

 

当多个进程同时操作同一文件的时候,为了保证文件数据的准确性,Liunx通常采用的方法就是对文件上锁,来避免多个进程同时操作同一文件时产生竞争状态。比如,进程对文件进行IO操作的时候,首先对文件进行上锁,将其锁住,然后在进行读写操作,这要该进程没有解锁,则其他进程就无法对其进行操作。

文件锁的分类

建议性锁、强制性锁

1)建议性锁是一种协议。程序访问之前,先对文件进行上锁,上锁成功后在访问;但是也是而可以不上锁直接进行访问文件。所以要使用建议性锁就要遵循协议。

2)强制性锁,它是一种强制性的要求,如果进程对文件上了强制性锁,其它的进程在没有获取到文件锁的情况下是无法对文件进行访问的。其本质原因在于,强制性锁会让内核检查每一个 I/O 操作(譬如 read()、write()),验证调用进程是否是该文件锁的拥有者,如果不是将无法访问文件。

flock函数加锁

 

 LOCK_NB: 表示以非阻塞的方式获取锁,调用flock无法获得文件锁会被阻塞,直到其他进程释放锁为止。如果不想程序被阻塞,可以指定LOCK_NB标志。就会立即返回。

 

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值