四、进程基本知识
已经进入多进程阶段
1、进程标识符pid
类型pid_t(传统意义上是16位有符号的整形数)
虚拟机计数pid号不够用
命令ps
ps axf
ps axm
ps ax -L 以linux特有的方式查看
ps -ef 可以查看系统中的所有进程
进程号是顺次向下使用
getpid获得当前进程的pid号:pid_t getpid(void);
getppid获得当前进程父进程号:pid_t getppid(void);
2、进程的产生
fork();
子进程会把父进程的页表复制一份,因此子进程继承了父进程的代码和数据(当然也包括打开的文件、标准 io 的缓存区等),
但是并不复制 file 结构体(文件表),因此父子进程会使用同一个文件移,fork 采用写时拷贝技术,
无论父子进程谁对哪一页进行写操作都会把那一页拷贝一份,未处理的闹钟和未决信号子进程不继承。
注意理解关键字:duplicating,意味着拷贝,克隆,一模一样等含义。
fork后父进程的区别:
fork的返回值不一样,pid不一样,ppid也不同
未决信号和文件锁不继承,资源利用量清零。
init进程:1号,是所有进程的祖先进程。
fork之前要刷新所有的缓冲区的流用fflush。
输出到终端是行缓冲,输出到文件是全缓冲。
1>创建一个子进程:pid_t fork(void);
返回值:子进程返回0,父进程返回子进程ID;出错,返回-1.
(调度器的调度策略来决定那个那个进程先运行)
[fork实例1]
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>
int main()
{
pid_t pid;
printf("[%d]:Begin!\n",getpid());//父进程ID
fflush(NULL); /*!!!*/
pid = fork();
if(pid < 0)
{
perror("fork()");//产生子进程
exit(1);
}
if(pid == 0) // child
{
printf("[%d]:Child is working!\n",getpid());
}
else // parent
{
printf("[%d]:Parent is working!\n",getpid());
}
printf("[%d]:End!\n",getpid());
sleep(10000);
exit(0);
}
孤儿进程:父进程先于子进程结束,子进程就变成孤儿进程,不过孤儿进程马上就会成为 init进程的子进程。
僵尸进程:如果子进程结束了而父进程没结束且父进程没有替子进程收尸,那么子进程就会变成僵尸进程,但如果父进程结束了,相应的僵尸进程也就没有了。
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#define LEFT 30000000
#define RIGHT 30000200
int main()
{
int i,j,mark;
pid_t pid;
for(i = LEFT; i <= RIGHT; i++)
{
pid = fork();
if(pid < 0)
{
perror("fork()");
exit(1);
}
if(pid == 0) // child
{
mark = 1;
for(j = 2 ; j <= i/2; j++)
{
if(i % j == 0)
{
mark = 0;
break;
}
}
if(mark)
printf("%d is a primer.\n",i);
//sleep(10000);//让父进程先结束,出现孤儿进程
exit(0);//结束做完运算的子进程
}
}
//sleep(10000);//让子进程先结束,出现僵尸进程
exit(0);
}
pid_t vfork(void);
vfork 函数创建的子进程和父进程在同一个地址空间运行,并且会保证子进程先执行(子进程执行完成后父进程才执行),一般用在子进程立即调用 exec 的场合。
注意:如果在子进程中修改了数据、调用了函数或者没有调用 exit 或者 exec 函数就返回可能会有不可预知的结果出现。
注意:某些平台已经把 vfork 弃用,因此该函数可能导致程序不可移植。
3、[进程的消亡及释放资源]
等待子进程状态的改变(收尸)
进程状态的改变可能是进程的终止(异常或者正常)、进程停止、进程恢复执行.
等待子进程结束可以获得子进程的终止状态,并且和以防止子进程变成僵尸进程.
1>pid_t wait(int *status);
返回值:成功,返回进程ID;失败,返回0或-1.
1.如果没有子进程在运行,则出错返回
2.如果有子进程已经终止,则立即返回
3.如果所有子进程都在运行,则阻塞等待
获取进程状态的方法:
WIFEXITED 判断进程是否正常终止
WEXITSTATUS 如果进程正常终止,可用这个方法获得进程的返回值
WIFSIGNALED 判断进程是否被信号杀死
WTERMSIG 如果进程被信号杀死,可用这个方法获得杀死进程的信号
WCOREDUMP 如果进程被信号杀死,可用这个方法获得进程是否生成了 core 文件
WIFSTOPPED 判断进程是否被信号停止
WSTOPSIG 如果进程被信号停止,可以这个方法获得停止进程的信号
WIFCONTINUED 判断进程是否被信号唤醒继续执行
(wait等待收尸)
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#define LEFT 30000000
#define RIGHT 30000200
int main()
{
int i,j,mark;
pid_t pid;
for(i = LEFT; i <= RIGHT; i++)
{
pid = fork();
if(pid < 0)
{
perror("fork()");
exit(1);
}
if(pid == 0) // child
{
mark = 1;
for(j = 2 ; j <= i/2; j++)
{
if(i % j == 0)
{
mark = 0;
break;
}
}
if(mark)
printf("%d is a primer.\n",i);
exit(0);
}
}
//父进程创建fork201个子进程,子进程跑,
//父进程等待201次到那201个子进程exit(0)回来,父进程释放子进程。
int st;
for(i = LEFT; i <= RIGHT; i++)
wait(NULL);//收尸 不关心状态
// wait(&st);//关心状态
exit(0);
}
2>pid_t waitpid(pid_t pid, int *status, int options);
返回值:成功,返回进程ID;失败,返回0或-1.
1.pid==-1 等待任何一个进程,等价于 wait
2.pid==0 等待和调用进程同组的进程(进程组 ID 相等)
3.pid>0 等待进程 ID 等于 pid 的进程
4.pid<0 等待进程 ID 等于|pid|的进程
options 参数:(位图)
WNOHANG 不阻塞
WUNTRACED 如果等待的进程在停止后恢复了执行,则返回其状态
WCONTINUED 如果等待的进程处于停止状态并且没有报告过,则返回其状态
3>int waitid(idtype_t idtype, id_t id, siginfo_t *infop, int options);
返回值:成功返回0;失败返回-1.
1.type == P_PID 等待进程 ID 为 id 的进程
2.type == P_PGID 等待和调用进程同一组的进程
3.type == P_ALL 等待所有进程,忽略 id
options 选项值:
WEXITED 等待终止的子进程
WSTOPPED 等待被信号停止的子进程
WCONTINUED 等待被信号恢复执行的子进程
WNOHANG 不阻塞
WNOWAIT 把进程放在一个可以 wait 的状态,下一次 wait 还可以获得该进程的状态
如果子进程需要做比较耗时的任务,但是父进程又不想等待子进程结束而且不能产生僵尸进程,
方法是 fork 两次,爷进程用 wait 等待,父进程调 exit 退出,孙进程做比较耗时的任务,
这样爷进程的 wait 很快就返回,这样孙进程就成为了 init 进程的子进程
竞争条件:多个进程同时操作共享资源时会有竞争,有竞争就可能存在错误.
N个进程筛质数
1、分块
2、交叉分配
3、池
(创建3个子进程运用交叉分配实例)
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#define LEFT 30000000
#define RIGHT 30000200
#define N 3
int main()
{
int i,j,mark,n;
pid_t pid;
for(n = 0 ; n < N; n++)
{
pid = fork();
if(pid < 0)
{
perror("fork()");
exit(1);
}
if(pid == 0)
{
for(i = LEFT+n; i <= RIGHT; i+=N)
{
mark = 1;
for(j = 2 ; j <= i/2; j++)
{
if(i % j == 0)
{
mark = 0;
break;
}
}
if(mark)
printf("[%d]%d is a primer.\n",n,i);
}
exit(0);
}
}
for(n = 0 ; n < N; n++)
wait(NULL);
exit(0);
}
/*
[2]30000023 is a primer.
[1]30000001 is a primer.
[2]30000041 is a primer.
[1]30000037 is a primer.
[1]30000049 is a primer.
[2]30000059 is a primer.
[1]30000079 is a primer.
[2]30000071 is a primer.
[1]30000109 is a primer.
[2]30000083 is a primer.
[1]30000133 is a primer.
[2]30000137 is a primer.
[1]30000163 is a primer.
[2]30000149 is a primer.
[1]30000169 is a primer.
[2]30000167 is a primer.
[1]30000193 is a primer.
[1]30000199 is a primer.
*/
4、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 execv(const char *path, char *const argv[]);
int execvp(const char *file, char *const argv[]);
int execvpe(const char *file, char *const argv[], char *const envp[]);
返回值:失败返回-1;成功不返回。
注意:file 可以传递路径名也可是文件名,如果是文件名则按照 PATH 环境变量去寻找命令新程序的地址空间会覆盖旧程序的地址空间,
新程序会继承 PID,PPID,UID,EUID(如果是一个非设置用户 ID 程序)等。
(exacl实现data+%s相当于大时戳)
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
// date +%s
int main()
{
puts("Begin!");
fflush(NULL); /*!!!使用execl之前应刷新所有的缓冲流*/
//新进程映像替换旧得旧进程映像
execl("/bin/date","date","+%s",NULL);
perror("execl()");
exit(1);
puts("End!");
exit(0);
}
/*
Begin!
1505125257(时戳)
*/
自己替换成别人,不如一开始就是别人,因为pid一直未改变。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#uinclude <sys/wait.h>
// date +%s
int main()
{
pid_t pid;
puts("Begin!");
fflush(NULL); /*!!!*/
pid = fork();
if(pid < 0)
{
perror("fork()");
exit(1);
}
if(pid == 0)
{
execl("/bin/date","date","+%s",NULL);
perror("execl()");
exit(1);
}
wait(NULL);
puts("End!");
exit(0);
}
/*
Begin!
1505125821
End!
*/
(execl实现sleep)
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
// sleep 100
int main()
{
pid_t pid;
puts("Begin!");
fflush(NULL); /*!!!*/
pid = fork();
if(pid < 0)
{
perror("fork()");
exit(1);
}
if(pid == 0)
{
execl("/bin/sleep","sleep","100",NULL);
perror("execl()");
exit(1);
}
wait(NULL);
puts("End!");
exit(0);
}
(myshell)
#include <stdio.h>
#include <stdlib.h>
#include <glob.h>
#include <unistd.h>
#include <string.h>
#define DELIMS " \t\n"
struct cmd_st
{
glob_t globres;
};
void prompt(void)
{
printf("[mysh-0.1]$ ");
}
int parse(char *line,struct cmd_st *res)
{// "ls -l -a /etc" -> "ls" "-l" "-a"
char *tok;
int i = 0;
while(1)
{
tok = strsep(&line,DELIMS);
if(tok == NULL)
break;
if(tok[0] == '\0')
continue;
glob(tok,GLOB_NOCHECK|GLOB_APPEND*i,NULL,&res->globres);
i = 1;
}
return 0;
}
int main()
{
char *line = NULL;
size_t linesize = 0;
struct cmd_st cmd;
pid_t pid;
while(1)
{
prompt();//打印提示符
if(getline(&line,&linesize,stdin) < 0)//从输入流中读入一行
break;
// line-> ls -l -a /etc
parse(line,&cmd);//解析
if(0/*内部命令*/)
{ }
else /*外部命令*/
{
pid = fork();
if(pid < 0)
{
perror("fork()");
exit(1);
}
if(pid == 0)
{
execvp(cmd.globres.gl_pathv[0],cmd.globres.gl_pathv );
perror("execvp()");
exit(1);
}
wait(NULL);
}
}
//globfree();
exit(0);
}
5、用户权限及组权限
u+s
g+s
获取用户身份:
uid_t getuid(void);
返回值:返回调用进程的实际用户ID。
uid_t geteuid(void);
返回值:返回调用进程的有效用户ID。
获取组身份:
gid_t getgid(void);
返回值:返回调用进程的实际组ID。
gid_t getegid(void);
返回值:返回调用进程的有效组ID。
设置实际用户ID和有效用户ID:int setuid(uid_t uid);
设置实际组ID和有效组ID:int setgid(gid_t gid);
返回值:成功,返回0;出错,返回-1.
交换实际用户ID和有效用户ID值:
int setreuid(uid_t ruid, uid_t euid);
int setregid(gid_t rgid, gid_t egid);
返回值:成功,返回0;出错,返回-1.
只更改有效用户ID和有效用户ID:
int seteuid(uid_t uid);
int setegid(gid_t gid);
返回值:成功,返回0;出错,返回-1.
6、观摩课:解释器文件
解释器文件(脚本文件)
exec 函数支持解释器文件
execl(“/aaa/bbb/ccc.sh”, “a0”, “a1”, NULL);
一般的解释器文件格式为 #!/xxx/yyy arg0 arg1 ...
当 exec 执行的是一个解释器文件时,其实并不执行文件本身,而是执行#!/xxx/yyy
yyy 程序从命令行得到的参数是
argv[0]= /xxx/yyy
argv[1]=arg0
argv[2]=arg1
...
argv[n]= /aaa/bbb/ccc.sh
argv[n+1]=a1
7、system
函数 system = fork + exec + wait
在代码中用 system 函数可以执行 shell 命令
执行一个shell命令:int system(const char *command);
注意:system 是 fork 然后用一个新的 shell 去执行 cmmand
#include <stdio.h>
#include <stdlib.h>
int main()
{
system("date +%s > /tmp/out");
exit(0);
}
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
// system -> date +%s
int main()
{
pid_t pid;
puts("Begin!");
fflush(NULL); /*!!!*/
pid = fork();
if(pid < 0)
{
perror("fork()");
exit(1);
}
if(pid == 0)
{
execl("/bin/sh","sh","-c","date +%s",NULL);
perror("execl()");
exit(1);
}
wait(NULL);
puts("End!");
exit(0);
}
8、进程会计
作用:记录每一个进程的结束。
进程会计记录了什么时间启动了什么进程,进程的属主是什么,进程的结束时间等信息。
acct:启用和禁止进程会计。
开关进程会计:int acct(const char *filename);
返回值:成功,返回0;出错,返回-1并设置errno.
复制代码
会计记录结构定义在头文件<sys/acct.h>中(Linux中,该头文件在/usr/include目录下),其形式如下:
typedef u_short comp_t; /* 3-bit base 8 exponent; 13-bit fraction */
struct acct
{
char ac_flag; /* flag */
char ac_stat; /* termination status (signal & core flag only) */
/* (Solaris only) */
uid_t ac_uid; /* real user ID */
gid_t ac_gid; /* real group ID */
dev_t ac_tty; /* controlling terminal */
time_t ac_btime; /* starting calendar time */
comp_t ac_utime; /* user CPU time (clock ticks) */
comp_t ac_stime; /* system CPU time (clock ticks) */
comp_t ac_etime; /* elapsed time (clock ticks) */
comp_t ac_mem; /* average memory usage */
comp_t ac_io; /* bytes transferred (by read and write) */
/* "blocks" on BSD system */
comp_t ac_rw; /* blocks read or written */
/* (not present on BSD system) */
char ac_comm[8]; /* command name: [8] for Solaris, */
/* [10] for Mac OS X, [16] for FreeBSD, and */
/* [17] for Linux */
};
9、进程时间
times()获取自己以及已终止进程的值(墙上时钟时间、用户CPU时间和系统CPU时间)
clock_t times(struct tms *buf);
返回值:成功,返回的是真实时间,buf中返回系统时间和用户时间(以时钟滴答数为单位);出错,返回-1.
buf指向的tms结构体:
struct tms {
clock_t tms_utime; /* user time */
clock_t tms_stime; /* system time */
clock_t tms_cutime; /* user time of children */
clock_t tms_cstime; /* system time of children */
};
注意:如果要测试一段代码的执行时间,应该在开始调用一次 times 结束之后再调用times一次,取两次的差值便是程序的执行时间,
但是 times 获取到的时间都是时钟嘀嗒数,需要除以 sysconf(_SC_CLK_TCK)才能换算成秒.
10、守护进程
Linux Daemon(守护进程)是运行在后台的一种特殊进程。
它独立于控制终端并且周期性地执行某种任务或等待处理某些发生的事件。
会话session,标识sid
终端
会话
登陆系统时候会创建一个会话(始于用户登录,终于用户退出)
Linux 下每打开一个终端都会创建一个会话
一个会话是多个进程组的组合
获得进程的会话ID:pid_t getsid(pid_t pid);
建立一个新会话:pid_t setsid(void);
1.调用进程变成会话首进程
2.调用进程成为组长进程
3.调用进程即将没有控制终端(收不到终端的 crtl+c 信号)
注意:组长进程不能建立新会话,新会话的首进程一定是组长进程
注意:父进程结束,子进程变为后台进程组
编写守护进程的一般步骤步骤:
(1)在父进程中执行fork并exit推出;
(2)在子进程中调用setsid函数创建新的会话;
(3)在子进程中调用chdir函数,让根目录 ”/” 成为子进程的工作目录;
(4)在子进程中调用umask函数,设置进程的umask为0;
(5)在子进程中关闭任何不需要的文件描述符
Linux中的进程与控制终端,登录会话和进程组之间的关系:进程属于一个进程组,
进程组号(GID)就是进程组长的进程号(PID)。登录会话可以包含多个进程组。
这些进程组共享一个控制终端。这个控制终端通常是创建进程的登录终端。
setsid();
getpgrp();
getpgid();
setpgid();
进程组:同一个进程组中的进程的进程组 ID 一样,同一个进程组中的进程接收来自同一终端的各种信号.
用管道连接到一起的进程在一个进程组
cat /dev/sda >/dev/null | cat /dev/sda >/dev/null
刚 fork 出来的子进程和父进程在同一个组
获得进程的进程组 ID
pid_t getpgrp(void);
/* POSIX.1 version */
pid_t getpgid(pid_t pid);
/* SUS */
设置进程的进程组 ID
int setpgrp(void);
int setpgid(pid_t pid, pid_t pgid);
/* System V version */
注意:查看进程 ps -eo comm,pid,ppid,pgid
注意:一个进程只能设置自己或者子进程的进程组 ID
注意:由 bash shell 启动的新进程是一个独立的进程组(和 bash 不是同一个组)
单实例守护进程:锁文件 /var/run/name.pid
启动脚本文件:/etc/rc* ->local
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <syslog.h>
#include <errno.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#define FNAME "/tmp/out"
int deamonize()
{
pid_t pid;
int fd;
pid = fork();
if(pid < 0)
{
syslog(LOG_ERR,"fork():%s",strerror(errno));
return -1;
}
if(pid > 0) //parent
exit(0);
// child
// close(0);
// close(1);
// close(2);
fd = open("/dev/null",O_RDWR);
if(fd < 0)
{
syslog(LOG_WARNING,"open():%s",strerror(errno));
}
else
{
dup2(fd,0);
dup2(fd,1);
dup2(fd,2);
}
setsid();//守护进程
umask(0);
chdir("/");
return 0;
}
int main()
{
FILE *fp;
int i;
openlog("MyDeamon",LOG_PID,LOG_DAEMON);
if(deamonize() != 0)
{
syslog(LOG_ERR,"deamonize() failed.");
exit(1);
}
else
syslog(LOG_INFO,"deamonize() successed.");
/*do sth*/
fp = fopen(FNAME,"w");
if(fp == NULL)
{
syslog(LOG_ERR,"fopen():%s",strerror(errno));
exit(1);
}
syslog(LOG_DEBUG,"%s was opened.",FNAME);
for(i = 0 ; ; i++)
{
fprintf(fp,"%d\n",i);
fflush(fp);
syslog(LOG_DEBUG,"%d was printed.",i);
sleep(1);
}
fclose(fp);
closelog();
exit(0);
}
11、系统日志
日志对任何一个OS、应用软件、服务进程而言都是必不可少的模块。日志文件对于系统和网络安全起到中大作用,同时具有审计、跟踪、排错功能。
可以通过日志文件监测系统与网络安全隐患,以及监测黑客入侵攻击路线。
/var/log
主日志:messages
权限分割层
syslogd服务
将内容交给syslogd,syslogd统一写。
ps axj | grep “syslogd”查看当前syslogd。
openlog();
syslog();
sysclose();
原理:通常,syslog守护进程读取三种格式的记录消息。此守护进程在启动时读一个配置文件。
一般来说,其文件名为/etc/syslog.conf
(注释:if you want to redirect log to other place,you need to change this),
该文件决定了不同种类的消息应送向何处。例如,紧急消息可被送向系统管理员(若已登录),
并在控制台上显示,而警告消息则可记录到一个文件中。
void openlog(const char *ident, int option, int facility);
调用 openlog() 函数改变你的日志消息的呈现方式。这允许你设置一个字符串--ident,
这将被加在你的日志消息的最前面。你可以使用这些来指示哪一个程序正在创建这条消息。
facility 参数记录了一个默认的程序标识码---这在后来使用 syslog() 里会被用到。
默认的 facility 值为 LOG_USER. logopt 参数配置将来要调用 syslog() 函数的行为。
它是0个或多个一下参数的OR运算:
logopt Parameter Description
LOG_PID 加上进程标识符(系统分配给每个进程的一个独一无二的数字标识)
LOG_CONS 如果消息无法记录到日志文件里则发送消息到控制台
LOG_NDELAY 在第一次调用 syslog 函数时打开日志功能
LOG_NDELAY 立即打开日志功能,而不等到第一次记日志时
openlog() 函数将分配和打开一个用来写到日志功能的文件描述符,
你也可以通过调用 closelog() 函数来关闭它。
注意,你无须在调用 syslog() 之前调用 openlog() ,
这是因为如果有需要的话 syslog 它自己也会打开日志功能。
void syslog(int priority, const char *format, ...);
syslog() 函数向日志设备(日志工具 facility)发送日志消息。
每一个消息都有一个 priority(优先级) 参数。
以下是危险等级的降序排列:
Priority Level Description
LOG_EMERG 紧急状况
LOG_ALERT 高优先级故障,如数据库损坏
LOG_CRIT 关键性(critical)错误,如硬件操作失败
LOG_ERR 一般错误
LOG_WARNING 一般警告
LOG_NOTICE 需要注意的特殊条件
LOG_INFO 通知性消息(Information messages)
LOG_DEBUG 调试消息
void closelog(void);