1,进程:是容器,是内存上的概念。线程是CPU的概念。
2,fork的作用是根据一个现有的进程复制出一个新进程,原来的进程称为父进程(Parents Process),新进程称为子进程(Child Process)。
系统中同时运行着许多进程,这些进程都是从最初只有一个进程开始一个一个复制出来的。
分进程函数fork(),用来开辟子进程。执行fork()之后,会有两个进程,一个是原来的那个,一个是新生成的子进程。这两个进程都会执行程序下面的代码。pid = fork();pid得到的是返回子进程的id,不是自己的id。
1.c
#include
#include
#include
#include
int main(void)
{
pid_t pid = fork();
if(pid < 0){
perror("fork");
exit(0);
}
if(pid > 0){
printf("Parents!n");
}
if(0 == pid){
printf("Child!n");
}
printf("Hello world!n");
return 0;
}
3,先让父进程睡1秒,会先执行子进程。
2.c
#include
#include
#include
#include
int main(void)
{
pid_t pid = fork();
if(pid < 0){
perror("fork");
exit(0);
}
if(0 == pid){
printf("Child!n");
}else{
sleep(1);
printf("Parents!n");
}
return 0;
}
如果父进程结束后,sleep(1);子进程会打印到终端。
3.c
#include
#include
#include
#include
int main(void)
{
pid_t pid = fork();
if(pid < 0){
perror("fork");
exit(0);
}
if(0 == pid){
sleep(1);
printf("Son!n");
}else{
printf("Father!n");
}
return 0;
}
4,进程号:pid 。getppid()是得到当前进程父进程的pid;getpid()是得到当前进程的pid。
如果子进程getppid()时,父进程已经死掉,子进程就会被托管,那getppid()的值就为1。
所以父进程要sleep(1),这样子进程getppid()时,得到的就是此进程父进程的pid。
父进程getppid()时,得到的是终端的pid。
4.c
#include
#include
#include
#include
int main(void)
{
pid_t pid = fork();
if(pid < 0){
perror("fork");
exit(0);
}
if(0 == pid){
printf("Child!ngetppid=%d getpid=%d pid=%dn", getppid(),
getpid(), pid);
}else{
printf("Parents!ngetppid=%d getpid=%d pid=%dn", getppid(),
getpid(), pid);
sleep(1);
}
return 0;
}
5,ps查看进程;
ps -jas 查看所有的进程。
6,探讨怎么让父进程知道子进程结束?
exit(0)不会结束整个程序,只会结束一个进程。wait(NULL)等待自己子进程的结束。
父进程执行到wait()时,会进入休眠,子进程结束时,会发一个信号,wait()会捕捉到这个信号,以此来唤醒父进程。
5.c
#include
#include
#include
#include
int main(void)
{
pid_t pid = fork();
if(pid < 0){
perror("fork");
exit(0);
}
if(0 == pid){
sleep(3);
printf("Child!n");
exit(0);
}
wait(NULL);
printf("Parents!n");
return 0;
}
7,探讨父进程与子进程关于变量使用的问题?
在fork()之后,会把原来的内存完全复制一份,但不管怎么样更改,都只会各改各的,互不干扰。
6.c
#include
#include
#include
#include
int main(void)
{
int a = 5;
int * p = &a;
pid_t pid = fork();
if(pid < 0){
perror("fork");
exit(0);
}
if(pid > 0){
*p = 6;
printf("Parents:%dn", *p);
}else{
printf("Child:%dn", *p);
}
return 0;
}
8,怎么样杀死进程?
下面的程序是个死循环,只能重新开一个终端,执行“kill -9 进程号”。
如果执行以下命令,一定要删除交换文件:
$vi 5.c //进入5.c 此进程进程号为3456
$Ctrl + z
//暂停当前程序运行
$kill -9 3456 //杀死vi 5.c,这个进程
$fg
//回到刚暂停的程序中,会报错,因为那个进程已经被杀死了。要删除那个交换文件才能执行这个操作。
$ls -a//查看所有文件,能把交换文件查看出来。
$rm -fr ..5.c.swap. //删除交换文件。
$fg
7.c
#include
#include
#include
#include
int main(void)
{
pid_t pid = fork();
if(pid < 0){
perror("fork");
exit(0);
}
if(0 == pid){
while(1){
printf("Child!n");
}
}else{
printf("Parents!n");
}
return 0;
}
9,exec族:用来代替原来进程,执行的时候完全代替,不会再回来。
#include
所需头文件
(1)execl("/bin/ls", "ls", "-l",
NULL); 后面一定要加NULL,表示参数传完了。
int
execl(const char *path, const char *arg0, ... )
8.c
#include
#include
#include
#include
int main(void)
{
int ret = execl("/bin/ls", "ls", "-l", NULL);
if(ret < 0){
perror("execl!n");
exit(0);
}
printf("Hello world!n");
return 0;
}
(2)
execlp已经安装过的程序,不需要写路径。
int
execlp(const char *file, const char *arg0, ... );
9.c
#include
#include
#include
#include
int main(void)
{
int ret = execlp("ls", "ls", "-l", NULL);
if(ret < 0){
perror("execl!n");
exit(0);
}
printf("Hello world!n");
return 0;
}
(3)int execv(const char *path, char
*const argv[]);
int
execvp(const char *file, char *const argv[]);
10.c
#include
#include
#include
#include
int main(void)
{
char * buf[5] = {};
buf[0] = "ls";
buf[1] = "-l";
buf[2] = NULL;
int ret = execv("/bin/ls", buf);
// int ret =
execvp("ls", buf);
if(ret < 0){
perror("execv");
exit(0);
}
return 0;
}
10,whereis ls 查看ls的路径。
11,-rwx-rwx-rwx:普通文件
drwx-rwx-rwx:d代表是一个文件夹
lrwx-rwx-rwx:l代表是一个快捷方式
12,cd是shell的内置命令。如果输入等于cd,则用chdir(),它并没有fork()子进程,是父进程在执行chdir()。
11.c
#include
#include
#include
int main(void)
{
chdir("dic");
//这个目录要事先创建好。
mkdir("./ttt");
return 0;
}
如果用execlp(),会达不到想要的结果。
12.c
#include
#include
#include
#include
int main(void)
{
int ret = execlp("cd", "cd", "/", NULL);
if(ret < 0){
perror("execlp");
exit(0);
}
return 0;
}
13,一定要记住(固定格式):切割字符串函数strtok()
13.c
#include
#include
#include
#include
#define BUF 512
#define BUFF 10
int main(void)
{
char buf[BUF] = {};
ssize_t ret = read(0, buf, BUF);
int i = 0;
char * buff[BUFF] = {};
char * p = buf;
while(1){
p = strtok(p, " n");
if(!p)
break;
buff[i++] = p;
p = NULL;
}
for(i = 0; i < BUFF; i++){
if(NULL == buff[i])
break;
printf("%sn", buff[i]);
}
execvp(buff[0], buff);
return 0;
}
14,编写一个自己的终端
14.c
#include
#include
#include
#include
#define BUF 512
#define BUFF 10
int main(void)
{
char buf[BUF];
char * buff[BUFF];
char * p = buf;
int i = 0;
pid_t pid;
int ret = 0;
while(1){
write(1, "myshell$", 8);
memset(buf, 0,
BUF);
read(0, buf, BUF);
while(1){
p = strtok(p, "n ");
if(!p){
break;
}
buff[i++] = p;
p = NULL;
}
buff[i] = NULL;
if(strcmp(buff[0], "cd") == 0){
chdir(buff[1]);
continue;
}
pid = fork();
if(pid < 0){
perror("fork");
exit(0);
}else if(pid == 0){
ret = execvp(buff[0], buff);
if(ret < 0){
perror("execvp");
exit(0);
}
}else{
wait(NULL);
}
}
return 0;
}
15,仔细探讨fork()的用法。
15.c
#include
#include
#include
#include
int main(void)
{
pid_t pid;
char * message;
int n = 0;
pid = fork();
if(pid < 0){
perror("fork");
exit(0);
}
if(0 == pid){
message = "This is the childn";
n = 6;
}else{
message = "This is the parentn";
n = 3;
}
for(;n > 0; n--){
printf("%s", message);
sleep(1);
}
return 0;
}
程序运行顺序如下:
(1)父进程初始化。
(2)父进程调用fork,这是一个系统调用,因此进入内核。
(3)内核根据父进程复制出一个子进程,父进程和子进程的PCB信息相同,用户态代码和数据也相同。因此,子进程现在的状态和父进程一样,做完了初始化,刚调用了fork进入内核,还没有从内核返回。
(4)现在有两个一模一样的进程看起来都调用了fork进入内核等待从内核返回(实际上fork只调用了一次),系统中还有很多别的进程也等待从内核返回。是父进程先返回还是子进程先返回,还是这两个进程都等待,先去调度执行别的进程,这都不一定,取决于内核的调度算法。
(5)如果某个时刻父进程被调度执行了,从内核返回后就从fork函数返回,保存在变量pid中的返回值是子进程的id,是一个大于0的整数,因此执行下面else分支,然后执行for循环,打印"This is parentn"三次之后终止。
(6)如果某个时刻子进程被调度执行了,从内核返回后就从fork函数返回,保存在变量pid中的返回值是0,因此执行下面的if(0 == pid)分支,然后执行for循环,打印"This is childn"六次这样之后终止。fork调用把父进程的数据复制一份给子进程,但此后二者互不影响,在这个例子中,fork调用之后父进程和子进程的变量message和n被赋予不同的值,互不影响。
(7)父进程每打印一条信息就睡眠1秒,这时内核调度别的进程执行,在这1秒这么长的间隙里(对于计算机来说1秒很长了)子进程很有可能被调度到。同样地,子进程每打印一条信息就睡眠1秒,在这1秒期间父进程也很有可能被调度到。所以程序运行地结果基本上是父子进程交替打印,但这也不一定的,取决于系统中其他进程的运行情况和内核的调动算法,如果系统中其他进程非常繁忙则有可能观察到不同的结果。
(8)这个程序是在Shell下运行的,因此Shell进程是父进程的父进程。父进程运行时Shell进程处于等待状态,当父进程终止时Shell进程认为命令执行结束了,于是打印Shell提示符,而事实上子进程这时还没结束,所以子进程的消息打印到了Shell提示符后面。最后光标停在This is the child的下一行,这时用户仍然可以敲命令,即使命令不是紧跟在提示符后面,Shell也能正确读取。
fork函数:
(1)fork函数的特点"调用一次,返回两次",在父进程中调用一次,在父进程和子进程中各返回一次。一开始是一个控制流程,调用fork之后发生了分叉,变成两个控制流程,这也就是"fork"(分叉)这个名字的由来了。子进程中fork的返回值是0,而父进程中fork的返回值则是子进程的id(从根本上说fork是从内核返回的,内核自有办法让父进程和子进程返回不同的值),这样当fork函数返回后,程序员可以根据返回值的不同让父进程和子进程执行不同的代码。
(2)fork的返回值这样规定也是有道理的。fork在子进程中返回0,子进程仍可以调用getpid函数得到自己的进程id,也可以调用getppid函数得到父进程的id。在父进程中用getpid可以得到自己的进程id,然而要想得到子进程的id,只有将fork的返回值记录下来,别无他法。
(3)fork的另一个特性是所有由父进程打开的描述符都被复制到子进程中。父、子进程中相同编号的文件描述符在内核中指向同一个file结构体,也就是说file结构体的引用计数要增加。
16,exec函数
用fork创建子进程后执行的是和父进程相同的程序(但有可能执行不同的代码分支),子进程往往要调用一种exec函数以执行另一个程序。当进程调用一种exec函数时,该进程的用户空间代码和数据完全被新程序替换,从新程序的启动例程开始执行。调用exec并不创建新进程,所以调用exec前后该进程的id并未改变。
exec族
man
3 exec
头文件:#include
int execl(const char *path, const char *arg0, ... );
int execlp(const char *file, const char *arg0, ... );
int execle(const char *path, const char *arg0, ...);
int execv(const char *path, char *const argv[]);
int execvp(const char *file, char *const argv[]);
int execvP(const char *file, const char *search_path, char *const
argv[]);
man 2 execve
int execve(const char *path, char *const argv[], char *const
envp[]);
这些函数如果调用成功则加载新的程序从启动代码开始执行,不再返回,如果调用出错则返回-1,所以exec函数只有出错的返回值而没有成功的返回值。
不带p(表示path)的exec函数第一个参数必须是程序的相对路径或绝对路径,例如"/bin/ls"或"./a.out",而不能是"ls"或"a.out"。对于带字母p的函数:如果参数中包含/,则将其视为路径名。否则视为不带路径的程序名,在PATH环境变量的目录列表中搜索这个程序。
带由字母l(表示list)的exec函数要求将新程序的每个命令行参数都作为一个参数传给它,命令行参数的个数是可变的,因此函数原型中有…,…中的最有一个可变参数应该是NULL,起sentinel的作用。
对于带有字母v(表示vector)的函数,则应该先构造一个指向各参数的指针数组,然后将该数组的首地址当作参数传给它,数组中的最后一个指针也应该是NULL,就像main函数的argv参数或者环境变量一样。
对于以e(envirnoment)结尾的exec函数,可以把一份新的环境变量传给它,其他exec函数仍使用当前的环境变量表执行新程序。
事实上,只有execve是真正的系统调用,其他五个函数最终都调用execve,所以execve在man手册第2节,其他函数在man手册第3节。