《应用编程 — 进程的概念》
1.进程终止
- 从main返回;
- 调用exit;
- 调用_exit或_Exit;
- 最后一个线程从其启动例程返回;
- 从最后一个线程调用ptherad_exit;
以上五种位正常终止。
- 调用abort;
- 接到一个信号;
- 最后一个线程对取消请求作出响应;
以上三种为异常终止。
其中_exit和_Exit立即进入内核,exit则会先执行一些清理处理,然后返回内核。
#include <stdlib.h>
void exit(int status);
void _Exit(int status);
#include <unistd.h>
void _exit(int status);
2.命令行参数
#include <stdio.h>
int main(int argc, char *argv[])
{
int i;
for(i = 0; i < argc; i++)
{
printf("argv[%d]:%s\n", i, argv[i]);
}
exit(0);
}
假设进行名字为app,则运行./app arg1 TEST foo
运行结果:argv[0]:./app
argv[1]: arg1
argv[2]: TEST
argv[3]: foo
ISO C和POSIX.1都要求argv[argc]是一个空指针。所以可以将参数处理循环改写为:
for(i = 0; argv[i] != NULL; i++)
3.C程序的存储空间布局
- 正文段:这是由CPU执行的机器指令部分,也就是函数。正文段是可共享的,因此在存储器中只需有一个副本,只读。
- 初始化数据段:也叫数据段,包含程序中需明确地赋初值的变量。比如全局变量。int i = 1;
- 未初始化数据段:也叫BSS段,在程序执行前,内核将此段中的数据初始化为0或者空指针。比如全局变量。long sun[10];
- 栈:自动变量以及每次函数调用时所需保存的信息都存放在此段中。说白就是局部变量以及函数调用的时候保存环境的地方。
- 堆:动态存储分配。
size命令可以查看当前一个进程的正文段、数据段等。eg:size /usr/bin/test
4.存储空间分配
- malloc:分配指定字节数的存储区。此存储区中的初始值不确定。
- calloc:为指定数量指定长度的对象分配存储空间,该空间中的每一位(bit)都初始化为0。
- realloc:增加或减少以前分配去的长度。当增加长度时,可能需将以前分配区的内容移动另一个足够大的区域,以便在尾端提供增加的存储区,而新增区域的初始值不确定,会返回新的地址值。
#include <stdlib.h>
void *malloc(size_t size);
void *calloc(size_t nobj, size_t size);
void *realloc(void *ptr, size_t newsize);
void free(void *ptr);
大多数实现所分配的存储空间比所要求的要稍大一些,额外的空间用来记录管理信息。这意味着,如果超过一个已分配区的尾端或者在已分配区起始位置之前进行写操作,则会改写另一块的管理记录信息。
其他可能产生的致命性错误:
- 释放一个已经释放了的块;
- 调用free时所用指针不是3个alloc函数的返回值;
- 如果一个进程调用malloc函数,但却忘记调用free函数,那么该进程占用的存储空间就会连续增加,这被称为泄漏。如果不调用free函数释放不再使用的空间,那么进程地址空间长度就会慢慢增加,直至不再有空闲空间。此时,由于过度的换页开销,会造成性能下降。
5.函数setjmp和longjmp
在C中,goto语句不能跨越函数实现。函数setjmp和longjmp可以实现跨函数的跳转。(目前没有使用到,暂不深入了解)
6.函数getrlimit和setrlimit
目前没有使用到,暂不深入了解。
7.fork函数
#include <unistd.h>
pid_t fork(void);
返回值:父进程返回的是新建的子进程的进程ID,子进程返回的是0。
当调用完fork函数后,子进程获得父进程的数据空间、堆和栈,但是这是子进程单独拥有的,并不和父进程共享,因此修改子进程的变量不会影响父进程的变量。父进程和子进程共享正文段。
由于在fork之后经常跟随着exec,所以现在的很多实现并不执行一个父进程数据段、栈和堆得完全副本,作为代替,使用了写时复制(copy-on-write,COW)技术。
写时复制:写时拷贝是一种可以推迟甚至避免拷贝数据的技术。内核此时并不复制整个进程的地址空间,而是让父子进程共享同一个地址空间。只用在需要写入的时候才会复制地址空间,从而使各个进行拥有各自的地址空间。也就是说,资源的复制是在需要写入的时候才会进行,在此之前,只有以只读方式共享。这种技术使地址空间上的页的拷贝被推迟到实际发生写入的时候。在页根本不会被写入的情况下---例如,fork()后立即执行exec(),地址空间就无需被复制了。
不使用写时复制的缺点:传统的fork()系统调用直接把所有的资源复制给新创建的进程。这种实现过于简单并且效率低下,因为它拷贝的数据或许可以共享。但是如果新进程立即执行了exec()函数,那么之前的拷贝就全都浪费了。
#include "apue.h"
int globvar = 6;
char buf[] = "a write to stdout \n";
int main(void)
{
int var;
pid_t pid;
var = 88;
if(write(STDOUT_FILENO, buf, sizeof(buf) - 1) != sizeof(buf) -1)
{
err_sys("write_error");
}
printf("before fork\n");
if((pid = fork()) < 0)
{
err_sys("fork error");
}
else if(pid == 0)
{
globvar++;
var++;
}
else
{
sleep(2);
}
printf("pid=%ld, glob=%d, var=%d\n",(long)getpid(), globvar, var);
exit(0);
}
$ ./a.out
a write to stdout
before fork
pid=430,glob=7,var=69 //子进程的变量值改变了
pid=430,glob=6,var=68 //父进程的变量值没有改变
由此可以证明:父子进程不同享数据。
$ ./a.out > temp.out
$ cat temp.out
a write to stdout
before fork
pid=430,glob=7,var=69 //子进程的变量值改变了
before fork
pid=430,glob=6,var=68 //父进程的变量值没有改变
问题:这边为什么会输出两个before fork?
因为将标准输出重定向到一个文件时,这个时候标准I/O就变成了一个全缓冲区(当缓冲区或者程序结束才会将缓冲区的数据输出),所以在第一次printf的时候,因为缓冲区没有满所以不会将数据打印出来,而是存在缓冲区中。fork函数将父进程复制完后,会将这个缓冲区也复制下来,所以在子程序的第二个printf中,会在已有的缓冲区数据后面再增加数据,最后在子程序的第二个printf中就会将两个数据一起输出出来。
8.vfork函数
vfork和fork的区别:
执行次序:vfork是先调用子进程,等子进程的_exit(exit是不正确的)或exec被调用后,再调用父进程。
fork对父子进程的调度室由调度器决定的。
数据段的影响:fork采用的是写时复制技术。
vfork的父子进程是共享数据的,所以在子程序中修改变量,父进程的变量也会被修改。(在fork中不会这样)
总结:vfork创建的子进程调用exec前,与父进程是共享一个地址空间的(根本不存在复制的这个步骤,因此直接执行exec效率方面比fork快)。但是需要注意的是:如果子进程修改数据、进行函数调用或者没有调用exec或exit就直接return(return后,会释放局部变量并弹栈。但是因为vfork是共享父进程的地址空间,那么换句话说也就是return掉了main函数。return后还会调用类似exit这样的退出函数。那么就又回到父进程的vfokr往下运行,但是由于之前子进程return掉了main函数的栈,所以会出现段错误。这就类似C++中return会调用局部对象的析构函数,exit不会,是直接退出)会总成未知的后果。
9.wait和waitpid函数
调用wait或waitpid的进程会发生什么:
- 如果其所有子进程都还在运行,则阻塞。
- 如果一个子进程已终止,正等待父进程会获取其终止状态,然后立即返回。
- 如果没有任何子进程,则立即出错返回。
#include <sys/wait.h>
pid_t wait(int *statloc);
pid_t waitpid(pid_t pid, int *statloc, int options);
两个函数返回值:成功,返回进程ID;出错:返回0或-1
这两个函数的区别:
- 在一个子进程终止前,wait使其调用者阻塞,而waitpid有一个选项,可使调用者不阻塞。
- waitpid并不等待在其调用之后额第一个终止子进程,它有若干个选项,可以控制它所等待的进程。