Linux进程基础

进程的基本概念:(由浅入深)

1、什么是进程:

      比如我们写好一个hello.c并编译得到可执行程序hello,但是此时它是静止的,就像一个剧本放在那一样,只有执行它的时候它才动了起来(即就是进程了!)

2、为什么要有进程这个东西:

      因为出现了分时操作系统这个东西:比如同时有10个用户需要和系统进行交互,总不可能一个个的来吧(后面的用户要等死!),所以大牛们想到了用时间片轮转这个方法

      这样就可以这个用户执行一个很小的时间然后下个用户接着上,而由于计算机速度很快,看起来好像10个用户都在并行执行一般(这10个用户不就是10个进程嘛!)

     很重要的概念:进程都各自有自己独立的内存空间!

3、进程的三态:

(1)运行:当一个进程在处理机上运行时,则称该进程处于运行状态。处于此状态的进程的数目小于等于处理器的数目,对于单处理机系统,处于运行状态的进程只有一个。在没有其他进程可以执行时(如所有进程都在阻塞状态),通常会自动执行系统的空闲进程。
(2)就绪:当一个进程获得了除处理机以外的一切所需资源,一旦得到处理机即可运行,则称此进程处于就绪状态。就绪进程可以按多个优先级来划分队列。例如,当一个进程由于时间片用完而进入就绪状态时,排入低优先级队列;当进程由I/O操作完成而进入就绪状态时,排入高优先级队列。
(3)阻塞:也称为等待或睡眠状态,一个进程正在等待某一事件发生(例如请求I/O而等待I/O完成等)而暂时停止运行,这时即使把处理机分配给进程也无法运行,故称该进程处于阻塞状态。

当然了,以上只是最基本的框架,不同的操作系统(windows,linux等)对三态的实现都是不一样的,对于linux来说如下:

◆运行状态(TASK_RUNNING)

指正在被CPU运行或者就绪的状态。这样的进程被成为runnning进程。运行态的进程可以分为3种情况:内核运行态、用户运行态、就绪态。

◆可中断睡眠状态(TASK_INTERRUPTIBLE)

处于等待状态中的进程,一旦被该进程等待的资源被释放,那么该进程就会进入运行状态。

◆不可中断睡眠状态(TASK_UNINTERRUPTIBLE)

该状态的进程只能用wake_up()函数唤醒。

◆暂停状态(TASK_STOPPED)

当进程收到信号SIGSTOP、SIGTSTP、SIGTTIN或SIGTTOU时就会进入暂停状态。可向其发送SIGCONT信号让进程转换到可运行状态。

◆僵死状态(TASK_ZOMBIE)

当进程已经终止运行,但是父进程还没有询问其状态的情况。


注意:只有当进程从“内核运行态”转移到“睡眠状态”时,内核才会进行进程切换操作。在内核态下运行的进程不能被其它进程抢占,而且一个进程不能改变另一个进程的状态。为了避免进程切换时造成内核数据错误,内核在执行临界区代码时会禁止一切中断。


4、进程的结构

进程(执行的程序)会占用一定数量的内存,它或是用来存放从磁盘载入的程序代码,或是存放取自用户输入的数据等等。不过进程对这些内存的管理方式因内存用途 不一而不尽相同,有些内存是事先静态分配和统一回收的,而有些却是按需要动态分配和回收的。对任何一个普通进程来讲,它都会涉及到5种不同的数据段。

Linux进程 = 五个段 +  进程控制块(PCB)

五个段:

BSS段:BSS段(bss segment)通常是指用来存放程序中未初始化的全局变量的一块内存区域。BSS是英文Block Started by Symbol的简称。BSS段属于静态内存分配。

数据段:数据段(data segment)通常是指用来存放程序中已初始化的全局变量的一块内存区域。数据段属于静态内存分配。

代码段:代码段(code segment/text segment)通常是指用来存放程序执行代码的一块内存区域。这部分区域的大小在程序运行前就已经确定,并且内存区域通常属于只读, 某些架构也允许代码段为可写,即允许修改程序。在代码段中,也有可能包含一些只读的常数变量,例如字符串常量等。

堆(heap):堆是用于存放进程运行中被动态分配的内存段,它的大小并不固定,可动态扩张或缩减。当进程调用malloc等函数分配内存时,新分配的内存就被动态添加到堆上(堆被扩张);当利用free等函数释放内存时,被释放的内存从堆中被剔除(堆被缩减)

栈(stack)栈又称堆栈, 是用户存放程序临时创建的局部变量,也就是说我们函数括弧“{}”中定义的变量(但不包括static声明的变量,static意味着在数据段中存放变量)。除此以外,在函数被调用时,其参数也会被压入发起调用的进程栈中,并且待到调用结束后,函数的返回值也会被存放回栈中。由于栈的先进后出特点,所以栈特别方便用来保存/恢复调用现场。从这个意义上讲,我们可以把堆栈看成一个寄存、交换临时数据的内存区。它是由操作系统分配的,内存的申请与回收都由OS管理。

补充:

       全局的未初始化变量存在于.bss段中,具体体现为一个占位符;全局的已初始化变量存于.data段中;而函数内的自动变量都在栈上分配空间。.bss是不占用.exe文件空间的,其内容由操作系统初始化(清零);而.data却需要占用,其内容由程序初始化,因此造成了上述情况。bss段(未手动初始化的数据)并不给该段的数据分配空间,只是记录数据所需空间的大小。data(已手动初始化的数据)段则为数据分配空间,数据保存在目标文件中。 数据段包含经过初始化的全局变量以及它们的值。BSS段的大小从可执行文件中得到 ,然后链接器得到这个大小的内存块,紧跟在数据段后面。当这个内存区进入程序的地址空间后全部清零。包含数据段和BSS段的整个区段此时通常称为数据区。

进程控制块(PCB):

linux进程控制块为一个由结构task_struct(在/include/ linux/sched.h中)所定义(包含进程的重要信息),它是对系统的进程进行管理的重要依据!

 下面对部分数据成员进行说明:
(1)unsigned short pid 为用户标识
(2)int pid 为进程标识
(3)int processor标识用户正在使用的CPU,以支持对称多处理机方式;
(4)volatile long state 标识进程的状态,可为下列六种状态之一:
可运行状态(TASK-RUNING);
可中断阻塞状态(TASK-UBERRUPTIBLE)
不可中断阻塞状态(TASK-UNINTERRUPTIBLE)
僵死状态(TASK-ZOMBLE)
暂停态(TASK_STOPPED)
交换态(TASK_SWAPPING)
(5)long prority表示进程的优先级
(6)unsigned long rt_prority 表示实时进程的优先级,对于普通进程无效
(7)long counter 为进程动态优先级计数器,用于进程轮转调度算法
(8)unsigned long policy 表示进程调度策略,其值为下列三种情况之一:
SCHED_OTHER(值为0)对应普通进程优先级轮转法(round robin)
SCHED_FIFO(值为1)对应实时进程先来先服务算法;
SCHED_RR(值为2)对应实时进程优先级轮转法
(9)struct task_struct *next_task,*prev_task为进程PCB双向链表的前后项指针
(10)struct task_struct *next_run,*prev_run为就绪队列双向链表的前后项指针
(11)struct task_struct *p_opptr,*p_pptr,*p_cptr,*p_ysptr,*p_ptr指明进程家族间的关系,分别为指向祖父进程、父进程、子进程以及新老进程的指针。


4、进程编程实践

4.1、fork系统调用(具体用法使用man 2 fork查看)

调用fork函数后会得到一个子进程,它集成父进程的地址空间,包括:进程上下文、进程堆栈、内存信息、打开的文件描述符、信号控制设置、进程优先级、进程组号、当前工作目录、根目录、资源显示、控制终端等。

问:为什么子进程只从fork函数之后往下继续运行呢?

答:因为父进程fork之后会复制它自己的代码段,数据段,堆栈段等等给子进程,所以子进程在出生的时候这些工作就已经存在好了!


fork函数用途:

比如做服务器就会用到此函数:一个服务器可以同时与多个客户端进行互动,可以为每个客户端都fork一个进程,并在其中做各自的事情,可以看我博客的《socket网络编程基础篇》最后部分贴出的代码。


孤儿进程/僵尸进程(更具体参考博客中:《孤儿进程与僵尸进程总结》)


补充避免僵尸进程的方法:(从内核角度看)

在父进程fork之前就先调用一句:signal(SIGCHLD, SIG_IGN),就是告诉内核说我不收尸,你来给我管这些收尸工作吧!(即提前甩包袱给内核了)

那么内核是怎么来做的呢?————当子进程退出的时候,内核会去检查进程状态(通过进程控制块)


fork之后父子进程共享文件

例如在父进程中调用了 fd = open("1.txt",O_RDWR);   然后调用了fork函数,  那么在父进程和子进程中这个fd是共享的,都可以对这个1.txt进行操作。

具体的原理解释如下:

我们之前介绍过,fork之后会拷贝父进程的那些东西(包括fd)给子进程,所以子进程是可以使用fd的,但是要知道每个进程都有自己独立的用户空间,所以这个fd是拷贝出来的,那么为什么能操作同一个1.txt文件呢?那是因为这两个fd也只是相当于一个指向这个文件的指针而已,而最终执行的时候也还是执行的1.txt,如下图所示:

所以父子进程可以共享打开的文件描述符了,但是由于是并行的会对文件操作的乱套了,所以需要的话也可以加上锁等等来解决。


execve函数(一个进程拉起来另一个进程

        在父进程中fork一个子进程,在子进程中调用exec函数启动新的程序。exec函数一共有六个,其中execve为内核级系统调用,其他(execl,execle,execlp,execv,execvp)都是调用execve的库函数。

适用场景:         在应用程序中执行脚本、执行命令(例如$ls xxxx等等)

头文件:            #include<unistd.h>

函数定义:         int execve(const char *filename, char *const argv[ ], char *const envp[ ]);

返回值 :           函数执行成功时没有返回值,执行失败时的返回值为-1.

函数说明 :execve()用来执行参数filename字符串所代表的文件路径,第二个参数是利用数组指针来传递给执行文件,并且需要以空指针(NULL)结束,最后一个参数则为传递给执行文件的新环境变量数组。

简单代码如下:(功能:在应用程序中执行命令行)

#include<unistd.h>   
main()   
{   
   char *argv[ ]={"ls", "-al", "/etc/passwd", NULL};   
   char *envp[ ]={"PATH=/bin", NULL}   
   execve("/bin/ls", argv, envp);   
}  

运行结果为:-rw-r--r-- 1 root root 1659 Feb 27 20:13 /etc/passwd

这与在bin目录下执行 ls -al /etc/passwd 所得到的结果是一样的。

fork和execve的区别:

fork是分身,execve是变身。exec系列的系统调用是把当前程序替换成要执行的程序,而fork用来产生一个和当前进程一样的进程。通常运行另一个程序,而同时保留原程序运行的方法是,fork+exec。







   

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值