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语言的启动和终止
内核调用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个字符串组成的环境指针
C程序的存储空间布局
可参考:http://blog.csdn.net/charles1e/article/details/51492638
- 正文段。这是由CPU执行的机器指令部分。通常正文段是可共享的。 另外正文段是只读的,以防止程序被意外修改。
- 初始化数据段。通常将此段称为数据段,它包含了程序中需明确地赋初值的变量。函数之外的变量通常在这个段内。
- 未初始化数据段。通常将此段称为bss段,这一名称来源于早期汇编程序一个操作符,在程序开始执行之前,内核将此段中的数据初始化为0或空指针。
- 栈。自动变量以及每次函数调用时所需保存的信息都存放在此段中。。
- 堆。通常在堆中进行动态存储分配。由于历史上形成的惯例,堆位于未初始化数据段和栈之间。
共享库
参考: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个存储空间动态分配的函数如下:
- malloc,分配指定字节数的存储区。此存储区中的初始值不确定。
- calloc,为指定数量指定长度的对象分配存储空间。该空间中的每一位都初始化为0
- 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:增加资源限制值时,权能不允许