APUE学习记录——进程环境

Linux环境编程 专栏收录该内容
4 篇文章 0 订阅

进程相关概念

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 <stdio.h>
#include <stdlib.h>

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 <stdlib>
void exit(int status);
void _Exit(int status);

#include <unistd.h>
void _exit(int status);

  首先,_exit和_Exit是系统调用,会立即结束程序;调用exit会先执行一些处理操作(如调用终止处理程序、冲洗I/O流等等),然后调用_exit或_Exit结束程序;
  其次,三个函数都带有一个整形参数status,我们称其为退出状态(exit status),取值范围为0-255,每一个取值都对应着一种退出状态;
  最后,在main函数中调用exit(ststus)等价于调用return status。
3、atexit

#include <stdlib.h>
int atexit(void (*func)(void));

               /*返回值:若成功,返回0;若失败,返回非0*/

  用atexit注册过的函数会在调用exit时以注册顺序的逆序被调用,被注册的函数称为“终止处理程序”(exit handler)
  “注册”这个词语听起来可能不太好懂,其实很简单啦,就是把终止处理程序的函数名(也就是函数地址)作为参数传递给atexit,但是,终止处理程序必须是无参无返回值的
  举个例子说明顺序注册逆序调用是怎么一回事:
  

/*
**程序说明:使用atexit函数
*/

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

/*
**声明两个终止处理函数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 <stdio.h>

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 <stdlib.h>
char *getenv(const char *name);

          /*返回值:若找到,返回与name关联的value指针;否则,返回NULL*/

  3)putenv、setenv、unsetenv修改环境变量

#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已存在,则先删除原来定义,然后用新值替换,如putenv(“PATH=/usr/apue”);

setenv:将name设置为value,这里分两种情况:
a) name不存在,直接添加新的环境变量。
b) name已存在:若rewrite为真,就用 value 覆盖 name 原来的值;若rewrite为假,则保留 name 原来的值。

unsetenv:删除name的定义,若name不存在也不算出错。
2)、3)部分测试代码:

#include <stdio.h>
#include <stdlib.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 <stdlib.h>

/*分配指定字节数的存储区,存储区中初始值不确定*/
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 <setjmp.h>
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 <stdio.h>
#include <stdlib.h>
#include <setjmp.h>

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);
}

运行结果:
这里写图片描述


结尾:进程的相关概念和进程环境暂且大概就是这些了,目前我的理解还比较浅,还写不出多么深刻的内容,也只能把书上的知识加以总结,以后有了更深的理解后,再来对这篇博客内容进行修改。


  • 0
    点赞
  • 0
    评论
  • 0
    收藏
  • 打赏
    打赏
  • 扫一扫,分享海报

©️2022 CSDN 皮肤主题:大白 设计师:CSDN官方博客 返回首页

打赏作者

与你见证

你的鼓励将是我创作的最大动力

¥2 ¥4 ¥6 ¥10 ¥20
输入1-500的整数
余额支付 (余额:-- )
扫码支付
扫码支付:¥2
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、C币套餐、付费专栏及课程。

余额充值