2019独角兽企业重金招聘Python工程师标准>>>
一、创建一个进程
进程是系统中最基本的执行单位。Linux系统允许任何一个用户进程创建一个子进程,创建之后,子进程存在于系统之中并独立于父进程。
关于父进程与子进程这两个概念,除了0号进程以外(由系统创建),Linux系统中任何一个进程都是由其它进程创建的。创建新进程的进程,即调用fork()函数的进程就是父进程。
Linux中使用fork()函数创建一个新进程,函数原型如下:
#include <unistd.h>
pid_t fork(void);
fork()函数不需要参数,返回值是一个进程ID。对于返回值,有以下3中情况:
- 对于父进程,fork()函数返回新创建的子进程的ID。
- 对于子进程,fork()函数返回0.由于系统的0号进程是内核进程,所以子进程的进程号不可能是0,由此可区分父进程和子进程。
- 如果出错,fork()函数返回-1.
fork()函数会创建一个新的进程,并从内核中为此进程得到一个新的可用进程ID。之后为这个新进程分配进程空间并将父进程的进程空间中的内容复制到子进程空间中,包括父进程的数据段和堆栈段,并且和父进程共享代码段。
由于复制了父进程的堆栈段,所以两个进程都停留在fork()函数中,等待返回。因此,fork()函数会返回两次,一次是在父进程中返回,另一次是在子进程中返回,两次的返回值是不一样的。
下面示例创建一个子进程:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>int main(int argc, char *argv[])
{pid_t pid;pid = fork();if(pid < 0) { //fork出错printf("fail to fork.\n");exit(1);} else if(pid == 0) { //子进程printf("this is child, pid is : %u\n", getpid());} else {printf("this is parent, pid is : %u, child-pid is %u\n", getpid(), pid);}return 0;
}
运行结果:
二、父子进程的共享资源
子进程完全复制了父进程的地址空间的内容,包括堆栈段和数据段的内容。子进程并没有复制代码段,而是和父进程共用代码段。
下面的实例定义了一个全局百变量、一个局部变量和一个指针。之后该程序创建一个子进程,在子进程中修改上面定义的值,并打印出来。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>int global; //全局变量,在数据段中int main(int argc, char *argv[])
{pid_t pid;int stack = 1; //局部变量,在栈中int *heap;heap = (int *)malloc(sizeof(int)); //动态分配的内存,在堆中*heap = 2;pid = fork();if(pid < 0) {printf("fail to fork\n");exit(1);} else if(pid == 0) { //子进程global++;stack++;(*heap)++;printf("the child, data: %d, stack: %d, heap: %d\n", global, stack, *heap);exit(0); //子进程结束}sleep(2); //父进程休眠2秒printf("the parent, data: %d, stack: %d, heap: %d\n", global, stack, *heap);return 0;
}
运行结果:
三、创建一个共享空间的子程序
进程在创建一个新的子进程之后,子进程的地址空间完全和父进程分开,父子进程是两个完全独立的进程,接受系统调度和分配系统资源的机会均等。如果父子进程共用父进程的地址空间,则子进程就不是独立于父进程的。Linux下提供了一个和fork()函数类似的函数,可以用来创建一个共用父进程地址空间的子进程,函数原型如下:
#include <unistd.h>
pid_t vfork();
vfork()和fork()函数的区别如下:
- vfork()函数产生的子进程和父进程完全共享地址空间,包括代码段、数据段和堆栈段,子进程对这些资源所做的修改可以影响父进程。vfork()函数产生的进程更像一个线程。
- vfork()函数产生的子进程一定比父进程先运行,即父进程调用vfork函数后,会等待子进程运行后再运行。
使用vfork()函数还应注意不要在非main函数的函数中调用vfork()函数。
四、退出进程
当一个进程需要退出时,需要调用退出函数:
#include <stdlib.h>
void exit(int status);
exit()函数的参数表示退出的状态,这个状态的值是一个整型。在shell中可以检查这个退出的状态值。
C程序中的return语句会被翻译为调用exit()函数:
return 1;
翻译为:
exit(1);
exit()函数与内核函数的关系
exit()函数是一个标准的库函数,其内部封装了Linux系统调用的_exit()函数。两者的主要区别在于exit()函数会在用户空间做一些善后工作,例如清理用户的I/O缓冲区将其内容写入磁盘文件等,之后再进入内核释放用户进程的地址空间;而_exit()函数直接进入内核释放用户进程的地址空间,所以用户空间的缓冲区内容都将丢失。
五、设置进程所有者
每一个进程都有两个用户ID,实际用户ID和有效用户ID。通常这两个ID的值是相等的,其值为进程所有者的用户ID。但是有些场合需要改变进程的有效用户ID。
Linux下使用setuid()函数改变一个进程的实际用户ID和有效用户ID,其函数原型如下:
#include <unistd.h>
int setuid(uid_t uid);
setuid()函数的参数表示改变后的新用户ID,如果成功修改当前进程的实际用户ID和有效用户ID,setuid()函数返回0,失败返回-1。
两种用户可以修改进程的实际用户ID和有效用户ID:
- 根用户。根用户可以将进程的实际用户ID和有效用户ID更改。
- 其他用户,且该用户的用户ID等于进程的实际用户ID或者保存的ID。
下面的程序演示修改当前进程的用户ID:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>int main(int argc, char *argv[])
{FILE *fp;uid_t uid, euid;uid = getuid(); //实际用户IDeuid = geteuid(); //有效用户IDprintf("the uid is : %d\n", uid);printf("the euid is : %d\n", euid);if(setuid(8000) == -1){perror("fail to set uid");exit(1);}printf("after changing\n");uid = getuid(); //实际用户IDeuid = geteuid(); //有效用户IDprintf("the uid is : %d\n", uid);printf("the euid is : %d\n", euid);return 0;
}
运行结果:
Linux下还提供只修改有效用户ID的函数:
int seteuid(uid_t uid);
以及修改实际组ID和有效组ID的函数:
#include <unistd.h>
int setgid(gid_t gid);
int setegid(gid_t gid);