Linux环境编程之进程

来源:互联网 发布:制导算法 编辑:程序博客网 时间:2024/05/21 04:22

    一.概念

    什么是进程,什么是程序。这个在操作系统的书籍上有很多种阐述。程序是一个包含可执行代码的文件,它放在磁盘等介质上。当程序被操作系统装载到内存并分配给它一定资源后,此时可称为进程。为方便操作系统管理,每个进程都会有一个唯一的非负整数编号。程序是一个静态概念,进程是一个动态概念。

 

    理解进程之前先了解一下什么是用户空间和内核空间,我们知道32位系统最大的寻址空间是4G,在Linux系统中,0~3G为用户空间,3~4G为内核空间,当进程陷入内核时,我们可以认为内核代表进程的运行。

 

    在Linux中有很多方式在表示一个进程在运行中,进程描述符:当进程产生时有Linux操作系统分配。内存:用来存放进程要执行的代码和使用的数据。文件描述符:进程运行时打开的文件。认证信息:用户和组ID。进程执行环境:各种环境变量。资源安排:CPU时间

   Linux的每一个进程都存在一个状态,只要该进程与运行中,就肯定存在一种状态。运行状态:当进程正在被运行或者已经处于可调度状态。
可中断状态:进程正在等待一个信号或者资源。不可中断状态:不可被信号唤醒,一般用于硬件初始化时。暂停状态:当进程收到SIGSTOP、SIGTSTP、SIGTTIN、SIGTTOU信号后就会进入TASK_STOPPED状态,可向其发送SIGCONT信号让进程转换到可运行状态 。僵尸状态:当进程已经运行结束,但其父进程还未查询其状态。见下图所示。

 

 

    进程的布局:所有的C语言代码实现基本都是按照这样的存放方式放置在内存中。栈用来存放局部变量和函数的返回地址。地址从高到低生长。堆是一块连续的内存,有低地址向高地址生长。需要程序在运行时动态申请和释放。数据段存放了程序运行时的各种数据。代码段存放了可执行指令,一般为只读。如下图所示.

 

    二.有关基础函数

    以上所述都是操作系统的基本概念,我想对于学过计算机操作系统的童鞋们一定功底比我扎实,毕竟俺不是计算机科班出身,半路出家的我也只好把这些基本概念理解一下下。下面会多用代码说话,少用文字。以下所有代码都可以编译通过,可以作为学习linux编程的一种范例,仅供参考。

 

    1.进程环境变量:

    环境变量和命令行参数都放在进程的高地址。环境变量可用 environ来引用。以name=string的形式存放。

 

 

    2.程序的启动和终止

    一个进程的开始是以程序的启动为开始,程序的结束为终止。下图可以详细说明一个程序的生命周期。一个程序是其实就是内核和应用层之间的交互,这里他们的接触方式就是系统调用。程序是从main函数开始,到exit函数截止,最后内核会回收进程的所有资源。

   

 

    3.进程的终止

    Linux进程终止函数 exit(status) _exit(status)
    #include <stdlib.h>
    void exit(int status);


    #include <unistd.h>
    void _exit(int status);
   参数status为进程返回状态,可在shell用$?来获取。_exit 为系统调用,此函数直接进入内核终止进程。exit为glibc库函数,它会先运行注册函数,也有可能会进行文件流的关闭操作,之后再调用_exit系统调用。

 

    4.终止处理程序atexit函数

    atexit注册一个进程正常终止时要调用的函数,一个进程最多可注册32个函数。
    #include <stdlib.h>
    int atexit(void (*function)(void));
    成功返回0,失败返回非0。
    参数为函数指针,此函数会在进程调用exit时调用。在main函数返回出,分别用return exit(0) _exit(0),看其中的区别。_exit函数是不会调用注册函数的。

 

    5.关于环境变量

    getenv用来获取指定环境变量。
    #include <stdlib.h>
    char *getenv(const char *name);
    参数name为环境变量的名称,如  PATH,SHELL 等。如果环境变量存在那么返回该环境变量的值,否则为NULL

 

    setenv用来设置环境变量,会把value拷贝到环境变量所在的内存区域。
    #include <stdlib.h>
    int setenv(const char *name, const char *value, int overwrite);
    参数name为环境变量的名称,value为该环境变量的值。如果环境变量已经存在,overwrite非0时,改变该环境变量的值。overwrite为0时,什么都不做直接返回。成功返回0,失败返回-1(一般为放置环境变量的内存空间不够)。环境变量都是以 name=string的形式存放。

 

 

   putenv 无论环境变量是否存在,都会使设置值生效。
   #include <stdlib.h>
   int putenv(char *string);
   参数string的格式为 name=string。成功返回0,失败返回非0
   putenv() 函数并不拷贝环境变量字符串到进程环境表,只是存放环境变量数值的指针,而setenv()函数则完全拷贝环境变量字符串到进程环境表。分别用 gcc putenv.c 和gcc putenv.c –DLOCAL –g编译,产看运行结果。把putenv.c中的putenv换成setenv看结果。

 

    清除环境变量

    clearenv清除所有的环境变量, 并且把environ设置为NULL。unsetenv清除名字为name的环境变量。
    #include <stdlib.h>
    int clearenv(void);
    int unsetenv(const char *name);

 

    6.跨函数跳转

    goto只能在函数中跳转,如果要求从一个函数跳到另外一个函数?用setjmp和longjmp。跨函数的跳转主要用在调用函数层次较深时,为了节约函数的返回时间。

 

    setjmp函数

   setjmp用来设置返回点,保存当前的寄存器值。
   #include <setjmp.h>
   int setjmp(jmp_buf env);
   void longjmp(jmp_buf env, int val);
   参数jmp_buf env用来保存当前寄存器值。longjmp会根据env跳转到setjmp处。一个setjmp可以对应n个longjmp,可以用setjmp的返回值来区分。如果成功setjmp返回0,如果是从longjmp返回的,那么返回值有longjmp的第二个参数决定。所以longjmp的第二个参数不可以为0,否则无法判断setjmp是如何返回的。

 

    longjmp函数

    longjmp的第一个参数是setjmp返回的,第二个参数是给setjmp的返回值。
当longjmp返回后,setjmp所在函数中的自动变量恢复到调用setjmp时的值。如果变量是保存在内存中的,那么它的值仍然是调用longjmp的时候的值。当你希望变量值在setjmp和longjmp时仍然保持其值,但必须用volatile说明该变量,并且需打开-O优化选项。gcc setjmp.c  和 gcc setjmp.c –O观察运行结果。

 

 


    三.多进程编程

    Linux操作系统会为每个进程维护一个进程控制块(俗称PCB,不是印制电路板,而是process control block)。进程控制块保存了进程执行过程中的某个瞬间。操作系统因此可以做进程的切换。见下图。

   

 

    1.进程ID

   getpid得到自己的进程ID,getppid得到自己父进程ID
   #include <sys/types.h>
   #include <unistd.h>
   pid_t getpid(void);
   pid_t getppid(void);
   返回值为进程ID

 

   用户和组ID

   getuid返回实际用户ID, geteuid返回有效用户ID。getgid返回实际组ID,getegid返回有效组ID。
   #include <unistd.h>
   #include <sys/types.h>
   uid_t getuid(void);
   uid_t geteuid(void);
   gid_t getgid(void);
   gid_t getegid(void);

 

    2.进程的创建

    你可以在shell里敲入命令的方式来创建进程,也可以在在程序中通过调用fork系统调用来生成新的进程 。新产生的进程我们称他为子进程。init进程的进程ID为1,是一个特殊的用户进程。它会收集孤儿进程(父进程已退出的子进程),并结束它们。

 

    fork函数(十分著名的函数)

    当你的程序执行到fork语句时:操作系统会复制一个与父进程完全相同的子进程。 新进程和原有进程共享代码空间, 可执行程序是同一个程序。Linux操作系统会为新进程产生一个ID和进程控制块。当子进程或者父进程不进行写操作时,父子进程共享一分数据,当有些操作时,数据会分离(称为写时复制Copy-On-Write),互不干涉。子进程/父进程对数据所做的任何修改,都不会影响另一方。

fork会产生一个新的进程。
    #include <unistd.h>
    pid_t fork(void);
   返回值为-1时,创建子进程失败。返回0时,子进程开始执行。返回值 >0时,父进程开始执行。父子进程都是从fork之后开始执行。到底是父进程还是子进程先开始执行,要看操作系统的调度算法。

父进程或者子进程有些操作时,子进程/或父进程将新产生一份进程的数据拷贝,然后再修改。见下图。

 

 

    子进程的继承:文件描述符,实际用户/组ID,有效用户/组ID,附加组ID,控制终端,根目录,信号屏蔽和安排,存储映射,资源限制。

    vfork函数

    vfork和fork一样都是创建一个子进程,但子进程不会从父进程复制任何东西。子进程完全和父进程共享数据和堆栈,子进程对数据的修改不会触发写时复制,也就是说子进程所作的修改会出现在父进程中。
   #include <sys/types.h>
   #include <unistd.h>
   pid_t vfork(void);
   返回值为-1时,创建子进程失败。返回0时,子进程开始执行。返回 >0时,父进程开始执行。

 

    vfork和fork的区别

    vfork产生的子进程必须以exit或者exec返回,否则会出现未定义错误。vfork一定保证子进程先运行,在子进程调用exit或者exec之前父进程是不可能运行的。vfork的使用场合是子进程不会用到父进程任何资源的情况下。vfork只产生一个进程控制块,然后再通过exec产生子进程所需要的资源和代码。当vfork的子进程用return返回时,看有什么结果?

 


    3.父子进程的终止

    父子进程谁先终止时未定义的,如果父进程先于子进程终止。那么子进程会被init进程“领养”,子进程的父进程id变为0。如果父进程不去查询子进程的状态,那么子进程一直会处于“僵尸”状态。这也是init进程存在的原因之一。父进程可以通过wait和waitpid函数来显示的查询子进程。

    wait和waitpid可查询子进程的结束状态,如果子进程还处于运行状态,那么父进程则会阻塞。
    #include <sys/types.h>
    #include <sys/wait.h>
    pid_t wait(int *status);
    pid_t waitpid(pid_t pid, int *status, int options);
    wait函数只要任意一个子进程结束,就会返回该子进程ID,如果出错则返回-1。status为子进程返回的状态,需要通过宏WIFEXITED和WEXITSTATUS最终获得返回状态。

 

    waitpid可以指定某个进程id或者进程组id
    参数解析
   pid==-1 等待任意一个进程结束,此时与wait等效。
   pid>0  等待与pid相符的子进程结束。
   pid==0 等待组ID等于调用者进程组ID的任意进程结束。
   pid<-1 等待组ID等于|pid|的任意进程结束。

 

    4.exec函数

    fork进程后,往往通过exec函数执行另一个程序。此时该子进程完全被替换为新程序。exec用一个全新的程序替换当前进程的正文,数据,堆和栈。exec有6中变体可以使用,俗称为exec函数。
    int execl(const char *path, const char *arg, ...);
    int execlp(const char *file, const char *arg, ...);      
    int execle(const char *path, const char *arg,   ..., char * const envp[]);
    int execve(const char *path, char *const argv[], char * const envp[]);
    int execv(const char *path, char *const argv[]);
    int execvp(const char *file, char *const argv[]);

    下图说明exec函数关系

 

    execl和execv,这两个函数的区别在于程序的命令行参数如何传递。l代表list,意味着execl的每个命令行参数都是单独传入。v代表vector,所有命令行参数打包成 char *argvp[]的方式传给execv。其实execl所做的是把单独的命令行参数打包后传给execv。execlp和execvp,p代表path,也就是说你只要在第一个参数中指明可执行文件的名字,系统便会从PATH指定的路径中寻找那个可执行文件并执行。不设置environ,观察程序运行区别execle和execve,函数的最后一个参数为环境变量。

 

 

 

    5.system函数

    system执行外部一个命令或者程序,相当于fork, exec,waitpid。
    #include <stdlib.h>
    int system(const char *command);
   返回-1表示失败,成功返回命令的返回状态。如果command中没有路径符/,则从PATH环境变量指定的目录中寻找该命令。

 

   最后是一个思考题,就是下面代码中一共创建了多少个进程

  

 

    练习题:使用fork,exec,waitpid函数实现一个简单的system函数。

                    编写一段程序,创建一个“僵尸”进程,然后调用system执行ps命令验证改进程是“僵尸”进程。验证完后父进程用wait或者waitpid对  该“僵尸”进程“收尸”。

 

参考文献:unix环境高级编程

原创粉丝点击