week6-linux进程环境

week6 进程环境(课本unix-ch05)

介绍进程执行环境相关的内容,如程序的开始、命令行参数的接收和环境变量、程序终止时的动作进程的地址空间和存储分配等。同时,我们还将介绍用setjmp()和longjmp()进行非局部控制转移的方撞,以及关于进程资拥统计信息和用户信息的有关函数。

5.1 main()函数

int main( int argc, char *argv[]);
  • argc:给出命令行参数个数
  • argv:给出命令行参数字符串的指针数组。

1.程序启动点:

尽管我们说C 程序是从main()开始执行的,但实际上程序的真正启动点是系统提供的一个初启函数,该函数位于名为crtO .o 的文件中。编译时,链接器ld将此文件域c文件装配在一起形成可执行文件。

2.初启函数作用:

从unix shell获取命令行参数和环境变量值,并将它们组织称main函数需要的参数形式,然后调用main函数。此外,当c程序从main返回时,初启函数也负责调用exit终止进程。

5.2 命令行参数

1.命令行参数是在启动程序执行的shell 命令中给出的以空格分开的字符串

2.当程序执行时,调用exec()的进程(shell 或用户进程)可以将命令行参数传送给它

3.程序能够看到命令行参数的唯一途径是通过main ,如果main 没有参数,则得不到命令行。

4.如果程序的命令行参数很简单,则可直接从argv 中获取这些参数,在命令行中给出较多和较复杂的参数时,这些参数最好遵循POSIX 推荐的语法并用函数getopt() 来读取它们。

5.3 环境变量

1.程序除了可通过main()的argc 和argv 参数获得执行环境外,还可以通过环境变量获得执行环境

2.argv 一般用于程序的某次特定运行,而环境变量则用于定义不经常改变的或者许多程序共享的信息

3.环境变量有一些是在注册时自动设置的( 如HOME 、USER 等环境变量,另一些则由环境变量赋值命令env 和export 命令来设置

4.每一个进程都有自己的一组环境变量,由shell 执行的进程继承shell 的所有环境变量。

5.3.1 环境表

1.环境表:是一个数据结构,环境表与main的argv 参数类似,是一个字符指针数组,以NULL指针表示元素的结束。

作用是用来记录进程的所有环境变量及其值。

2.环境指针:environ是系统定义的全局变量,叫做环境指针,用来引用环境表。应用程序
在引用envlron 之前必须先加以说明,如

extern char ** environ ;

3.环境变量的打印几种方法

方法1:

  
#include "ch06.h"
//打印环境变量
int main(int argc,char *argv[],char *env[])
{
	int i=0;
	for( i=0;i<argc;i++)
	{
		printf("argv[%d]  :%s\n",i,argv[i]);

	}
	for(i=0;env[i];i++)
	{
		printf("env[%d] :%s\n",i,env[i]);
	}
	return 0;
}

方法2:使用系统环境变量指针environ进行获取

//使用系统环境变量指针进行获取
int main()
{
	int i=0;
	extern char **environ;//外部变量的引用
	
	for(i=0;environ[i]!=NULL;i++)
		printf("env[%d]:%s\n",i,environ[i]);
	return 0;
}

//课本5-2  使用系统环境变量指针进行获取
#include "ch05.h"
int main(int argc, char *argv[])
 {
    extern  char  ** environ;
    char **env = environ;
    int i;

    for (i=0; i<argc; i++)
      printf("argv[%d]: %s\n", i, argv[i]);
    while (*env) {
        printf("%s\n",*env);
        env++;
    }
    exit(EXIT_SUCCESS);
 }

5.3.2 访问环境变量

应用程序一般在需要访问整个环境向其他程序递交环境的情况下才使用envlron

当只需要访问某个特定的环境变量时,通常使用函数getenv 和putenv

#include <stdlib.h>

char *getenv(const char* name);
int putenv(char *str);
getenv:

返回的字符串是环境变量name(参数)的定义。这个字符串不能被修改,但是可能被后面的getenv调用覆盖。

如果name参数指定的环境变量没有定义,则getenv()返回一个空指针NULL

putenv:

当参数形式为“name=value”时,

  • 则putenv吧name定义加到环境表;但是如果环境表中name存在,则覆盖旧定义。这两种情况下,参数str所指字符串作为环境的一部分,后面修改这个字符串将改变环境。
  • 因此,注意不要用自动变量字符数组作为str参数。

当参数形式为其他时

  • 此参数被视为环境变量名,此时去掉该变量名在环境中的定义

课上getenv和putenv的使用,获取shell中PATH路径

int main()
{
	printf("%s \n",getenv("PATH"));
	return 0;

}
测定环境变量的生存期
#include "ch06.h"
// 测定环境变量的生存期
static void set_env_string(void)
{
	//char test_env[]="test_env=test";//修改之前,运行show的时候为null
	static char test_env[]="test_env=test";//修改之后,运行show的时候为test
	if(putenv(test_env)!=0)
	{
		printf("Failed to put new env var!\n");
	}
	printf("1.The test_env string is %s \n",getenv("test_env"));
}

static void show_env_string(void)
{
	printf("2.The test_env string is %s \n",getenv("test_env"));
}

int main()
{
	set_env_string();
	show_env_string();
	return 0;
}

例5-3

我们来写一个输出和设置某个环境变量的程序。如果命令行只有一个参数,该程序简单地将它视为环境变量名并输出其值:如果有第二个参数,则用它给出的值设置第一个参数指定的环境变量。

#include "ch05.h"
#include "xmalloc.c"

int main(int argc,char *argv[])
{
   char *value;  
   if(argc==1||argc>3) {   /* 检查调用参数是否正确*/
      fprintf(stderr, "Usage: <environ var> [<value>]\n");
      exit(1);
   }
  
   value = getenv(argv[1]); /* 取环境变量之值 */
   if(value)
      printf("Variable %s = %s\n",argv[1],value);
   else
      printf("Variable %s undefined\n",argv[1]);
   
    if(argc==3){  /* 三个参数的情形  */
      char *string = xmalloc(strlen(argv[1])+strlen(argv[2])+2);
      strcpy(string,argv[1]);  // 形成"name=value"  
      strcat(string,"=");
      strcat(string,argv[2]);
      printf("calling putenv with: %s\n",string);
      if(putenv(string)!=0){  /* 设置新环境变量  */
         free(string);//释放空间
         err_exit("putenv error");
      }
      value = getenv(argv[1]); /* 核实该变量的新值  */
      printf("New value of %s = %s\n",argv[1],value);
   }
   exit(0);
}

用命令"a.out PATH qwe" 运行这个程序,会发现程序中对环境变量PATH 的改变不会影响shell 原先对PATH 的设置。这是因为putenv 设置的是进程自己的环境变量,而不是shell的环境变量

子进程虽然继承父进程的环境变量但不传播自己的环境变量给父进程

5.4 终止进程

1.终止方式:

正常终止:

  • main正常返回即由初启函数调用exit–>exit(main(*argc,argv))
  • 调用exit()、–>调用_exit()返回内核
  • 调用_exit()–>直接返回到内核,且调用之前需要完成某些清理工作,因此不推荐直接用 _exit()

异常终止:调用abort()或由于信号终止

#include <stdlib.h>
void exit(int status);

exit以出口状态status终止进程,不返回。调用之后引起下面动作:

  • atexit 注册时相反的顺序调用所有注册函数( 5 .4 .2 节)。
  • 关闭所有打开的流。这将导致写出所有被缓冲的输出。
  • 删除用tmpfile建立的所有临时文件。
  • 调用_exit(status) 终止进程。

5.4.1出口状态

1.调用exit 和_exit 都需要传递一个整数作为出口状态。出口状态用于向父进程报告出口状态用于向父进程报告程序运行成功与否

2.进程的出口状态是主程序main的返回值,尽管这个返回值可以是任何整数,但有效的出口状态值只有8 位,即0~255 之间的整数值

3.如果没有显示写出return语句,那么编译程序自动地为它设置一个值为0 的出口状态,即正常出口

4.出口状态习惯上的一些约定(并没有统一规定):

  • 成功返回0 ,失败返回1
  • 完成比较操作的程序除外:它们用出口状态值1 指出不匹配, 2 指出不能比较
  • 大于等于128的出口状态值通常保留作为特殊用途;例如值128 用于指出在子进程中执行另一程序失败

5.增强程序可移植性

用系统定义的宏常数EXIT SUCCESS 和EXIT FAILURE作为出口状态;它们的说明在头文件<stdlib.h> 中,其值分别是0 和1。

6.注意:不要用程序中统计错误的个数或errno 的值作为出口状态

因为出口状态值实际上被截断成只有8 位有效位。因此,若程序企图报告有256 个错误,
则父进程将收到0 个错误的报告,即成功完成。

5.4.2 终止前的清理atexit

1.问:如果编写了一个库程序,库内部数据在应用程序退出的时候需要清理,让每个应用显示迪奥哟库的清理函数不切实际,则有以下两种方法:

  • 一方面在库中建立这种清理函数
  • 另一方面在初启函数中调用atexit指明程序终止时执行这些清理函数,从而使清理工作对应用透明。
#include <stdlib.h>

int atexit(void (*func)(void));

2.说明

  • 出口句柄:func所指的函数,不带参数。
  • 以参数func 调用atexit导致func 所指的函数被注册为程序正常终止时要执行的函数
  • 所有注册的函数以与调用atexit 相反的顺序被调用,井且调用的次数与注册的次数相同。

3.进程在调用任意一个exec 之后(6.7 节) ,前面曾经注册过的函数都将不再是注册的。这是因为exec 总是执行一个新程序,新程序与调用execO 的进程的程序不同,它无法访问以前曾注册过的那些函数。

4.atexit示例
//析构函数--在最后执行,收回对象等清理工作
static void __attribute__((destructor))After_main()
{
	printf("-----------AfterMain----------");
}

//回调函数,清理工作
void callback1()
{
	printf("-----------callback1---------\n");
}

void callback2()
{
	printf("-----------callback2---------\n");
}

void callback3()
{
	printf("-----------callback3---------\n");
}



int main()
{
	atexit(callback1);
	atexit(callback2);
	atexit(callback3);
	printf("-------program finished-------\n");
	return 0;

}

程序运行结果

-------program finished-------
-----------callback3---------
-----------callback2---------
-----------callback1---------
-----------AfterMain----------

5.课本5-4示例

#include "ch05.h"
static void func_1(void)
{
    printf("invoked first exit handler func_1\n");
}

static void func_2(void)
{
    printf("invoked second exit handler func_2\n");
}

int main(void)
{
    if ( atexit(func_1) != 0 ){ 
        printf("ERROR: atexit() register func_1 failed\n");
        exit(EXIT_FAILURE);
    }
    if ( atexit(func_2) != 0 ){ 
        printf ("ERROR: atexit() register func_2 failed\n");
        exit(EXIT_FAILURE);
    }
    if ( atexit(func_2) != 0 ){ 
        printf("ERROR: atexit() register func_2 failed\n");
        exit(EXIT_FAILURE);
    }
    printf("main: exit normally\n");
}

输出

main: exit normally
invoked second exit handler func 2
invoked second exit handler func 2
invoked first exit handler func 1

5.4.3 流产程序

异常终止程序常用的方能是调用abort函数。由于该函数会在进程的当前工作目录中生成
一个内存转储文件(core) ,因此也称它使程序流产。

#include <stdlib.h>

void abort(void);

调用abortO 将立即终止程序的执行,它不执行用atexitO 注册的清理函数,但在终止程序之前关闭所有打开的流井生成一个core 文件。

所有异常终止程序的原因都是由信号造成的。实际上. abort通过生成一个SIGABRT 信号来流产程序。

5.5 进程的存储空间(重)

5.5.1 进程的地址空间

1.进程(用户)地址空间

  • 因为从用户的角度看到的总是虚地址空间,今后我们简单地称进程的虚地址空间为进程的地址空间或用户地址空间。
  • 进程的地址空间由系统允许程序引用或访问的所有存储单元组成。
  • 进程的地址空间由若干不同的段组成,每个段是一片连续存储单元,分别用于存放程序的代码、初始数据、静态变量,以及用作检或堆的空间。

2.建立地址空间:

  • 当进程用exec() 装入一个新程序时,内核便为这个新程序建立地址空间。

3.进程当前状态:

  • 进程的地址空间及其寄存器上下文,反映了进程所运行程序的当前状态。

4.进程地址空间的各种段

  • 正文段:存放程序执行代码

    它是只读共享段,即在存储器内只保存一个副本而允许多个程序执行该副本。对于那种经常需要运行的程序,共享可以大大减少占用的内存空间。此外,正文段是只读的还可以避免程序被不经意地破坏。

  • 初始化数据段:也简称为数据段,用于存放程序中赋予了初值的静态变量和全局变量

  • 未初始化数据段::也称为bss 段,程序中所有不带初值的全局变量和静态局部变量均存放在此段中;这个段的数据在程序运行之前可由内核置为0。

  • **栈:**用于存放函数内的局部变量、临时变量以及函数的调用环境,如函数的返回地址、函数的栈帧指针、函数人口参数等。

  • 堆:用于动态、申请的存储空间。堆是大家共有的空间,分全局堆和局部堆。全局堆就是所有没有分配的空间,局部堆就是用户分配的空间。堆在操作系统对进程 初始化的时候分配,运行过程中也可以向系统要额外的堆,但是记得用完了要还给操作系统,要不然就是内存泄漏

5.注意

  • shell 命令size(1) 报告可执行文件的正文段、数据段以及bss 段的字节个数。
  • 但是尽管size 能报告出可执行文件bss 段的大小,然而在磁盘上的可执行文件中实际并不包含bss 段的空间,它只保存正文段和初始化数据段。bss 段由内核在程序运行之前分配空间井置初值为0 。

5.5.1 动态存储分配与释放

#include <stdlib.h>
void *malloc(size_t size);
void * calloc(size_t number of elem, size_t elem size);
void * realloc (void *ptr, size_t newsi z e);
#include <malloc.h>
void * memalign(size_ t,size_ t boundary);
void * valloc( size_t size);

malloc:

  • 从堆中为调用进程分配大小为size字节的一片堆储存单元,这片单元的初值是不确定的。

  • 调用成功:返回所分配单元的首地址,并保证地址边界严格对齐

  • 调用失败:返回空指针并置errno;唯一的错误是ENOMEM,即无足够的储存空间可以分配,我们应该在每次调用时检查malloc()的返回值。

  • 尽管返回的(void *)类型可以自动转换到其他任何指针类型,但是使用时一般会将其转换为想要存在这片单元中的对象的类型的指针。如下:

  • struct foo *ptr;
    ptr=(struct foo*)malloc(10*sizeof(struct foo));
    if(ptr==NULL)abort();
    

calloc:

  • 用于为结构数组分配空间,第一个参数指出数组元素个数,第二个给出元素字节大小。
  • 调用成功:用0填充存储单元,并返回指针指向第一个元素
  • 其他与malloc相同

realloc:

  • 增加或缩小已分配的存储块大小。

  • 第一个参数参数ptr指向原存储块,第二个参数给出新分配大小

  • 如果没有更大存储空间,那么返回空指针,此时原来存储块不变,因此不要轻易就给原来的指针赋值。

  • my_ptr=malloc(BLOCK_SIZE);
    ...
    my_ptr=realloc(my_ptr,BLOCK_SIZE*10);
    最好不要这么用,因为如果没有更多空间,则返回空指针会导致原空间位置丢失。
    

memalign()和valloc提供一种灵活控制存储边界对其能力。

用memalign 和valloc 分配的存储不能用free来释放,因为这两个函数都通过调用mallocO来分配一块比所申请空间要大的存储,然后返回此块中具有指定边界的地址,而这个地址已不是malloc的返回值。不过,在Linux 系统中没有此问题, GNU libc 允许用free释放这两个函数分配的空间。

例5-4测试malloc能分配的最大空间
#include "ch05.h"

#define CHUNK_SIZE  (1024*1024)

int main()
{
   char *some_memory;
   int megs_obtained = 0 ;
   struct rlimit rlt;
   rlim_t j,k=RLIM_INFINITY;

   getrlimit(RLIMIT_AS,&rlt);
   if (rlt.rlim_cur==RLIM_INFINITY){ /* 如果空间无限制,退出执行 */
      printf("no limit on virture memory\n");
      exit(0);
   }

   while (1){
      some_memory = (char*)malloc(CHUNK_SIZE);
      if (some_memory == NULL) {
         printf("no more memory\n");
         exit(EXIT_FAILURE);
      }
      megs_obtained++;
      printf("Now allocated %d Megabytes\n",megs_obtained);
   }
   exit(EXIT_SUCCESS);
}

例5-5 xmalloc()函数

为了避免每次调用malloc检查返回值的烦琐,习惯上是调用一个名为xmalloc的函数,由该函数调用malloc并进行返回值检查

#include <sys/types.h>
#include "err_exit.h"
void * xmalloc(size_t size)
{
    register void  * value = malloc(size);
    if (value == 0)
        err_exit ("virtual memory exhausted");
    return value;
}

5.5.3 释放分配的储存单元

应用程序可占据由malloc分配的存储空间一直至程序结束,程序结束时系统会自动释放它们。但是为了更有效地利用存储空间,常常是需要时申请,不需要时便释放。

#include <stdlib.h>

void free(void *ptr);

注意:

  • free的参数所指地址必须是由动态内存分配函数malloc,calloc等分配的地址空间。
  • 这块被释放的存储通常放回到由malloc管理的自由存储链表中以便将来存储申请时使用。
  • 如果不是那些函数分配的地址空间,free不会给出错误信息,只是沉默的返回。但是会导致存储泄露问题,以为释放了但其实没有释放。
  • 存储泄露:指向动态分配空间的指针被改变或者丢失引起。如果没有指针指向这块存储,它将成为不可访问也无法释放的存储块,这将导致增大程序的存储空间。如果泄漏的存储很多,程序将最终慢下来井耗尽所有的存储空间。
链表释放示例
//这里每一个链表元素和字符串均是由malloc等函数分配的。
struct chain{
struct chain *next;
char *name;
}

void free chain(struct chain *ptr)
{
	while(ptr!=NULL) {
		struct chain *next = ptr- >next ;
		free(ptr - >name); /*允释放链结点指向的数据* /
		free(ptr); /*然后再释放链结点本身*/
		ptr = next;
	}
}

5.6 setjmp()和longjmp()函数

#include <setjmp.h>

int setjmp(jmp_buf env);
void longjmp(jmp_buf env, int val);

在C 程序中,不能用转移到一个标号的方法转移到其他函数中去,但是可以用setjmp()和longjmp()实现从一个函数直接跳至另一个函数,这种控制转移称为非局部转移

不同于正常的函数调用与返回,这种非局部转移可从最内层活跃函数直接跳至最外层活跃函数,其间可能相
隔好几层函数调用,而正常的函数调用与返回需遵循先进后出规则

非局部转移作用:

有时很有用。例如,编译程序通常用一个主控程序逐行读入源文件井对它进行词法分析、语法分析等处理。当发现存在语法错误时, 一般不会终止程序,而是要设法回到主控程序继续后继处理。但是,此时往往已经嵌套了好多层函数调用。如果仍按照调用顺序逐层返回,贝IJ 在每一个函数调用返回之后都必须进行错误检查。这种做法显然既烦琐又低效。使用setjmp和longjmp可以很方便地解决这种问题。

setjmp

作用:

  • 在外层函数中设置可以由内层函数返回的返回点
  • 保存当前栈环境
  • 作为longjmp转移的目的地

参数env :

  • 通常是一个全局变量,因为要跨函数调用使用,保存调用函数的栈环境,该变量之后被内层函数所调用的longjmp使用。
  • 数据类型buf_jmp:系统定义的一种特殊的数据类型,是某种形式的数组,容纳有longjmp()恢复栈环境需要的所有信息。

返回值:

  • 调用setjmp()时的返回值,值为0
  • 从内层函数调用longjmp的返回值,值不为0
  • 因此setjmp的返回值经常放在if中进行判断
longjmp

作用:

  • 使用外层函数调用setjmp保存在变量env的栈环境,将控制返回到setjmp调用之处。
  • 恢复由setjmp保存的栈环境井使程序从最后一次设置栈环境变量env 的setjmp返回,参数val 将作为setjmp的返回值,但如果它为0 , setjmpO 仍将返回1。
编译优化问题

对于从longjmp返回后的变量的取值情况,有两种

  • 全局变量:其值维持最后在longjmp中得到的定值
  • 局部(自动)变量:如果没有编译优化,则维持最后在longjmp中的定值不变;如果有编译优化,那么其值hi回到setjmp时的状态。
  • 原因:如果有编译优化,那么局部变量会分配到寄存器中,而setjmp中保存的环境env包含了寄存器的保护,因此从longjmp恢复环境之后,变量的值也被恢复了,所以会改变。而不进行优化的变量是保存在栈中的。
  • 解决方法:将变量声明为volatile类型,这告诉编译器这种变量是易变的,从而不对其进行优化。
  • 补充优化命令gcc -g -Wall 7-3.c -o 7-3.u -O2
  • 如下程序,使用优化和不优化时i3的值分别为4和8.而i1,i2始终为6和7
#include "ch05.h"
static jmp_buf jmpbuffer;
int i1;
static void f1(int i, int j, int k) 
{
   printf ("     in f1 (): i1=%d, i2=%d, i3=%d\n", i, j, k);
   longjmp(jmpbuffer,1);
}
int main(void)
{
   volatile int i2;
   int i3;
   i1=2; i2=3; i3=4;
   if (setjmp (jmpbuffer)!=0) {
      printf("after longjmp: i1=%d, i2=%d, i3=%d\n", i1, i2, i3);
      exit(0);
   }
   i1=6; i2=7; i3=8;
   f1(i1, i2, i3);
}

老师上午讲解【杂】

进程:代码在资源环境中的一次运行

process=code+resource(I/O+MM+cpu)

atoi变为整数

使用strace跟踪文件strace ./6-2-1

派生子进程

中断

保存断点

加载进程映像

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-eiXQ7HPF-1605270026967)(assets/1604279206279.png)]

下午【杂】

查看环境变量三种方式:

  • env命令:
  • main函数中的第三个参数进行实现
  • 直接使用genenv在函数中实现

echo $HISTSIZE:查看能够存的命令的条数

环境表和环境变量

每个进程都有一张环境表

通过enciron找到环境表

extern **environ

file查看文件类型

库文件

动态库文件

gcc -Wall -shared 6-7-dlib.c -o libdlib.so

gcc -Wall 6-7.c -o 6-7 -L ./ -ldlib

-L告诉去哪里找库文件

-ldlib说明:l指的是我的库的名称,dlib是库文件名称,库文件名称都以lib开头,实际库文件名称为libdlib

运行可能报错:

echo $PATH
//将文件拷到库文件之后,则调用自己的库又可以成功
sudo cp ./libdlib.so /usr/lib

//将自己的文件从库文件进行删除,则调用自己的库又会失败
sudo rm /usr/lib/libdlib.so
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值