apue 第7章 进程环境

时间:2022-12-24 13:42:02

main函数

C程序总是从main函数开始执行。main函数的原型是:

int main(int argc, char *argv[]);

其中,argc是命令行参数的数目,argv是指向参数的各个指针所构成的数组。

当内核执行C程序时,在调用main之前先调用一个特殊的启动例程。可执行程序文件将此启动例程指定为程序的起始地址——这是由链接编辑器设置的,而连接编辑器则由C编译器调用。启动例程从内核取得命令行参数和环境变量值,然后为按上述方式调用mian函数做好安排。

进程终止

有8中方式使进程终止,其中5种为正常终止,它们是:

  • 从main返回;
  • 调用exit;
  • 调用_exit或_EXIT;
  • 最后一个线程从启动例程返回;
  • 从最后一个线程调用pthread_exit。

异常终止有3种方式,它们是:

  • 调用abort;
  • 接到一个信号;
  • 最后一个线程对取消请求做出响应。

退出函数

3个函数用于终止一个程序

  • _exit和_Exit立即进入内核
  • exit则执行一些清理工作:所有的打开流调用fclose函数
#include <stdio.h>
void exit(int status);
void _EXIT(int status);
#include <unistd.h>
void _exit(int status);

3个退出函数都带一个整型参数,称为终止状态。大多数UNIX系统shell都提供检查进程终止状态的方法。

下面3种情况下进程的终止状态是未定义的

  • 调用这些函数时不带终止状态
  • main执行了一个无返回值的return语句
  • main没有声明返回类型为整型,则该进程的终止状态是未定义的

若main的返回类型是整型,并且main执行到最后一条语句时返回,那么该进程的终止状态是0;

C语言的启动和终止

apue 第7章 进程环境

内核调用exec运行C启动例程,C启动例程调用main()函数,其他的所有函数都是有main函数直接或间接调用的。
内核与用户进程的交互,直接使用的只有三个函数:exec、_exit、_Exit

函数atexit

按照ISO C规定,一个进程可以登记多至32个函数,这些函数将有exit自动调用。这些函数为终止处理程序,并调用atexit函数来登记

#include <stdlib.h>
int atexit(void (*func)(void));
//返回值:若成功,返回0;若出错,返回非0

其中,atexit的参数是一个函数地址,当调用此函数时无需向它传递任何参数,也不期望它会返回一个值。exit调用这些函数的顺序与它们登记的顺序是相反的。同一函数若被登记多次,也会被调用多次。

使用:

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>

static void my_exit1();
static void my_exit2();

int main()
{
    if (atexit(my_exit2) != 0)
    {
        printf("can't register my_exit2\n");
    }

    if (atexit(my_exit1) != 0)
    {
        printf("can't register my_exit1\n");
    }
    if (atexit(my_exit1) != 0)
    {
        printf("can't register my_exit1\n");
    }

    printf("main is done\n");

    return 0;
}

static void my_exit1()
{
    printf("first exit handler\n");
}
static void my_exit2()
{
    printf("second exit handler\n");
}
main is done
first exit handler
first exit handler
second exit handler

命令行参数

当执行一个程序时,调用exec的进程可以将命令行参数传递给该新程序。

#include <stdio.h>
#include <stdlib.h>

int main(int argc, char* argv[])
{
    for(int i = 0; i < argc; ++i)
    {
        printf("argv[%d]: %s\n", i,argv[i]);
    }

    exit(0);
}
yanke@vm:~/Code/apue/ch7UnixForkEnv$ ./a.out test foo bar yanke
argv[0]: ./a.out
argv[1]: test
argv[2]: foo
argv[3]: bar
argv[4]: yanke

环境表

每个程序都接受到一张环境表。与参数表一样,环境表也是一个字符指针数组,其中每个指针包含一个以null结束的C字符串的地址。全局变量environ则包含了该指针数组的地址:

extern char **environ;

我们称environ为环境指针,指针数组为环境表,其中各指针指向的字符串为环境字符串。

  • 由5个字符串组成的环境指针

apue 第7章 进程环境

C程序的存储空间布局

可参考:http://blog.csdn.net/charles1e/article/details/51492638

  • 正文段。这是由CPU执行的机器指令部分。通常正文段是可共享的。 另外正文段是只读的,以防止程序被意外修改。
  • 初始化数据段。通常将此段称为数据段,它包含了程序中需明确地赋初值的变量。函数之外的变量通常在这个段内。
  • 未初始化数据段。通常将此段称为bss段,这一名称来源于早期汇编程序一个操作符,在程序开始执行之前,内核将此段中的数据初始化为0或空指针。
  • 栈。自动变量以及每次函数调用时所需保存的信息都存放在此段中。。
  • 堆。通常在堆中进行动态存储分配。由于历史上形成的惯例,堆位于未初始化数据段和栈之间。

apue 第7章 进程环境

共享库

参考:http://www.cnblogs.com/zhoutian6214/archive/2008/11/11/1331646.html
静态链接库:对函数库的链接是放在编译时期(compile time)完成的。所有相关的对象文件(object file)与函数库(library)被链接合成一个可执行文件(executable file)。程序在运行时,与函数库无关,通常文件名为“libxxx.a”的形式。
动态链接库(dynamic link library):把对一些库函数的链接载入推迟到程序运行的时期(runtime)

把库函数推迟到程序运行时期载入的好处:

  • 实现进程之间的资源共享。个程序的在运行中要调用某个动态链接库函数的时候,操作系统首先会查看所有正在运行的程序,看在内存里是否已有此库函数的拷贝了。如果有,则让其共享那一个拷贝;只有没有才链接载入。这样的模式虽然会带来一些“动态链接”额外的开销,却大大的节省了系统的内存资源。C的标准库就是动态链接库,也就是说系统中所有运行的程序共享着同一个C标准库的代码段。
  • 将一些程序升级变得简单。用户只需要升级动态链接库,而无需重新编译链接其他原有的代码就可以完成整个程序的升级。Windows 就是一个很好的例子。
  • 甚至可以真正做到链接载入完全由程序员在程序代码中控制。程序员在编写程序的时候,可以明确的指明什么时候或者什么情况下,链接载入哪个动态链接库函数。你可以有一个相当大的软件,但每次运行的时候,由于不同的操作需求,只有一小部分程序被载入内存。所有的函数本着“有需求才调入”的原则,于是大大节省了系统资源。比如现在的软件通常都能打开若干种不同类型的文件,这些读写操作通常都用动态链接库来实现。在一次运行当中,一般只有一种类型的文件将会被打开。所以直到程序知道文件的类型以后再载入相应的读写函数,而不是一开始就将所有的读写函数都载入,然后才发觉在整个程序中根本没有用到它们。

存储空间分配

ISO C 3个存储空间动态分配的函数如下:

  1. malloc,分配指定字节数的存储区。此存储区中的初始值不确定。
  2. calloc,为指定数量指定长度的对象分配存储空间。该空间中的每一位都初始化为0
  3. realloc,增加或减少以前分配区的长度。
#include <stdlib.h>
void *malloc(zise_t size);
void *calloc(size_t nobj, size_t size);
void *realloc(void *ptr, size_t newsize);
//3个函数返回值:若成功,返回非空指针;若出错,返回NULL
void free(void *ptr);

free为释放空间,一般释放后会送入可用内存池,以后申请空间时再分配。

环境变量

ISO C定义了一个函数getenv,可以用其取环境变量值

#include <stdlib.h>
char *getenv(const char *name);
//返回值:指向与name关联的value的指针;若未找到,返回NULL

注意,此函数返回一个指针,它指向name = value 字符串中的value。我们应当使用getenv从环境中取一个指定环境变量的值,而不是直接访问environ。

设置环境变量的的函数

#include <stdlib.h>
int putenv(char *str);
//函数返回值:若成功,返回0;若出错,返回非0
int setenv(const char *name, const char *value, int rewrite);
int unsetenv(const char *name);
//两个函数返回值:若成功,返回0;若出错,返回-1
  • putenv取形式为name = value的字符串,将其放到环境表中。如果name已经存在,则先删除其原来的定义。
  • setenv将name设置为value。如果在环境中name已经存在,那么(a)若rewrite非0,则首先删除其现有的定义;(b)若rewrite为0,则不删除现有的定义。
  • unsetenv删除name的定义。

函数setjmp和longjmp

参考:http://blog.csdn.net/chenyiming_1990/article/details/8683413

setjmp和longjmp函数是非局部跳转语句。非局部指的是,这不是由普通C语言goto,语句在一个函数内实施的跳转,而是在栈上跳过若干调用帧,返回到当前函数调用路径上的某一个函数中。

#include <setjmp.h>
Int setjmp(jmp_buf  env);
//返回值:若直接调用则返回0,若从longjmp调用返回则返回非0值的longjmp中的val值
Void longjmp(jmp_buf env,int val);
//调用此函数则返回到语句setjmp所在的地方,其中env 就是setjmp中的 env,而val 则是使setjmp的返回值变为val。

当检查到一个错误时,则以两个参数调用longjmp函数,第一个就是在调用setjmp时所用的env,第二个参数是具有非0值的val,它将成为从setjmp处返回的值。使用第二个参数的原因是对于一个setjmp可以有多个longjmp。

例子:

#include <stdio.h>
#include <setjmp.h>

static jmp_buf buf;

void second()
{
    printf("first!\n");
    longjmp(buf, 1);
}

void first()
{
    second();
    printf("first!\n");
}

int main()
{
    if (!setjmp(buf))
        first();
    else
        printf("main!\n");

    return 0;
}

输出:

first!
main!

说明:调用first(),然后调用second(),setjmp返回1,继续进入主函数里面判断。每次longjmp之后都会去检测setjmp(buf).因此输出else的内容。

  • setjmp与longjmp结合使用时,它们必须有严格的先后执行顺序,也即先调用setjmp函数,之后再调用longjmp函数,以恢复到先前被保存的“程序执行点”。否则,如果在setjmp调用之前,执行longjmp函数,将导致程序的执行流变的不可预测,很容易导致程序崩溃而退出

  • longjmp必须在setjmp调用之后,而且longjmp必须在setjmp的作用域之内。具体来说,在一个函数中使用setjmp来初始化一个全局标号,然后只要该函数未曾返回,那么在其它任何地方都可以通过longjmp调用来跳转到 setjmp的下一条语句执行。实际上setjmp函数将发生调用处的局部环境保存在了一个jmp_buf的结构当中,只要主调函数中对应的内存未曾释放 (函数返回时局部内存就失效了),那么在调用longjmp的时候就可以根据已保存的jmp_buf参数恢复到setjmp的地方执行。

函数getrlimit和setrlimit

参考:http://www.cnblogs.com/niocai/archive/2012/04/01/2428128.html

可以使用getrlimit和setrlimit函数来查询和更改资源限制。获取或设定资源使用限制。每种资源都有相关的软硬限制,软限制是内核强加给相应资源的限制值,硬限制是软限制的最大值。非授权调用进程只可以将其软限制指定为0~硬限制范围中的某个值,同时能不可逆转地降低其硬限制。授权进程可以任意改变其软硬限制。RLIM_INFINITY的值表示不对资源限制。

#include <sys/resource.h>

int getrlimit(int resource, struct rlimit *rlim);
int setrlimit(int resource, const struct rlimit *rlim);
  • resource:可能的选择有

    • RLIMIT_AS //进程的最大虚内存空间,字节为单位。
    • RLIMIT_CORE //内核转存文件的最大长度。
    • RLIMIT_CPU //最大允许的CPU使用时间,秒为单位。当进程达到软限制,内核将给其发送SIGXCPU信号,这一信号的默认行为是终止进程的执行。然而,可以捕捉信号,处理句柄可将控制返回给主程序。如果进程继续耗费CPU时间,核心会以每秒一次的频率给其发送SIGXCPU信号,直到达到硬限制,那时将给进程发送 SIGKILL信号终止其执行。
    • RLIMIT_DATA //进程数据段的最大值。
    • RLIMIT_FSIZE //进程可建立的文件的最大长度。如果进程试图超出这一限制时,核心会给其发送SIGXFSZ信号,默认情况下将终止进程的执行。
    • RLIMIT_LOCKS //进程可建立的锁和租赁的最大值。
    • RLIMIT_MEMLOCK //进程可锁定在内存中的最大数据量,字节为单位。
    • RLIMIT_MSGQUEUE //进程可为POSIX消息队列分配的最大字节数。
    • RLIMIT_NICE //进程可通过setpriority() 或 nice()调用设置的最大完美值。
    • RLIMIT_NOFILE //指定比进程可打开的最大文件描述词大一的值,超出此值,将会产生EMFILE错误。
    • RLIMIT_NPROC //用户可拥有的最大进程数。
    • RLIMIT_RTPRIO //进程可通过sched_setscheduler 和 sched_setparam设置的最大实时优先级。
    • RLIMIT_SIGPENDING //用户可拥有的最大挂起信号数。
    • RLIMIT_STACK //最大的进程堆栈,以字节为单位。
  • rlim:描述资源软硬限制的结构体,原型如下

struct rlimit {
  rlim_t rlim_cur;
  rlim_t rlim_max;
};
  • 返回值:成功执行时,返回0。失败返回-1,errno被设为以下的某个值
    • EFAULT:rlim指针指向的空间不可访问
    • EINVAL:参数无效
    • EPERM:增加资源限制值时,权能不允许