1、什么是进程?
通俗的说,进程是一个具有一定独立功能的程序的一次运行活动。,对于Linux这种多任务操作系统来说,每一个运行者的程序就构成一个进程,可以用cat /proc/sys/kernel/pid_max
命令查看系统支持的最大进程数,我在Ubuntu14.04中得到的结果是32768。
2、进程与程序的区别与联系
(1)进程是动态的,程序是静态的
(2)一个进程只对应一个程序,一个程序可以对应多个进程;
(3)进程的生命周期是有限的,而程序则可以永久保存与磁盘中。
3、进程的特点:动态性、并发性、独立性、异步性
动态性:由进程的概念可知,程序运行起来才是进程,所以具有动态性
并发性:就是说在同一时间可以同时执行多个进程,这叫做并发性
独立性:进程是一个能独立运行的基本单位,同时也是系统分配资源和调度的独立单位;
异步性:所谓异步性是指进程以不可预知的速度向前推进。每个进程何时执行,何时暂停,以怎样的速度向前推进,何时完成等,都是不可预知的。
4、进程的三态——就绪、执行、阻塞
图一:进程执行过程的三态
进程执行过程:
如图一所示,进程A被创建后进入就绪态,通过进程调度占用CPU进入执行态,用完时间片后回到就绪态等待下一次被调度;
在执行过程中如果遇到I/O请求(如访问磁盘、文件)且得不到满足(如磁盘、文件正在被进程B占用),则进程A进入阻塞态,CPU被让出来给其他进程使用,待B进程使用完磁盘、文件后,A进程便解除阻塞状态去执行I/O操作,但由于进入阻塞时让出了CPU,所以I/O完成后A进程只能回到就绪状态了。
5、进程互斥、临界资源、临界区
进程互斥:指若干进程都要使用某一资源,但该资源在同一时刻最多允许一个进程使用(临界资源),这时其他进程必须等待,直到占用该资源者释放了该资源为止。
临界资源:操作系统中将同一时刻只允许一个进程访问的资源称为临界资源。
临界区:进程中访问临界资源的那段程序代码称为临界区。为实现对临界资源的互斥访问,应保证诸进程互斥地进入各自的临界区。
6、进程同步
一组进程按一定的顺序执行的过程称为进程间的同步,具有同步关系的这组进程称为合作进程。
7、进程调度
在任何时刻,一个CPU上运行的进程只能有一个,但系统中就绪的进程有很多,所以操作系统系统按一定算法,从一组待运行的进程中选出一个来占有CPU运行,这就是进程调度。
常见的调度算法有:先来先服务、短进程优先、高优先级优先、时间片轮转法
8、调度时机
按调度时机,调度分为抢占式调度和非抢占式调度。
举个例子,A、B两个进程,A优先级高B优先级低,B进程正在运行,此时A进程请求占用CPU,由于A的优先级高,抢占式调度会强制使B进程结束使A进程运行,而非抢占式调度会等B进程执行完毕再使A进程执行。
1、main()函数与命令行参数
int main(int argc, char *argv[]);
argc是命令行参数的数目,argv是指向各个命令行参数的指针构成的指针数组,其中,argv[0]一定指向程序名,argv[argc]是一个空指针。
我们在使用 shell 命令的时候经常使用各种参数, 如gcc main.c -o main、ls -l,shell可以把这些参数传递给程序,具体就是传递给main函数的argc和argv了。
举一个例子,实现shell的echo命令(PS:shell的echo并不回显argv[0])
/*
**程序说明:实现echo命令——将命令行参数回显到标准输出
*/
#include
#include
int main(int argc, char *argv[])
{
int i = 0;
if (argc < 2)
{
printf("\n");
exit(0);
}
for (i = 1; argv[i] != NULL; i++)
/*或者for (i = 1; i < argc; i++)*/
printf("%s ", argv[i]);
printf("\n");
exit(0);
}
运行结果,说明命令行参数的确传递给了main函数。
2、进程终止(termination)
Linux系统有8种方式可以使进程终止,其中5种正常终止,3种异常终止。
5种正常终止:
(1)从main函数返回
(2)调用exit
(3)调用_exit或_Exit
(4)最后一个线程从其启动例程返回
(5)从最后一个线程调用pthread_exit
3种异常终止:
(6)调用abort
(7)接到一个信号
(8)最后一个线程对取消请求做出响应
我们暂且只讨论前三种,其他的等学到信号和线程时再说。
(1)从main函数返回:在main()中使用return语句返回一个int值给调用者,返回0表示正常终止,其他表示异常终止。
在说明(2)(3)之前我们先看一下这三个函数的原型:
#include
void exit(int status);
void _Exit(int status);
#include
void _exit(int status);
首先,_exit和_Exit是系统调用,会立即结束程序;调用exit会先执行一些处理操作(如调用终止处理程序、冲洗I/O流等等),然后调用_exit或_Exit结束程序;
其次,三个函数都带有一个整形参数status,我们称其为退出状态(exit status),取值范围为0-255,每一个取值都对应着一种退出状态;
最后,在main函数中调用exit(ststus)等价于调用return status。
3、atexit
#include
int atexit(void (*func)(void));
/*返回值:若成功,返回0;若失败,返回非0*/
用atexit注册过的函数会在调用exit时以注册顺序的逆序被调用,被注册的函数称为“终止处理程序”(exit handler)。
“注册”这个词语听起来可能不太好懂,其实很简单啦,就是把终止处理程序的函数名(也就是函数地址)作为参数传递给atexit,但是,终止处理程序必须是无参无返回值的。
举个例子说明顺序注册逆序调用是怎么一回事:
/*
**程序说明:使用atexit函数
*/
#include
#include
/*
**声明两个终止处理函数my_atexit1和my_atexit2
*/
static void my_atexit1(void);
static void my_atexit2(void);
int main(int argc, char *argv[])
{
if (atexit(my_atexit2) != 0) /*这里只是注册,并不执行终止处理程序*/
printf("can't register my_atexit2\n");
else
printf("register my_atexit2 first time done\n");
if (atexit(my_atexit1) != 0)
printf("can't register my_atexit1\n");
else
printf("register my_atexit1 first time done\n");
if (atexit(my_atexit1) != 0)
printf("can't register my_atexit1\n");
else
printf("register my_atexit1 second time done\n");
printf("main is done and before exit\n");
exit(0);
}
static void my_atexit1(void)
{
printf("my_atexit1\n");
}
static void my_atexit2(void)
{
printf("my_atexit2\n");
}
编译并运行结果,大家应该明白了吧!
注意终止处理程序每注册一次,就会被调用一次,my_exit1注册了两次,所以有两条打印语句。
4、环境表与环境变量
每个程序都收到一张环境表,环境表是一个字符指针数组,其每个元素都指向一个形如name=value的环境字符串,而全局变量extern char **environ又指向环境表这个字符指针数组,environ叫做环境指针。这样,环境指针、环境表、环境字符串构成了环境,如图二所示:
图二 由5个环境字符串组成的环境
1)用environ指针查看整个环境,可以用如下方式:
#include
extern char **environ; /*全局变量,环境指针*/
int main()
{
int i;
/*
**查看整个环境,必须使用environ指针
*/
for(i = 0; environ[i] != NULL; i++)
printf("environ[%d]: %s\n", i, environ[i]);
return 0;
}
运行部分结果:
2)getenv从环境中取名字为name的环境变量的值,返回指向value的指针
#include
char *getenv(const char *name);
/*返回值:若找到,返回与name关联的value指针;否则,返回NULL*/
3)putenv、setenv、unsetenv修改环境变量
#include
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已存在,则先删除原来定义,然后用新值替换,如putenv(“PATH=/usr/apue”);
setenv:将name设置为value,这里分两种情况:
a) name不存在,直接添加新的环境变量。
b) name已存在:若rewrite为真,就用 value 覆盖 name 原来的值;若rewrite为假,则保留 name 原来的值。
unsetenv:删除name的定义,若name不存在也不算出错。
2)、3)部分测试代码:
#include .h>
#include .h>
int main()
{
/*
**查看指定环境变量的值
*/
printf("HOME = %s\n", getenv("HOME"));
printf("PATH = %s\n", getenv("PATH"));
printf("PWD = %s\n", getenv("PWD"));
/*
**修改环境变量
*/
putenv("PWD=/home/"); /*注意=两边不能有空格*/
printf("PWD = %s\n", getenv("PWD")); /*putenv直接用新值替换*/
setenv("PATH", "/user/local/bin", 0);
printf("PATH = %s\n", getenv("PATH")); /*rewrite=0,不用新值替换*/
setenv("PATH", "/user/local", 1);
printf("PATH = %s\n", getenv("PATH")); /*rewrite=1,用新值替换*/
unsetenv("PWD");
if (!getenv("PWD"))
printf("PWD is deleted\n"); /*PWD环境变量被删除*/
return 0;
}
运行结果:
关于修改环境变量要注意一点,我们所做的修改只在当前进程和修改之后产生的子进程有效,而不能影响父进程(通常为shell)的环境,也就是说你这个程序运行结束,你做的修改就全部无效,这是用env命令查看可发现所有的环境变量都已回到初始的样子。
5、C程序的存储空间布局
一个C程序从低地址到高地址依次是正文段(代码段、text段)、初始化数据段(data段)、未初始化数据段(bss段)、堆(heap)、栈(stack)这几个部分组成,一种典型的存储空间布局如图三所示:
图三 典型的存储空间安排
正文段(text段):通常是可共享的、只读的,对于32位Intel x86处理器的Linux,正文段一定从0x08048000开始。代码块外const修饰的只读变量存放在此位置。
初始化数据段(data段):存放已初始化的变量,有两张情况:
a)代码块外已初始化的变量
b)代码块内用static修饰的已初始化变量
未初始化数据段(bss段):存放未初始化数据,也有两种情况:
a)代码块外未初始化的变量
b)代码块内用static修饰的未初始化变量
堆(heap):我们经常用malloc、calloc进行动态存储分配,分配的空间就在堆中进行。
栈(stack):存放代码块内没有用ststic修饰的自动变量等数据,代码块内const修饰的只读变量也存放在栈中。
以上各个段都存放哪些数据大家一定要牢记,最好是在理解了const和static的基础上记忆,效果更好。
既然提到了const和static,那就顺便复习下这两个关键字(PS:这两个关键字的含义在可是经常出现在面试中哦)
const的作用:
1)const是作为类型的附加修饰符来使用的,用const修饰的变量必须在声明时初始化,而且该变量是只读的,既它的值不能被更改;
2)提高代码可读性和可维护性。关键字const可以为读代码的人传达非常有用的信息,可以让读者很清楚的知道这些变量的值不能被修改,同时也为代码的维护提供了方便。
3)const也提供给编译器一些有用的信息,一旦不小心修改了这些变量的值,编译器会报错。
static的作用:
在C语言中:
1)static用于所有函数体外的全局变量声明,表明该变量只能在声明它的源文件中使用;
2)static用于函数的定义,表明该函数只能在声明它的源文件中使用;
3)static用于函数体内的局部变量声明,其作用域为该函数体,表明该变量的内存只在第一次调用该函数时分配一次,其值在下次调用时仍维持上次的值,而且一直到整个程序运行结束才销毁。
在C++中除了上面3条,还有下面2条:
4)在类中的static成员变量属于整个类所拥有,对类的所有对象只有一份拷贝;
5)在类中的static成员函数属于整个类所拥有,这个函数不接收this指针,因而只能访问类的static成员变量。
堆向高内存地址生长,栈向低内存地址生长,堆顶和栈底之间有一块很大的未用空间,叫做虚地址空间,当堆和栈的空间不够用时,就分别从两个方向向其扩展。
6、存储空间分配
和存储空间分配相关的函数:
#include
/*分配指定字节数的存储区,存储区中初始值不确定*/
void *malloc(size_t size);
/*
**为指定数量指定长度的对象分配空间,
**空间中每一位都初始化为0。
**但不保证此0与空指针的NULL或浮点数0相同
*/
void *calloc(size_t nobj, size_t size);
/*增加或减少以前分配区(ptr指向的地址)的长度,新增区域初始值不确定*/
void *realloc(void *ptr, size_t newsize);
/*申请的空间用完后必须释放,否则会造成内存泄露*/
void free(void *ptr);
7、函数setjmp和longjmp
这两个函数的功能是跨越函数(或者说跨越栈帧)进行跳转,它们在处理深层次嵌套函数的返回是非常有用的。
举个例子:假如有1000个嵌套的for循环,在最后一层嵌套中出错要返回,正常情况需要一层一层的返回,这显然是费时的,而有了这两个函数,我们就可直接跳转到最外层for循环之外,是不是很方便呢?
让我们看下这两个函数的原型:
#include
int setjmp(jmp_buf env);
/*返回值;若直接调用,返回0;若从longjmp返回,则为非0*/
void longjmp(jmp_buf env, int val);
这两个函数是相互配合使用的,我们一般先调用setjmp这个函数来设置跳转点,此时函数返回值是0,它的参数env是一个特殊类型jump_buf,通常定义为全局变量。
调用longjmp可以跳转到setjmp所在的位置,此时setjmp会再执行一次,但这次返回值就不是0了,而是longjmp的第二个参数val,val的值可以自己设置,它的作用是判断究竟是从哪个位置跳转回来的,所以 setjmp下面一般跟着一组if…else if或者switch来根据不同的返回值做不同的处理。
特殊的,如果val = 0,则 setjmp的返回值是 1,这样规定是为了避免跳转出现死循环。
举个例子:
#include
#include
#include
static jmp_buf jmpbuffer;
int ret;
static void f1();
static void f2();
static void f3();
static void f4();
int main()
{
ret = setjmp(jmpbuffer); //设置跳转点
if (ret == 0) //第一跳
{
printf("first jump: from main()\n");
printf("Begin: main() call f1()\n");
f1();
}
else //第二跳
{
printf("second jump: from f%d()\n", ret);
}
return 0;
}
static void f1()
{
printf("Begin: f1() call f2()\n");
f2();
printf("End: f1 call f2()\n");
}
static void f2()
{
printf("Begin: f2()() call f3()\n");
f3();
printf("End: f2 call f3()\n");
}
static void f3()
{
printf("Begin: f3()() call f4()\n");
f4();
printf("End: f3 call f4()\n");
}
static void f4()
{
longjmp(jmpbuffer, 4);
}
结尾:进程的相关概念和进程环境暂且大概就是这些了,目前我的理解还比较浅,还写不出多么深刻的内容,也只能把书上的知识加以总结,以后有了更深的理解后,再来对这篇博客内容进行修改。