进程的控制

*进程标识符pid

类型是pid_t,一般是有符号的16位整型数。进程号是顺次向下被取得使用的,和进程标识符优先取最小的使用不一样。

getpid

返回当前进程的pid

pid_t getpid(void);

getppid

返回当前进程的父进程的pid

pid_t getppid(void);

*进程的产生fork

通过复制当前的进程来创建一个子进程。注意复制,是一模一样,甚至连运行到的位置都一样,但是也会存在几点不一样,比如fork返回值不一样、pid/ppid不同、味觉信号和文件锁不继承、资源利用率归零。
init进程:是所有进程的祖先进程,pid是1号

pid_t fork(void);

如果成功,将会在父进程中返回子进程的pid,在子进程中返回0。如果失败将在父进程中返回-1,不生成子进程,并设置errno。

使用样例

样例1

int main()
{
	pid_t ret;
    printf("[%d] Begin!\n", getpid());

    ret = fork();
    if (ret < 0 )
    {
    	perror("fork()");
        exit(1);
    }
    else if (ret == 0) //child
        printf("[%d] child is working\n", getpid());
    else if (ret > 0) //parent
        printf("[%d] parent is working\n", getpid());

    printf("[%d] End!\n", getpid());
    exit(0);
}
输出:
[228] Begin!
[228] parent is working
[228] End!
[229] child is working
[229] End!

可以看到这里先输出了parent后输出child,但是这并不绝对,有可能是fork后父进程继续执行,子进程挂起;也有可能fork后优先执行子进程,然后父进程挂起。这取决于内核的进程调度算法。
注意这里只输出了一个Begin。但如果这样写命令:./fork > show.txt,那将会这样输出:

[245] Begin!
[245] parent is working
[245] End!
[245] Begin!
[246] child is working
[246] End!

这里输出了两个Begin,为什么呢?因为如果输出到标准输出上,默认是行缓冲,printf("[%d] Begin!\n", getpid());这句最后的换行符使得缓冲区的内容被刷新。现在重定向到了show.txt就变为了全缓冲,输出Begin的printf的换行符不会再刷新缓冲区,当fork时缓冲区的内容也被复制,所以子进程自己的缓冲区中也有了一个begin。所以在fork之前最后加上fflush非常重要。

样例2

父进程生成子进程,每个子进程判断一个数是否为质数

int main()
{
	pid_t pid;
	for (int i = 100; i <= 105; i++)
    {
    	pid_t pid = fork();
        if (pid < 0)
        {
        	perror("fork()");
            exit(1);
        }
        if (pid == 0)
        {
        	int mark = 1;
            for (int j = 2; j * j <= i; j++)
            {
            	if (i % j == 0)
                {
                	mark = 0;
                    break;
                }
            }
            if (mark)
            	printf("%d is a prime\n", i);
            //A
            exit(0);
		}
	}
	//B
	exit(0);
}

有一点要注意,注释A下方的exit(0)一定不能忘记写。如果不写子进程不会退出,而会继续循环然后fork自己的子进程,自己的子进程又会fork子进程…
如果只在代码A处写上sleep(1000),利用ps auf指令会看到进程状态如下:

USER       PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
yzq       1109  0.0  0.0   2360    76 pts/0    S    01:11   0:00 ./fork3
yzq       1108  0.0  0.0   2360    76 pts/0    S    01:11   0:00 ./fork3
yzq       1107  0.0  0.0   2492    76 pts/0    S    01:11   0:00 ./fork3
yzq       1106  0.0  0.0   2360    76 pts/0    S    01:11   0:00 ./fork3
yzq       1105  0.0  0.0   2492    76 pts/0    S    01:11   0:00 ./fork3
yzq       1104  0.0  0.0   2360    76 pts/0    S    01:11   0:00 ./fork3

在A处加上sleep导致父进程会先于子进程终止,最后fork的六个子进程就都变成了孤儿进程而被init回收,init等孤儿进程sleep完之后对它们完成状态收集。
如果只在代码B处写上sleep(1000),进程状态如下显示:

yzq       1202  0.0  0.0   2360   516 pts/0    S+   01:13   0:00  \_ ./fork3
yzq       1203  0.0  0.0      0     0 pts/0    Z+   01:13   0:00      \_ [fork3] <defunct>
yzq       1204  0.0  0.0      0     0 pts/0    Z+   01:13   0:00      \_ [fork3] <defunct>
yzq       1205  0.0  0.0      0     0 pts/0    Z+   01:13   0:00      \_ [fork3] <defunct>
yzq       1206  0.0  0.0      0     0 pts/0    Z+   01:13   0:00      \_ [fork3] <defunct>
yzq       1207  0.0  0.0      0     0 pts/0    Z+   01:13   0:00      \_ [fork3] <defunct>
yzq       1208  0.0  0.0      0     0 pts/0    Z+   01:13   0:00      \_ [fork3] <defunct>

这里fork3作为父进程,fork了6个子进程。因为父进程有sleep(1000),所以子进程肯定会先于父进程终止,但是父进程还在sleep,没有采取什么措施来对这些子进程进行善后,这6个子进程就变成了“僵尸进程”。等父进程sleep完exit后,这6个进程就变了孤儿进程,再由init接管。
出现僵尸进程很正常,但是正常应该一闪即逝,如果一直存在那么其进程号就会一直被占用。系统的进程号有限,产生大量的僵尸进程,就会因为没有可用的进程号而导致系统不能产生新的进程。所这里的代码并不完善,下一章的资源释放可以解决僵尸进程的问题。

vfork

如果父进程有很多数据,fork一个子进程只做了很简单的一些操作比如打印一句话,那么拷贝父进程的数据就是吃力不讨好。由此出现了vfork,vfork的子进程共享父进程的地址空间。vfork还有个限制,子进程生成后,父进程在vfork中被内核挂起,直到子进程exec或_exit。并且在此之前,子进程不能从调用vfork的函数中返回(同时,不能修改栈上变量、 不能继续调用除_exit或exec系列之外的函数,否则父进程的数据可能被改写)
但是现在的fork加入了写时拷贝技术:fork后父子会共用同一个数据块,如果某个进程要对数据块进行改写,此时该进程才会拷贝一份地址空间,然后在拷贝的地址空间上进行修改。这个技术导致vfork逐渐淘汰

*进程的消亡及资源释放

wait

当进程一旦调用wait,就会立刻阻塞自己,由wait自己分析当前进程的某个子进程是否已经退出,如果找到了某个已经变成僵尸的子进程,wait就会回收这个子进程的信息,然后销毁最后返回被收集的子进程的ID;如果没有僵尸进程,则wait会一直阻塞直到僵尸进程出现。

pid_t wait(int *wstatus);

参数wstatus用来保存被收集进程退出时的一些状态,配合一些宏使用。如果不在意子进程如何被回收,可以设置为NULL。

  • WIFEXITED(wstatus)
    正常终止子进程返回真
  • WEXITSTATUS(wstatus)
    只能在WIFEXITED为真时使用,可以用这个宏提取子进程返回的值
  • WIFSIGNALED(wstatus)
    如果子进程是被一个信号终止的,则返回真

如果成功,wait会返回被收集的子进程的进程ID,如果调用进程没有子进程,调用就会失败,此时wait返回-1

waitpid

可以看到wait不能收集一个指定的进程信息,但是waitpid可以。waitpid除了回收指定进程外还有一个更有用的选项:option,提供了一些额外的选项来控制waitpid

pid_t waitpid(pid_t pid, int *wstatus, int options);
  • pid > 0时,只回收进程ID等于pid的子进程,不管其它已经有多少子进程运行结束退出了,只要指定的子进程还没有结束,waitpid就会一直等下去。
  • pid = 0时,回收同一个组中的所有子进程,如果子进程已经加入了别的进程组,waitpid不会对它做任何操作
  • pid = -1时,回收任何一个子进程
  • pid < -1时,回收一任何子进程,这个进程所属组的ID是pid的绝对值。

而option有个常用的WNOHANG,如果没有子进程退出,将会立即返回,不会像wait一样阻塞等待僵尸进程出现。

所以wait(int *wstatus);等同于waitpid(-1, &wstatus, 0);

进程分配

之前求质数的代码中用6个子进程求六个数,其实这种实现非常不好。如果这里不止6个,而是6000、60000,将会占用非常多的进程号,甚至可能不够用。所以有几个方法来优化:

分块法

只需要几个进程(假设3个),然后将需要计算的数平均分成3份,每份交给对应的进程计算
在这里插入图片描述
可以看到每个进程负载程度不一样,质数在值偏小的地方分布比较密集,所以进程1的计算量比2、3大很多。

交叉分配

第一个数给1,第二个数给2,第三个数给3,第四个数给1…这样按顺序分
在这里插入图片描述
可以看到分配给进程1的数是0、3、6、9…到后面都是3的倍数,说明都不是质数,也就是说到后面进程1一个质数都没有,分配还是不均。这里因为数值的特殊性导致分配不均,但是交叉分配是使用比较广泛的一种分配方法
交叉分配法演示:

int main()
{
	pid_t pid;

    for (int n = 0; n < N; n++)
    {
    	pid = fork();
    	
        if (pid < 0)
        {
        	perror("fork()");
			/*
				这里应该写上回收前面子进程的代码
			*/
            exit(1);
        }
        if (pid == 0) //child
        {
        	for (int i = LEFT + n; i <= RIGHT; i += N)
            {
            	int mark = 1;
                for (int j = 2; j * j <= i; j++)
                {
                	if (i % j == 0)
                    {
                    	mark = 0;
                        break;
                    }
                }
                if (mark)
                	printf("[%d]: %d is a prime\n", n, i);
        	}
        exit(0);
		}
	for (int n = 0; n < N; n++)
    	wait(NULL);
	exit(0);
}

进程池

上游丢下质数放到一个容器里,子进程们在容器中抢任务
在这里插入图片描述
这是能者多劳的做法,如果进程拿到非质数可以多算几个,拿到质数可以少算几个。但是要考虑进程间的通信和竞争

*exec函数族

fork函数是用于创建一个子进程,该子进程几乎是父进程的副本,而有时我们希望子进程去执行另外的程序,exec函数族就提供了一个在进程中启动另一个程序执行的方法。它可以根据指定的文件名或目录名找到可执行文件,并用它来取代调用它的子进程的数据段、代码段和堆栈段,在执行完之后,调用进程的内容除了pid外,其他全部被新程序的内容替换了。另外,这里的可执行文件既可以是二进制文件,也可以是Linux下任何可执行脚本文件。

int execl(const char *pathname, const char *arg, ...
                       /* (char  *) NULL */);
int execle(const char *pathname, const char *arg, ...
                       /*, (char *) NULL, char *const envp[] */);
int execv(const char *pathname, char *const argv[]);
int execlp(const char *file, const char *arg, ...
                       /* (char  *) NULL */);
int execvp(const char *file, char *const argv[]);

pathname表示要启动程序的名称包括路径名,arg参数表示启动程序所带的参数,一般是第一个参数要执行的命令名,不用带路径且arg必须以NULL结束

如果任何exec函数有返回值,说明失败了(成功了就被新的进程替换了),会返回-1并设置error

使用样例

样例1

int main(int argc, char *argv[])
{
        puts("Begin!");
        fflush(NULL); //别忘了fflush,和fork一样

        execl("/bin/date", "date", "+%s", NULL);
        perror("execl()");
        exit(1);

        puts("End!");
        exit(0);
}

最后的end根本不会输出,因为这个进程调用了execl后已经被替换为date了

样例2

现在让父进程fork子进程,在子进程里调用execl,最后父进程回收子进程,同时输出End

int main()
{
        puts("Begin");

        fflush(NULL);
        int pid = fork();
        if (pid < 0)
        {
                perror("fork()");
                exit(1);
        }
        if (pid == 0) //是子进程
        {
                execl("/bin/date", "date", "+%s", NULL);
                perror("execl()");
                exit(1);
        }
        else if (pid > 0)
                wait(NULL);

        puts("End");
        exit(0);
}

用户权限及组权限

普通用户无法查看shadow文件,但是却可以利用passwd指令修改自己的密码,要修改密码又必须使用shadow文件,这不是矛盾了吗?
先学点概念。一个进程里存储三个userID,分别是real user id、effective user id和saved set-user-id。其中real user id就是真正启动进程的user id,就是谁调用的,一般不会被更改。如果一个可执行文件的ser-user-id bit=ON,当一个进程exec这个可执行文件后,进程的effective user id将会是这个可执行文件的属主id,权限也变成和这个属主id一样。换句话说,effective user id决定了进程访问文件的权限。而saved set-user-id则是effective user ID的副本,在执行exec调用时后能重新恢复原来的effectiv user ID。
现在看刚开始提出的问题。在shell中使用passwd指令,首先shell会fork一个子进程,然后exec可执行文件passwd,由于passwd设置了set-user-id位,而属主id是root,所以进程将会以root执行这个passwd

函数族

uid_t getuid(void); //返回real user id
uid_t geteuid(void); //返回effective user id
gid_t getgid(void); //返回real group id
gid_t getegid(void); //返回effective group id

int setuid(uid_t uid);
int setgid(uid_t uid);

int seteuid(uid_t euid);
int setegid(gid_t egid);

int setreuid(uid_t ruid, uid_t euid); //交换real uid和effective uid
int setregid(gid_t rgid, gid_t egid); //交换real gid和effective gid

使用样例

实现一个mysu,输入./mysu 0 cat /etc/shadow就可以用root查看shadow文件

int main(int argc, char *argv[])
{
        if (argc < 3)
        {
                fprintf(stderr, "Usage: %s <userID> <cmd>\n", argv[0]);
                exit(1);
        }

        pid_t pid = fork();
        if (pid < 0)
        {
                perror("fork()");
                exit(1);
        }

        if (pid == 0)
        {
                setuid(atoi(argv[1])); //设置effective user id为argv[1]的属主id
                execvp(argv[2], argv + 2);
                perror("execvp()");
                exit(1);
        }
        else if (pid > 0)
                wait(NULL);

        exit(0);
}

system

用于执行一个shell命令,内部通过/bin/sh -c <cmd>实现,其实是:

system("date +%s");
//相当于下面三个函数的封装
fork()
execl("/bin/sh", "sh", "-c", "date + %s", NULL);
wait(NULL);

进程会计

一个进程消亡时将信息存到filename指定的结构体中

int acct(const char *filename);

进程时间

用time命令可以测得执行一个指令所消耗的时间,而time命令在内部是用times函数实现的

clock_t times(struct tms *buf);

struct tms
{
	clock_t tms_utime; //命令在用户态执行时间
	clock_t tms_stime; //命令在核心态执行时间
	clock_t tms_cutime; //tms_utime+子进程的tms_utime时间
	clock_t tms_cstime; //tms_cstime+子进程的tms_cstime时间
};

守护进程

先学点概念。我们常见的 Linux session(会话)一般是指 shell session。Shell session是终端中当前的状态,在终端中只能有一个 session。当我们打开一个新的终端时,总会创建一个新的 shell session。就进程间的关系来说,session 由一个或多个进程组组成。一般情况下,来自单个登录的所有进程都属于同一个 session。当创建一个进程时,它和父进程在同一个进程组、session 中。我们可以通过下图来理解进程、进程组和 session 之间的关系:
在这里插入图片描述
会话是由会话中的第一个进程创建的,一般情况下是打开终端时创建的 shell 进程。该进程也叫 session 的领头进程。Session 中领头进程的 PID 也就是 session 的 SID。
进程组又被称为job,有一个 job 会成为 session 的前台 job(foreground),其它的 job 则是后台 job(background)。每个 session 连接一个控制终端(control terminal),控制终端中的输入被发送给前台 job,从前台 job 产生的输出也被发送到控制终端上。
在子进程中调用setsid()后,子进程成为新会话首进程

pid_t setsid(void);

该函数可以建立一个新的会话,前提是调用setsid的进程不是一个进程组的组长(如果父进程fork一个子进程,则父进程是这个组的组长,所以只有子进程可以调用)。调用函数之后子进程将会变成该新建会话的领头进程,同时还变成该会话中新的进程组的组长
守护进程创建步骤:
  1. 创建子进程,父进程退出
    所有工作在子进程中进行
    形式上脱离了控制终端
  2. 在子进程中创建新会话
    setsid()函数
    使子进程完全独立出来,脱离控制
  3. 改变当前目录为根目录
    chdir()函数
    防止占用可卸载的文件系统
    也可以换成其它路径
  4. 重设文件权限掩码
    umask()函数
    防止继承的文件创建屏蔽字拒绝某些权限
    增加守护进程灵活性
  5. 关闭文件描述符
    继承的打开文件不会用到,浪费系统资源,无法卸载
    getdtablesize()
    返回所在进程的文件描述符表的项数,即该进程打开的文件数目

使用样例

int main()
{
	if (daemonize())
    	exit(1);

	FILE *fp = fopen("./test.txt", "w");
    if (fp == NULL)
    {
    	perror("fopen()");
        exit(1);
    }
    for (int i = 0; ; i++)
    {
    	fprintf(fp, "%d\n", i);
        fflush(fp);
        sleep(1);
    }

	fclose(fp);
    exit(0);
}
static int daemonize()
{
	pid_t pid = fork();
    if (pid < 0)
    {
    	perror("fork()");
        return -1;
    }

    if (pid > 0)
    	return 0;
    else if (pid == 0)
    {
    	int fd = open("/dev/null", O_RDWR);
        if (fd < 0)
        {
        	perror("open()");
            return -1;
        }
        dup2(fd, 0);
        dup2(fd, 1);
        dup2(fd, 2);
        if (fd != 2)
        	close(fd);

        setsid();
        chdir("/");
        umask(0);

        return 0;
	}
}

系统日志

每个人都有权限写日志,但是每个人写法都不一致,所以有个名为syslogd的服务。每个人通过一个函数接口将要写的日志提交给这个服务,然后由这个服务来写日志。所以实际只有syslogd这个服务有权利写日志。
以下三个函数就是一套系统日志写入接口:

void openlog(const char *ident, int option, int facility);
void syslog(int priority, const char *format, ...);
void closelog(void);
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值