kwseeker

学编程:找代码,读代码,改代码,写代码;少看书。但少看不是不看,看一本书要限制在一天内。任务是通读书中主要的概念,明白大致是干什么用的,形成一个大概的框架;然后一周内干掉书中全部代码,代码不理解再浏览书中相关章节,注意是浏览。或者刷博客。代码实在刷不懂,可以先刷后面,待日后重新刷;因为书中代码有些部分可能出自后面的章节。代码总是在多次刷过之后,拨开乌云见日月的。。。

导航

Linux系统编程@进程管理(一)


课程目标:

  构建一个基于主机系统的多客户即时通信/聊天室项目

涉及的理论知识
进程控制:僵尸进程/孤儿进程、进程控制、守护进程。。。
进程间通信:管道、命名管道、信号。。。
多线程编程: 锁、信号量。。。

参考教程

Robert Love, Linux System program

……

 


 

进程结构

进程由程序、数据和进程控制三部分组成

进程的阻塞态:由于访问设备时,没有数据输出的等待状态。

进程互斥:当有若干进程都要使用某一共享资源时,任何时刻最多允许一个进程使用,其他要使用该资源的进程必须等待,直到占用该资源者释放了该资源为止。例如串口同时只允许一个进程对齐进行访问。

临界资源:一次只允许一个进程访问的资源。

临界区:访问临界资源的那段代码称为临界区。为实现对临界资源的互斥访问,应保证诸进程互斥地进入各自的临界区。How?

进程同步:一组并发进程按一定的顺序执行的过程。具有同步关系的一组并发进程称为合作进程,合作进程间互相发送的信号称为消息事件

进程调度:按一定的算法,从一组待运行的进程中选出来一个占有CPU运行。

  调度方式:抢占式:高优先级进程能够打断正在运行的低优先级的进程。

       非抢占式:高优先级进程不能够打断低优先级的进程,只能等待正在运行的进程结束。

  调用算法:先来先服务调度算法 根据就绪的先后来决定先后

       断进程优先调度算法 根据进程运行时间排列,最短时间先运行

       高优先级优先调度算法 注意不同的系统优先级数字高低与优先级高低规定不同,Linux是数字越小,优先级越高。Window相反。

        时间片轮转法 时间片是一段规定的时间,进程在时间片中运行未结束则挂起,由其他进程继续在这么长的一段时间中运行,然后轮询。

死锁:多个进程因竞争资源而形成的一种僵局,需要依靠外力解决。

 

进程状态

TASK_RUNNING(运行): R 可执行状态。正在执行,在就绪队列中等待。 

TASK_INTERRUPTIBLE(可中断): S 睡眠(阻塞)。如果条件满足,内核将其状态设置为运行。收到信号而被提前唤醒并投入运行。

TASK_UNINTERRUPTIBLE(不可中断): D 同可中断状态,但不会因为接收到信号而被唤醒  
TASK_ZOMBIE(僵死):  Z 该进程已经结束,但其父进程尚未调用wait(),子进程的进程描述符仍然被保留着。
TASK_STOPPED(停止):  T 停止执行。这种状态发生在接收到SIGSTOP、SIGTSTP、SIGTTIN、SIGTTOU等信号的时候。此外,在调试期间接收到任何信号,都会使进程进入这种状态。
 
进程状态的查看
ps:显示瞬间进程的状态
常用参数:

l: 长格式输出

u: 按用户名和启动时间的顺序来显示进程

j: 用任务格式来显示进程

f: 用树形格式来显示进程

a: 显示所有用户的所有进程

x: 显示无控制终端的进程

r: 显示运行中的进程

ww: 避免详细参数被截断

$ps            //列出当前shell里当前用户的进程

$ps –u yuhong  //列出用户yuhong运行的所有进程

$ps –el       //以详细列表方式显示运行的所有进程

$ps aux        //以详细的BSD风格显示运行的所有进程

  %MEM:占用的内存的使用率

  VSZ :   虚拟内存大小,即一个程序完全驻留在内存的话需要占用多少内存空间

  RSS:     当前实际占用了多少内存

  STAT:    进程当前状态(R/S/D/Z/T)

       后缀:

        < (高优先级进程)
           N (低优先级进程)
           L (内存锁页)
           s (该进程为会话首进程)
           + (前台进程)
           l (多线程进程)

 


 

进程创建与终止

1、进程的创建

  创建函数:

  pid_t fork(void); (在父进程返回,fork()返回子进程ID,在子进程中返回,fork()返回0。当进程数达到上限或者内存不足时,可能会出错,返回值为-1,系统调用并不直接返回错误码,而是将错误码放在全局变量errno中)

  各种错误情况下errno的值:  1) 进程达到上限 errno=EAGAIN

                2) 系统内存不足 errno=ENOMEM

  相关面试题:考察fork()与编译器对逻辑运算符的处理规则,问题:下列程序一共能够创建多少个进程(包括main进程)?

#include <stdio.h> 
#include <unistd.h> 
int main(int argc, char* argv[]) 
{ 
    fork(); 
    fork() && fork() || fork(); 
    fork(); 
    return 0; 
}

  答案:20

  fork与文件操作面试题:在fork之前,父进程打开了一个文件。在fork之后,如果子进程移动了文件指针,父进程的文件指针有什么变化;如果子进程关闭了文件,父进程有什么变化?为什么会这样?

  不同进程打开同一个文件后,进程表和文件表的关系如下图所示:

 

 

  进程的所打开文件和在fork后的结构图如下所示,子进程是共享父进程的文件表项;

  

  测试代码:

1.	#include "slp.h"  
2.	  
3.	int main()  
4.	{  
5.	    int fd1,fd2,fd3,nr;  
6.	    char buff[20];  
7.	    pid_t pid;  
8.	    fd1 = open("data.in",O_RDWR);  
9.	    pid = fork();  
10.	    if(pid == 0)  
11.	    {     
12.	        nr = read(fd1,buff,10);  
13.	        buff[nr]='\0';  
14.	        printf("pid#%d content#%s#\n",getpid(),buff);  
15.	        exit(0);  
16.	    }     
17.	    nr = read(fd1,buff,10);  
18.	    buff[nr]='\0';  
19.	    printf("pid#%d content#%s#\n",getpid(),buff);  
20.	    return 0;  
21.	}   

测试用例 data.in  

  1. abcdefghijklmnopqrstuvwxyz1234567890  
  2. EOF 

测试结果:

pid#20029 content#abcdefghij#

pid#20030 content#klmnopqrst#

结果分析:

  进程20029对文件的读取后的当前位置应该为data.in的k字符所在的位置,进程20030是由20029进程之后开始读取的,他读取文件内容不是从a开始,而是从k开始,说明20030共享了20029的文件表

 

  pid_t vfork();  调用vfork的作用与调用fork的作用基本相同,但是vfork并不完全复制父进程的数据段,而是和父进程共享数据段。这是因为通常vfork函数是与exec函数族的函数连用,创建执行另一个程序的新进程。调用vfork时,父进程被挂起,子进程运行至调用exec函数族或调用 exit时解除这种状态。

   exec函数族(会替换掉原有进程的程序代码,system会调用fork产生子进程,然后在子进程中执行程序)

#include <unistd.h>
int execl(const char* path,const char* arg1,...)    //path:包含路径的程序名 
                                                    //后面为程序所需的命令行参数,包括程序名,以空指针NULL或(char*)0结束.  
int execlp(const char* path,const char* arg1,...)  //path:不包含路径的程序名,程序从环境变量中查找
                                //后面为程序所需的命令行参数,包括程序名,以空指针NULL或(char*)0结束.
int execv(const char* path,char* const argv[])    //path:包含路径的程序名,不包含程序名
                                //将命令行参数字符串放在了一个字符串数组里面

#include <stdlib.h>
int system(const char* string)              //调用fork产生子进程,由子进程来调用/bin/sh -c string来执行参数string所代表的命令

  exec函数族说明

  一个进程一旦调用exec类函数,它本身就“死亡”了

  (1)系统把代码段替换成新的程序的代码
    废弃原有的数据段和堆栈段,并为新程序分配新的数据段和堆栈段
    唯一留下的就是进程号
  (2)对系统而言,还是同一个进程,不过已是另一个程序了
  
  进程终止后,操作系统执行的操作:
  (1)取消挂起的定时器和信号
  (2)释放虚拟内存资源
  (3)释放其它进程持有的资源如锁
  (4)关闭打开的文件。
  (5)操作系统记录进程状态和资源的使用情况,并通知父进程对wait函数进行响应。

  exec函数族实例

#include <unistd.h>
#include <stdlib.h> main() { execl("/bin/ls","ls","-al","/etc/passwd",(char*)0); execlp("ls","ls","-al","/etc/passwd",(char*)0);
   char *argv[]={"ls","-al","/etc/passwd",(char*)0};
   execv("/bin/ls",argv);
  
   system("ls -al /etc/passwd");
}

  

  查看errno数值的意义
    errno.h
    man 3 errno

  获取进程ID:getpid(); getppid();

  应该避免产生“孤儿进程”(孤儿进程还未结束,父进程却已经结束),解决方法:子进程托孤,或者让其父进程最后退出。

  子进程托孤:init进程(PID=1)接管。

  

  Questions:

  如何实现子进程托孤?fork()例3中的子进程为何能够在父进程退出后,托孤给init进程(难道父进程退出后,自动托孤,不用额外的操作)?

  fork()例3中为什么原进程会存在一个父进程?

  子进程都继承了父进程哪些东西?试用代码举例。

  

2、Linux中的两个特殊的进程

  0号进程:所有进程的祖先

    swapper进程(调度进程):负责进程间的调度,内核直接控制,用户进程无法访问。

    执行cpu_idle()函数

    没有其他进程处于TASK_RUNNING,内核会选择0号进程运行

  0号进程创建的1号进程

    初始化进程在内核引导流程结束时被调用,用于初始化系统环境。初始化文件是/erc/rc*文件、/etc/inittab文件及/etc/init.d目录下的文件。初始化进程从不退出。

    init进程创建和监控其他进程的活动

    接管孤儿进程

3、进程的终止

  1)显式的系统调用

#include <stdlib.h>
void exit(int status);    //wxit是标准C中提供的函数。将关闭所用被该文件打开的文件描述符。退出前把文件缓冲区的内容写回文件

#include <unistd.h>
void _exit(int status);    //调用_exit是为了关闭一些Linux下特有的退出句柄。退出后缓冲区数据丢失

  这两个函数调用后,进程转化为僵尸进程。

  其他用于进程终止的系统调用(需要用时再仔细研究)

#include <stdlib.h>
int atexit(void (*function)(void));  //用于注册一个不带参数也没有返回值的函数以供程序正常退出时被调用。
int on_exit(void(*function)(int, void*),void *arg); //类似atexit,但是它注册的函数具有参数,退出状态和参数arg都传递给该程序使用。
                                //调用成功时,返回值为0;调用失败时,返回值为-1,并将errno设置为响应值。 void abort(void);   //实际是用来发送一个SIGABRT信号,使当前进程终止。 #include <assert.h> void assert(int expression);  //assert是一个宏。调用assert时,将先计算参数表达式expression的值,如果为0,则调用abort函数结束进程。
                   //通常用此函数来检测某些参数是否有不当情况出现,并在不当情况发生时以结束进程作为相应处理。

  

  2)从程序结尾离开

 

  3)被信号终止 SIGTERM(signal terminate) SIGKILL 

    kill [-s <信号名称或编号>][程序]

      kill [-l <信号编号>]
         若不加<信息编号>选项,则-l参数会列出全部的信息名称。 

      //强行中止(杀掉)一个进程pid为324的进程:
    #kill -9 324

    #free

  Questions:

    进程管理中信号有哪些,以及编号都是什么,如何使用?

 

  4)被内核杀掉 Segmentation violation

    当进程出现异常时,会被内核杀掉。

 

  进程终止内核会传送一个SIGCHLD(signal child)信号给它的父进程

  若一个子进程在终止时整体消失,父进程将无法取回任何的信息

  若子进程先于它的父进程结束,则内核应该让子进程进入僵尸进程的状态,等待父进程来打听它的状态,状态打听后,僵尸进程才会正式结束。

  僵尸进程的内核数据结构

    僵尸进程只会保留最小的骨架:进程的PID,退出状态,运行时间

    僵尸进程的避免:

    i 父进程通过wait和waitpid等函数等待子进程结束(导致父进程立刻阻塞自己,直到有一个子进程退出)。

     #include <sys/types.h>

       #include <sys/wait.h>

       pid_t  wait(int *status);   wait(&status) =>waitpid(-1,&status,0)

     返回值:1.结束的子进程pid      2.-1,如果没有子进程

     status(两个字节):1.高字节:子进程exit时设置的代码,低字节为0    2.如果子进程的退出是因为收到信号,低字节为信号的编码

     有时会见到wait函数的参数是NULL,表示父进程并不关心子进程的状态,只是等待子进程结束,并获得子进程信息,防止其成为孤儿进程或僵死进程。

     pid_t  waitpid(pid_t pid, int *status, int options);

     pid取值

        ①< -1: 等待进程组id为pid的子进程的结束
      ②   -1: 等待任意子进程的结束(任意一个)
      ③    0: 等待进程组id跟父进程进程组id相同的子进程的结束
      ④  >0:等待进程id为pid的子进程的结束

     Options可以是以下几个常数中的一个或多个

      ①WNOHANG: 如果没有子进程退出的话马上返回
      ②WUNTRACED:如果有子进程停止的话返回
      ③WCONTINUED:如果一个停止的子进程重新开始执行的话返回(发送SIGCONT)
       当waitpid等待到有子进程退出,则返回子进程的PID, 若未等待有子进程退出便退出,则返回0;发生错误则返回-1。并且error值会是下面3个中的一个:
      ECHILD: PID所指定的进程不存在,或者不是调用者的子进程。
      EINTR: 没有设置WNOHANG,而且在等待过程中收到一个信号。
      EINVAL:参数options不合法。
 
  
      系统提供的另外两个用于进程等待的函数
     
#define _USE_BSD
#include <sys/types.h>
#include <sys/resource.h>
#include <sys/wait.h>

pid_t wait3(int *status, int options, struct rusage *rusage );
pid_t wait4(pid_t pid,int *status, int options, struct rusage *rusage);  

     Waitpid出错时的error返回值以及原因

      If waitpid() is not successful, errno usually indicates one of the following errors. Under some conditions, errno could indicate an error other than those listed here.

[ECHILD] Calling process has no remaining child processes on which wait operation can be performed.

[EINVAL] An invalid parameter was found.

A parameter passed to this function is not valid.

[EFAULT] The address used for an argument is not correct.

In attempting to use an argument in a call, the system detected an address that is not valid.

While attempting to access a parameter passed to this function, the system detected an address that is not valid.

[EINTR] Interrupted function call.

[EOPNOTSUPP] Operation not supported.

The operation, though supported in general, is not supported for the requested object or the requested arguments.

[EUNKNOWN] Unknown system state.

The operation failed because of an unknown system state. See any messages in the job log and correct any errors that are indicated, then retry the operation.

 

 
    
    ii 如果父进程很忙,可以用signal函数为SIGCHLD安装handler,因为子进程结束后,父进程会收到该信号,可以在handler中调用wait回收。
      signal系统调用详解:
      功能描述:为指定的信号安装新的处理句柄。信号处理句柄可能是用户指定的函 数,SIG_IGN 或 SIG_DFL。当信号到达时,如果其处理句柄是SIG_DFL,那么会以默认的方式处理信号;如果其处理句柄是SIG_IGN,那么信号会被忽略;最 后,如果处理句柄是用户指定的函数,此时先将信号处理方式重置为SIG_DFL,接着有可能阻塞处理中的信号,最后是调用信号处理句柄。

      用法: 

#include <signal.h>
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);

      参数: 
        signum:信号编码。
        handler:新的信号处理句柄。

      返回说明: 
        成功执行时,返回以前的信号处理句柄。失败返回SIG_ERR。

    iii 如果父进程不关心进程什么时候结束,那么可以用signal(SIGCHLD,SIG_IGN)通知内核,内核会回收,并不再给父进程发送信号。
 
     iv Stevens的两次fork避免僵尸进程:就是fork两次,父进程fork一个子进程,然后继续工作,子进程fork一 个孙进程后退出,那么孙进程被init接管,孙进程结束后,init会回收。不过子进程的回收还要自己做。
 
 

  状态标志:

  

  信号:

  

    

  三种方式执行多任务处理:轮询、中断、DMA(与中断的区别)

  Questions

  为什么两次fork可以将孙进程托孤给init进程?

  

  handler句柄是什么东西? 

  信号处理句柄可能是用户指定的函 数,SIG_IGN 或 SIG_DFL。 

4.system函数

  可以使用system函数在自己的程序中使用操作系统提供的各种命令(实质上system的实现是fork exec waitpid函数实现的)。

#include <stdlib.h>
int system(const char *cmdstring);

  system函数是否有效的测试方法

  设置cmdstring为NULL,然后调用system,如果有效则返回非NULL指针,无效则返回0.

  

 

To be continued...

posted on 2015-03-18 21:30  kwseeker  阅读(729)  评论(0编辑  收藏  举报