main函数
int main(int argc, char **argv)
C程序一般是从main函数开始执行,当内核执行C程序时,在调用main函数前会先调用一个特殊的启动例程,可执行程序文件将此启动例程指定为程序的起始地址(由链接器设定),启动例程从内核取得命令行参数和环境变量,为调用main函数做好准备。
C程序的存储空间布局
C程序一直由下列几部分组成:
- 正文段 text
由cpu执行的机器指令部分,通常正文段是只读可共享的。 - 初始化数据段 data
存放初始化的全局变量和静态变量 - 未初始化数据段 bss
存放未初始化的全局变量和静态变量,在程序开始执行之前,内核将此段中的数据初始化为0或者空指针。 - 栈
存放自动变量,向下生长,地址变小。 - 堆
动态存储分配,向上生长,地址变大
存放在磁盘上的只有正文段和初始化数据段。(size命令可以查看)
进程环境变量
每个程序都会有一张环境表,全局变量environ包括该指针数组的地址。环境表通常放在进程存储的顶部
extern char **environ;
环境字符串形式: name=value
#include
int putenv(char *str)
函数返回值:成功返回0,出错返回非0
int setenv(const char* name, const char* value, int rewrite);
函数返回值:成功返回0,出错返回非0
int unsetenv(const char* name);
函数返回值:成功返回0,出错返回非0
- 删除一个环境变量很简单,只要在环境表中找到相应环境字符串指针,将后续指针向前移动一个即可。
- 如果修改一个环境变量:
- 如果新的value长度小于或等于现有value长度,则只需复制即可
- 如果新的value长度大于原长度。则必须调用malloc为新字符串分配空间
进程标识
每一个进程都有一个非负整型pid_t表示的唯一进程ID。ID为0通常是调度进程,ID为1通常是 init进程,在自举过程中,此进程在自举内核后启动一个UNIX系统。
#include
pid_t getpid()
返回值:调用进程的进程ID
pid_t getppid()
返回值:调用进程的父进程ID
uid_t getuid()
返回值:调用进程的实际用户ID
uid_t getuid()
返回值:调用进程的有效用户ID
gid_t getgid()
返回值:调用进程的实际组ID
gid_t getegid()
返回值:调用进程的有效组ID
进程创建
#include
pid_t fork(void)
返回值:子进程返回0,父进程返回子进程ID,若出错,返回-1。
子进程是父进程的副本,子进程获得父进程的数据空间,堆和栈的副本。父子进程共享正文段。
写时复制
-
fork失败有2个主要原因:
- 系统中已经有太多的进程
- 实际用户ID的进程数量超出了限制
-
fork有2个主要用法:
- 父进程希望复制自己,使父进程和子进程执行不同的代码段。网络服务进程是常见的。
- 一个进程要执行一个不同的程序。这对shell是常见的情况。子进程从fork返回之后立即调用exec。
-
文件共享
父进程所有的打开文件描述符都被复制到子进程中。父进程和子进程共享同一个文件偏移量。
进程终止
-
有8种方式可以使进程正常终止
- 从main返回
- 调用exit - 调用_exit和_Exit
- 最后一个线程从启动例程返回
- 从最后一个线程调用pthread_exit
- 调用abort
- 接到一个信号
- 最后一个线程对取消请求作出响应
无论进程如何终止,最后都会执行内核中同一段代码,这段代码为xiangying
-
退出函数
#include
void exit(int status); void _Exit(int status); #include void _exit(int status);
3个函数用于正常终止一个程序:
_exit和_Exit立即进入内核,_exit和_Exit其目的是为进程提供一个无需运行终止处理程序或信号处理程序而终止的方法。它们并不冲洗标准I/O流 。
exit则先执行一些清理处理(典型的比如执行标准I/O库的清理关闭操作,对于所有的流调用fclose函数)
- 进程清理函数
#include
int atexit(void *(func) void); 函数返回值:成功返回0,出错返回非0
进程可以注册"终止处理函数",exit调用这些函数与他们注册时的顺序相反,同一函数若注册多次,也会被调用多次。
回收进程资源
当一个进程正常或异常终止时,内核就向其父进程发送 SIGCHLD信号,父进程可以选择忽略(默认做法),也可以提供一个信号处理函数。
-
wait函数
内核为每一个 终止子进程保存一定信息:包括进程ID,退出状态,以及CPU时间总量等信息。#include
pid_t wait(int *statloc); 返回值:成功返回进程ID,出错返回0或-1 如果一个进程有几个子进程,那么只要有一个子进程终止,wait就返回,在子进程终止前,调用者将会被阻塞。其错误原因:进程没有子进程,或者函数调用该被信号中断。
函数参数
statloc
是一个指针,如果不为空,则进程的终止状态会存放在它所指向的地址单元内。我们可以通过一些列的宏,来查看这些信息。宏 作用 WIFEXITED(status) 若子进程正常终止,返回true WIFSIGNALED(status) 若子进程异常终止,返回true WIFSTOPPED(status) WIFCONTINUED(status) -
waitpid函数
#include
pid_t waitpid(pid_t pid, int *staloc, int options); 返回值:成功返回进程ID,出错返回0或-1 waitpid相较于wait区别如下:
- 在一个子进程终止前,wait使其调用者阻塞,而waitpid有一选项,可使得调用者不阻塞。
- wait只要有一个子进程终止,就立即返回,waitpid 有很多选项,可以控制它等待的进程。
pid 作用 pid == -1 等待任一子进程 pid > 0 等待进程ID与pid相等的子进程 pid == 0 等待组ID等于调用进程组ID的任一子进程 pid < 0 等待组ID等于pid绝对值的任一子进程 常量 说明 WCONTINUED ? WNOHANG waitpid不阻塞 WUNTRACED ? 相对于wait函数:
- waitpid可等待一个特定的进程
- waitpid提供了一个wait的非阻塞版本。
- waitpid通过 WCONTINUED和WUNTRACED支持作业控制
僵尸进程
一个已经终止,但其父进程尚未对其进行善后处理(获取子进程的有关信息,释放它仍然占用的资源)的进程。孤儿进程
如果父进程在子进程之前终止,则它的子进程将会成为孤儿进程。此后,他们的父进程会变为init进程。
函数exec
在上面我们提到了fork的用法2:创建后调用exec以执行另一个程序
#include
int execl(const char *pathname, const char* arg0, ... /* (char*)0 */);
int execv(const char* pathname, char*const argv[]);
int execle(const char* filename, const char* arg0, ... /* (char*)0 */)
int execve(const char* pathname, char* const argv[], char* const envp[]);
int execlp(const char* filename, const char* arg0, ... /* (char*)0 */)
int execvp(const char* filename, char* const argv[]);
int fexecve(int fd, char* const argv[], char* const envp[]);
返回值:若出错,返回-1。若成功,不返回
进程资源限制
每个进程都有一组资源限制,其中可以用以下函数查询和更改。
#include
struct rlimit
{
rlim_t rlim_cur; // soft limit
rlim_t rlim_max; // hard limit
};
int getrlimit(int resource, struct rlimit* rlptr);
返回值:成功返回0,错误返回非0
int setrlimit(int resource, const struct rlimit* rlptr)
返回值:成功返回0,错误返回非0
在更改资源限制时,必须遵循下列3条原则:
- 任何一个进程都可以提升软限制值,但软限制值必须小于或等于硬限制值。
- 任何一个进程都可以降低硬限制值,但硬限制值必须大于或等于软限制值。
- 只有超级用户进程可以提高硬限制值。
参数resource
取下列值:
限制 | 作用 |
---|---|
RLIMIT_NOFILE | 每个进程能打开的做多文件数 |
RLIMIT_FSIZE | 可以创建文件的最大字节长度 |
RLIMIT_NICE | 影响进程调度优先级nice值可设置的最大值 |
RLIMIT_CORE | core文件的最大字节数,值为0则不会创建core文件 |
RLIMIT_STACK | 栈的最大字节长度 |