进程控制
在Linux系统中,用户创建一个进程的唯一方法就是使用系统调用fork()。内核为完成系统调用fork()要进行几步操作。
第一步:为新进程在进程表中分配一个表项task_struct。系统对一个用户可以同时运行的进程数是有限制的,对超级 用户没有该限制,但也不能超过进程表的最大表项的数目。
第二步:给子进程一个唯一的进程标识号(PID)。该进程标识号其实就是该表项在进程表中的索引号。
第三步:复制一个父进程的进程表项的副本给子进程。内核初始化子进程的进程表项时,是从父进程处拷贝的。所以子进程拥有父进程一样的uid、euid、gid,用于计算优先权的nice值、当前目录、当前根、用户文件描述符表等。
第四步:把与父进程相连的文件表和索引节点表的引用数加1,这些文件自动地与该子进程相连。
进程调度
CPU资源是有限的,那么在调度进程时,每个进程只允许运行很短的时间,当这个时间用完之后,系统将选择另一个进程来运行,原来的进程必须等待一段时间以继续运行,这段时间称为时间片。
Linux使用基于优先级的简单调度算法来选择下一个运行进程。当选定新进程后,系统必须将当前进程的状态,处理器中的寄存器以及上下文状态保存到task_struct结构中。同时它将重新设置新进程的状态并将系统控制权交给此进程。为了将CPU时间合理的分配给系统中每个可执行进程,调度管理器必须将这些时间信息也保存在task_struct
中。
fork函数创建进程
一个现有进程可以调用fork函数创建一个新进程。包括代码、数据和分配给进程的资源全部都复制一份,除了进程标识pid不同,其他基本都跟父进程一样,由fork创建的新进程被称为子进程(child process)。fork函数被调用一次但返回两次。
fork函数原型:
1)在父进程中,fork返回新创建子进程的进程ID; 2)在子进程中,fork返回0; 3)创建子进程失败,fork返回-1;
源代码: fork_test.c
#include
#include
int main(int argc, char** argv)
{
pid_t pid;
printf("Current process PID = [%d]\n", getpid());
pid = fork();
if(pid == 0)
{
printf("child process PID = [%d], parent PID = [%d]\n", getpid(), getppid());
}
else if(pid != -1)
{
printf("parent process, PID = [%d]\n", getpid());
}
else
{
perror("fork");
}
return 0;
}
程序非常简单,调用了fork()函数并把返回值赋值给pid变量。分别在pid等于0、pid不等于-1的时候输出当前进程的PID。
编译运行这个程序,结果如下:
可见,当pid等于0的时候,表明是当前进程派生出来的子进程。如果返回的pid不等于-1,表明派生操作成功并且返回值就是新的子进程PID。如果返回值等于-1,那么操作失败。
在代码pid=fork()之前,只有一个进程在执行这段代码,但在这条语句之后,就变成两个进程在执行了,这两个进程的代码部分完全相同。
源码:fork_test2.c
#include
#include
int main(int argc, char** argv)
{
pid_t pid;
int i;
printf("Current process PID = [%d]\n", getpid());
pid = fork();
if(pid == 0)
{
for(i = 0; i < 10; i++)
{
printf("child process, PID=[%d], my parent PID=[%d]\n", getpid(), getppid());
sleep(1);
}
}
else if(pid != -1)
{
for(i = 0; i < 10; i++)
{
printf("parent process, PID=[%d], child PID=[%d]\n", getppid(), pid);
sleep(1);
}
}
else
{
printf("fork error\n");
}
return 0;
}
我们的父进程和子进程函数中都加入了一个for循环,循环输出10次,并在每次输出之后睡眠1秒。下面是程序的执行结果:
从输出的结果,可以看到,子进程的输出和父进程的输出是交替输出的,而不是由某个一个进程执行完10次循环之后,才能执行另外一个进程,当进程进入睡眠模式的时候,内核会调度新的进程进入运行模式,这样使得两个进程看上去像是同步执行。
调用fork的时候直接从父进程 复制一份PCB到子进程,除了PID不同以外,其他的都一样,包括文件描述符表中的文件指针也指向同一个地址。就相当于是调用了==dup==复制了文件描述符。于是父子进程其实是共享同一个文件读写偏移量。
测试例子如下:
#include
#include
#include
#include
int main()
{
int fd,nr;
char buf[20];
pid_t pid;
fd = open("log",O_RDONLY);
pid = fork();
if(pid == 0)
{
nr = read(fd,buf,10);
buf[nr]='\0';
printf("pid %d buf %s\n",getpid(),buf);
exit(0);
}
nr = read(fd,buf,10);
buf[nr]='\0';
printf("pid %d buf %s\n",getpid(),buf);
return 0;
}
在log文件中写入helloworld1234567890,运行程序后输出:
足够说明fork的时候,复制出来的文件描述符是共享文件偏移量的。
获得进程有关的ID
用户标识号UID(user ID):用于标识正在运行进程的用户。
用户组标识号GID(group ID): 用于标识正在运行进程的用户所属的组ID。
进程标识号PID(process ID):用于标识进程。
#include
#include
int main(int argc, char** argv)
{
printf("Current process's UID = [%d]\n", getuid());
printf("Current process's GID = [%d]\n", getgid());
printf("Current process's PID = [%d]\n", getpid());
printf("Current process's PPID = [%d]\n", getppid());
return 0;
}
注意:
1、实际用户ID和实际用户组ID:标识我是谁。也就是登录用户的uid和gid,比如我的Linux以where登录,在Linux运行的所有的命令的实际用户ID都是where的uid,实际用户组ID都是where的gid(可以用id命令查看)。
2、有效用户ID和有效用户组ID:进程用来决定我们对资源的访问权限。一般情况下,有效用户ID等于实际用户ID,有效用户组ID等于实际用户组ID。当设置SUID位设置,则有效用户ID等于文件的所有者的uid,而不是实际用户ID;同样,如果设置了SGID位,则有效用户组ID等于文件所有者的gid,而不是实际用户组ID。