zoukankan      html  css  js  c++  java
  • 进程信号Linux操作系统分析(2) 进程的创建与可执行程序的加载

    查了好多资料,发现还是不全,干脆自己整理吧,至少保证在我的做法正确的,以免误导读者,也是给自己做个记录吧!

        学号:sa×××310 姓名:××涛

        环境:Ubuntu13.04  gcc4.7.3

        

    1.进程管理

           Linux中的进程主要由kernel来管理。系统调用是应用程序与内核交互的一种方式。系统调用作为一种接口,通过系统调用,应用程序能够进入操纵系统内核,从而使用内核提供的各种资源,比如操纵硬件,开关中断,改变特权模式等等。

           罕见的系统调用:exit,fork,read,write,open,close,waitpid,execve,lseek,getpid...

          用户态和内核态

          为了使操纵系统提供一个很好的进程抽象,限制一个程序可以执行的指令和可以拜访的地址空间。

          处置器平日是使用某个控制寄存器中的一个模式位来提供这类功能,该寄存器描述了进程以后享有的特权。当设置了模式位时,进程就运行在内核态,可以执行指令会合的任何指令,并且可以拜访系统中任何存储器位置。

          没有设置模式位时,进程就运行在用户态,不允许执行特权指令,也不允许直接引用地址空间中内核区内的代码和数据。任何这样的实验都市致使致命的掩护故障,反之,用户程序必须通过系统调用接口间接地拜访内核代码和数据。

        

            关于fork的分析,拜见这篇博文

            waitpid

           首先来了解一下僵尸进程,当一个进程由于某种原因终止时,内核并非当即把它从系统中清除。相反,进程被保存在一种已终止的状态中,直到它的夫进程回收。当父进程回收已终止的子进程时,内核将子进程的退出状态传递给父进程,然后摈弃已终止的进程,从此时开始,该进程就不存在了。一个终止了但还未被回收的进程称为僵尸进程。

         如果父进程没有回收子进程就终止了,子进程就成了僵尸进程,即时没有运行,但仍然消耗系统的存储器资源。

          一个进程可以通过调用waitpid函数来等待它的子进程终止或是停止。

         函数原型如下:

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

        如果成功,则为子进程的PID,如果WNOHANG,则为0,如果其他错误,则为-1.

        看一个waitpid函数的例子。

    #include"csapp.h"
    #include<errno.h>
    #define N 5
    int main()
    {
        int status, i;
        pid_t pid;
        
        for(i=0; i<N; i++)
        {
            if((pid = Fork())==0)
                exit(100+i);
        }
        while((pid = waitpid(-1, &status, 0))>0)
        {
            if(WIFEXITED(status))
                printf("Child %d exited normally with status=%d!\n",pid,WIFEXITED(status));
            else
                printf("Child %d terminated abnormally!\n",pid);
        }
        if(errno != ECHILD)
          unix_error("waitpid error\n");
        return 1;
    }

        运行结果

        进程和信号

        

        waitpid的第一个参数是-1,则等待集合由父进程的所有子进程构成。大于0的话就是等待进程的pid。 

        waitpid的第三个参数是-1,则waitpid会挂起调用进程的执行,直到它的等待集合的一个子进程终止。如果等待集合中的一个进程终止了,那么waitpid就当即返回。

        程序运行的结果就是waitpid函数不按照特定的次序回收僵死的子进程。

        

        提一下wait函数,它就是waitpid函数的简单版本,原型如下:

        pid_t wait(int *status)

        等价于waitpid(-1, &status, 0)

        

        execve

            在Linux中要使用exec函数族来在 一个进程中启动另一个程序。系统调用execve()对以后进程停止替换,替换者为一个指定的程序,其参数包括文件名(filename)、参数列表(argv)以及环境变量(envp)。exec函数族当然不止一个,但它们大致雷同,在 Linux中,它们分别是:execl,execlp,execle,execv,execve和execvp,下面我只以execve为例,其它函数究竟与execlp有何区分,请通过man exec命令来了解它们的具体情况。

    一个进程一旦调用exec类函数,它本身就"死亡"了,系统把代码段替换成新的程序的代码,放弃原有的数据段和堆栈段,并为新程序分配新的数据段与堆栈段,独一留下的,就是进程号,也就是说,对系统而言,还是同一个进程,不过已经是另一个程序了。(不过exec类函数中有的还允许继承环境变量之类的信息。)

        原型如下:

        int execve(const char *filename, const char *argv[], const char *envp[]);

        成功调用不会返回,犯错返回-1.

           execve函数加载并运行可执行目标文件filename,且带参数列表argv和环境变量列表envp.只有当涌现错误时,例如找不到filename,execve才会返回到调用程序。所以,与fork一次调用返回两次,execve调用一次并从不返回。

        argv的在内存中组织方式如下图:

        进程和信号

        argv[0]是可执行目标文件的名字。

        

        envp的在内存中组织方式如下图:

        进程和信号

        环境变量的列表是由一个和指针数组类似的数据结构表示,envp变量指向一个以null开头的指针数组,其中每个指针指向一个环境变量串,其中每个串都是形如“NAME=VALUE”的键值对。

        可以用下面的命令来打印命令行参数和环境变量:

    #include"csapp.h"
    
    int main(int argc, char *argv[], char *envp[])
    {
    	int i;
    
        printf("Command line arguments:\n");
        for(i=0; argv[i]!=NULL; i++)
            printf("argv[%2d]: %s\n", i, argv[i]);
        printf("\n");
        printf("Environment variables:\n");
        for(i=0; envp[i]!=NULL; i++)
            printf("envp[%2d]: %s\n", i, envp[i]);
        exit(0);
    }

        进程和信号

        

        

    2.简单的shell

        

    结合下面的fork,wait和exec,下面来实现一个简单shell。

        

    先搭建一个shell框架,步调是读取一个来自用户的命令行,求值并解析命令行。
    #include<stdio.h>
    #include"csapp.h"
    #define MAXARGS 128
    void eval(char *cmdline);
    int parseline(char *buf,char **argv);
    int builtin_command(char **argv);
    
    int main()
    {
    	char cmdline[MAXLINE];
    	while(1)
    	{
    		printf("> ");
    		Fgets(cmdline,MAXLINE,stdin);
    		if(feof(stdin)) exit(0);
    		eval(cmdline);
    	}
    	//printf("Hello\n");
    	return 1;
    }
    
    int builtin_command(char **argv)
    {
        if(!strcmp(argv[0],"quit")) exit(0);
        if(!strcmp(argv[0],"&")) return 1;
        if(!strcmp(argv[0],"-help"))
        {
        	printf("-help    help infomation.\n");
        	printf("ls       list files and folders of current path.\n");
        	printf("pwd      show current path.\n");
        	return 1;
        }
        if(!strcmp(argv[0],"pwd"))
        {
        printf("%s\n",getcwd(NULL,0));
        return 1;
    	}
        return 0;
    }
    
    void eval(char *cmdline)
    {
    	char *argv[MAXARGS];
    	char buf[MAXLINE];
    	int bg;
    	pid_t pid;
    	
    	strcpy(buf, cmdline);
    	bg = parseline(buf, argv);
    	if(argv[0] ==NULL) return;
    	if(!builtin_command(argv))
    	{
    		if((pid = Fork()) == 0)
    		{
    			if(execve(argv[0],argv,environ) < 0)
    			{
    				printf("%s:Command not found.\n",argv[0]);
    				exit(0);
    			}
    		}
    		if(!bg)
    		{
    			int status;
    			if(waitpid(pid,&status,0)<0)
    			unix_error("waitfg:waitpid error");		
    		}
    		else printf("%d %s",pid, cmdline);
    	}
    	return;
    }
    
    int parseline(char *buf, char **argv)
    {
    	char *delim;
    	int argc;
    	int bg;
    	buf[strlen(buf)-1]=' ';
    	while(*buf && (*buf==' ')) buf++;
    	
    	argc = 0;
    	while((delim = strchr(buf,' ')))
    	{
    		argv[argc++] = buf;
    		*delim = '\0';
    		buf = delim + 1;
    		while(*buf && (*buf==' ')) buf++;
    	}
    	argv[argc] = NULL;
    	if(argc == 0) return 1;
    	
    	bg = (*argv[argc-1] == '&');
    	if(bg !=0) argv[--argc] = NULL;
    	
    	return bg; 
    }


    解释一下代码。

        

    主要的几个函数:

        

    eval:解释收到的命令。

        

    parseline:解析以空格分隔的命令行参数,并构造argv传递给execve,执行响应的程序。

        

    builtin_command:  检测参数是不是为shell的内建命令,如果是,就当即解释这个命令,并返回1,否则返回0.

        


        

    下面用通过一些System Call,实现几个linux的常用命令。

        


        

    ls

        

    表现以后路径下的文件和文件夹信息。

        

    c代码实现:
        每日一道理
    正所谓“学海无涯”。我们正像一群群鱼儿在茫茫的知识之海中跳跃、 嬉戏,在知识之海中出生、成长、生活。我们离不开这维持生活的“海水”,如果跳出这个“海洋”,到“陆地”上去生活,我们就会被无情的“太阳”晒死。
    #include<stdio.h>
    #include<time.h>
    #include<sys/types.h>
    #include<dirent.h>
    #include<sys/stat.h>
    #include<stdlib.h>
    #include<string.h>
    #include<pwd.h> 
    #include<grp.h>
    void do_ls(char[]);
    void dostat(char *);
    void show_file_info(char *,struct stat *);
    void mode_to_letters(int,char[]);
    char * uid_to_name(uid_t);
    char * gid_to_name(gid_t);
    
    void main(int argc,char *argv[]){
        if(argc==1)
            do_ls(".");
        else
            printf("Error input\n");
    }
    
    void do_ls(char dirname[]){
        DIR *dir_ptr;   //Path
        struct dirent *direntp;     //Struct to save next file node
        if((dir_ptr=opendir(dirname))==0)
            fprintf(stderr,"ls:cannot open %s\n",dirname);
        else{
            while((direntp=readdir(dir_ptr))!=0)
                dostat(direntp->d_name);
            closedir(dir_ptr);
        }
    }
    
    void dostat(char *filename){
        struct stat info;
        if(lstat(filename,&info)==-1)
            perror("lstat");
        else
            show_file_info(filename,&info);
    }
    
    void show_file_info(char *filename,struct stat *info_p){
        char modestr[11];
        mode_to_letters(info_p->st_mode,modestr);
        printf("%-12s",modestr);
        printf("%-4d",(int)info_p->st_nlink);
        printf("%-8s",uid_to_name(info_p->st_uid));
        printf("%-8s",gid_to_name(info_p->st_gid));
        printf("%-8ld",(long)info_p->st_size);
        time_t timelong=info_p->st_mtime;
        struct tm *htime=localtime(&timelong);
        printf("%-4d-%02d-%02d %02d:%02d",htime->tm_year+1990,htime->tm_mon+1,htime->tm_mday,htime->tm_hour,htime->tm_min);
        printf(" %s\n",filename);
    }
    
    //cope with permission
    void mode_to_letters(int mode,char str[]){
        strcpy(str,"----------");
        if(S_ISDIR(mode))   str[0]='d';
        if(S_ISCHR(mode))   str[0]='c';
        if(S_ISBLK(mode))   str[0]='b';
    
        if(mode & S_IRUSR)  str[1]='r';
        if(mode & S_IWUSR)  str[2]='w';
        if(mode & S_IXUSR)  str[3]='x';
    
        if(mode & S_IRGRP)  str[4]='r';
        if(mode & S_IWGRP)  str[5]='w';
        if(mode & S_IXGRP)  str[6]='x';
    
        if(mode & S_IROTH)  str[7]='r';
        if(mode & S_IWOTH)  str[8]='w';
        if(mode & S_IXOTH)  str[9]='x';
    }
    
    //transfor uid to username
    char * uid_to_name(uid_t uid){
        struct passwd *pw_str;
        static char numstr[10];
        if((pw_str=getpwuid(uid))==NULL){
            sprintf(numstr,"%d",uid);       
            return numstr;
        }
        else
            return pw_str->pw_name;
    }
    
    //transfor gid to username
    char * gid_to_name(gid_t gid){
        struct group *grp_ptr;
        static char numstr[10];
        if((grp_ptr=getgrgid(gid))==NULL){
            sprintf(numstr,"%d",gid);
            return numstr;
        }
        else
            return grp_ptr->gr_name;
    }

    实现思路:
    主要是do_ls函数,通过opendir命令打开文件夹,然后用readdir来读取文件夹中的文件或文件夹,输出信息。

        


        

    通过刚才的shell调用编译好ls程序,效果如下:

        


        

    进程和信号

        

    3.信号

        

            软中断信号(signal,又简称为信号)用来通知进程发生了异步事件。进程之间可以互相通过系统调用kill发送软中断信号。内核也可以因为内部事件而给进程发送信号,通知进程发生了某个事件。注意,信号只是用来通知某进程发生了什么事件,并不给该进程传递任何数据。

            收到信号的进程对各种信号有不同的处置方法。处置方法可以分为三类:第一种是类似中断的处置程序,对于需要处置的信号,进程可以指定处置函数,由该函数来处 理。第二种方法是,忽略某个信号,对该信号不做任何处置,就象未发生过一样。第三种方法是,对该信号的处置保留系统的默许值,这类缺省操纵,对大部分的信 号的缺省操纵是使得进程终止。进程通过系统调用signal来指定进程对某个信号的处置行为。 比如一个进程可以通过向另一个进程发送SIGKILL信号强制终止它。当一个子进程终止或者停止时,内核会发送一个SIGCHLD给父进程。

        


        

            信号有很多种,每种信号类型都对应于某种系统事件。信号的处置流程如下:

        

    进程和信号

        

    定义信号的接受处置函数原型如下:
    #include <signal.h>
    typedef void (*sighandler_t)(int);
    sighandler_t signal(int signum, sighandler_t handler);
    Returns: ptr to previous handler if OK, SIG_ERR on error (does not set errno)

    看一个接受信号的例子:
    #include "csapp.h"
    
    /* SIGINT handler */
    void handler(int sig)
    {
    	return; /* Catch the signal and return */
    }
    
    unsigned int snooze(unsigned int secs) {
    	unsigned int rc = sleep(secs);
    	printf("Slept for %u of %u secs.\n", secs - rc, secs);
    	return rc;
    }
    
    int main(int argc, char **argv) {
    	if (argc != 2) {
    		fprintf(stderr, "usage: %s <secs>\n", argv[0]);
    		exit(0);
    	}
    
    	if (signal(SIGINT, handler) == SIG_ERR) /* Install SIGINT handler */
    		unix_error("signal error\n");
    	(void)snooze(atoi(argv[1]));
    	exit(0);
    }

     程序解析:
    程序接受一个int参数,用于设置sleep的秒数,畸形情况下sleep响应的秒数之后就自动退出程序,由于注册了SIGINI,当按下键盘的Ctrl+C键的时候,跳转到handler函数,处置信号。

        


        

    4.动态链接和静态链接

        

    库有动态与静态两种,动态平日用.so为后缀,静态用.a为后缀。例如:libhello.so libhello.a
    为了在同一系统中使用不同版本的库,可以在库文件名后加上版本号为后缀,例如: libhello.so.1.0,由于程序连接默许以.so为文件后缀名。所以为了使用这些库,平日使用建立符号连接的方式。
    ln -s libhello.so.1.0 libhello.so.1
    ln -s libhello.so.1 libhello.so
    使用库
    当 要使用静态的程序库时,连接器会找出程序所需的函数,然后将它们拷贝到执行文件,由于这类拷贝是完整的,所以一旦连接成功,静态程序库也就不再需要了。然 而,对动态库而言,就不是这样。动态库会在执行程序内留下一个标记‘指明当程序执行时,首先必须载入这个库。由于动态库节省空间,linux下停止连接的 缺省操纵是首先连接动态库,也就是说,如果同时存在静态和动态库,不特别指定的话,将与动态库相连接。

        


        

    5.ELF文件格式与进程地址空间的联系

        

    进程地址空间中典型的存储区域分配情况如下:

        


        

    进程和信号

        

    从图中可以看出:

    从低地址到高地址分别为:代码段、(初始化)数据段、(未初始化)数据段(BSS)、堆、栈、命令行参数和环境变量
    堆向高内存地址生长
    栈向低内存地址生长

        


        

    对于ELF文件,一般有下面几个段

        


    .text section:主要是编译后的源码指令,是只读字段。
    .data section :初始化后的非const的全局变量、局部static变量。
    .bss:未初始化后的非const全局变量、局部static变量。
    .rodata:是寄存只读数据

        

               

        

    关于ELF的文件的只是这里就不赘述了。

        


        

    在ELF文件中,使用section和program两种结构描述文件的内容。平日来讲,ELF可重定位文件采取section,ELF可执行文件使用program,可重链接文件则两种都用。
    装载文件,其实是一个很简单的过程,通过section或者program中的type属性判断是不是需要加载,然后通过offset属性找到文件中的数据,将它读取(复制)到响应的内存位置就可以了。 这个位置,可以通过program里头的vaddr属性确定;对于section来讲,则可以自己定义装载的位置。

        


        

    动态连接的实质,就是对ELF文件停止重定位和符号解析。
    重定位可以使得ELF文件可以在任意的执行(普通程序在链接时会给定一个牢固执行地址);符号解析,使得ELF文件可以引用动态数据(链接时不存在的数据)。
    从流程上来讲,我们只需要停止重定位。而符号解析,则是重定位流程的一个分支。

        


        

    6.参考

        

    程序员的自我涵养—链接、装载与库 

        

     
    Computer Systems: A Programmer's Perspective  3rd Edith

        


        

    Linux内核编程

        


        

    understanding the kernel  3rd Edith

    文章结束给大家分享下程序员的一些笑话语录: 与女友分手两月有余,精神萎靡,面带菜色。家人介绍一女孩,昨日与其相亲。女孩果然漂亮,一向吝啬的我决定破例请她吃晚饭。
    选了一个蛮贵的西餐厅,点了比较贵的菜。女孩眉开眼笑,与我谈得很投机。聊着聊着,她说:“我给你讲个笑话吧。”“ok”
      “一只螳螂要给一只雌蝴蝶介绍对象,见面时发现对方是只雄蜘蛛。见面后螳螂问蝴蝶‘如何?’,‘他长的太难看了’,‘别看人家长的丑,人家还有网站呢’。”
      “呵呵………”我笑。忽然她问:“你有网站吗?”  

    --------------------------------- 原创文章 By
    进程和信号
    ---------------------------------

  • 相关阅读:
    h5混合开发好用的下拉刷新插件
    设置窗体可以使用鼠标拖动大小 通过
    C# 使用System.Data.OleDb;避免oracle中文乱码问题
    C# 开启一个新进程并为新进程设置工作目录
    WPF窗体程序入口 自定义窗体启动页面
    WorkFlow WF如何为一个集合赋值
    第三方框架的使用
    AppIcon应用图标 and Launchimage启动图标的制作
    在didSelectRowAtIndexPath 里面取cell的方法
    iOS 开发技术体系
  • 原文地址:https://www.cnblogs.com/jiangu66/p/3102308.html
Copyright © 2011-2022 走看看